| 本人只是原創翻譯,而且翻譯也不一定好,純當鍛煉。內容如果英文好的同學,建議直接去看英文原版,比較爽。
NBAOL系列2代產品是 windows平台64位的應用程序,在技術測試過程中,遇到一些crash。但是通過生成的pdb文件卻無法找到崩潰的地址。后來在網上看到了X64 Deep Dive這篇文章,覺得寫得非常好,也解決了我的問題。這篇文章里面的知識點需要經常拿出來研究,本人英文不太好,每次看起來非常吃力,所以決定花點時間把這篇文章翻譯下,以備需要用的時候拿出來看看,內容有錯誤的地方歡迎大家指正。 如果文章有任何侵權的地方,請聯系本人,本人會第一時間修改,謝謝!
這篇文章主要討論X64架構體系代碼執行的幾個主要方面,比如編譯優化、異常處理、參數傳遞和參數回溯以及這幾個方面的內在聯系。文章會介紹理解這幾個概念常用的調試命令,以及理解和解釋這些命令所需的背景知識,同時會揭示X64和X86 CPU體系之間的不同點,以及不同的架構體系對調試方式帶來的影響。如何獲取X64下函數調用的參數值一直是一個挑戰,文章最后會運用這些知識去從X64調用棧當中獲取基於寄存器傳遞的參數值。本教程會一步一步的揭示相關內容,采用圖、反匯編列表和調試器輸出去詮釋相關要點。讀者通過這篇文章可以充分理解X86 CPU 相關知識,比如寄存器、棧以及函數調用。主要內容分為四部分:編譯器優化、異常處理、參數傳遞、參數獲取。
編譯器優化
本節討論影響X64架構下影響代碼生成的一些編譯選項。首先介紹下X64的寄存器,然后介紹一些常用的優化,比如函數內聯、尾調用消除、調用棧幀指針優化以及基於棧指針的局部變量存取。
寄存器變更
X64架構下,除了段寄存器和EFlags(狀態寄存器),其他所有的寄存器都是64位的 ,也就是這些寄存器處理數據的寬度是64 bit,它們能夠一次性處理64 bit的數據,所以X64是一個原生的64位處理器。X64 新增了8個新的寄存器r8-r15,不同於其他寄存器通過字母來標識,新增的8個寄存器通過數字來標識。在Windbg中,通過命令R可以顯示所有寄存器的值。
1: kd> r rax=fffffa60005f1b70 rbx=fffffa60017161b0 rcx=000000000000007f rdx=0000000000000008 rsi=fffffa60017161d0 rdi=0000000000000000 rip=fffff80001ab7350 rsp=fffffa60005f1a68 rbp=fffffa60005f1c30 r8=0000000080050033 r9=00000000000006f8 r10=fffff80001b1876c r11=0000000000000000 r12=000000000000007b r13=0000000000000002 r14=0000000000000006 r15=0000000000000004 iopl=0 nv up ei ng nz na pe nc cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00000282 nt!KeBugCheckEx: fffff800`01ab7350 48894c2408 mov qword ptr [rsp+8],rcx ss:0018:fffffa60`005f1a70=000000000000007f
一些寄存器的作用有別於X86,總結如下:
非易失性寄存器(Non-volatile register)是它的內容在子程序調用過程中必須被保存的一個寄存器。如果一個程序改變了一個非易失性寄存器的值,它必須在改變這個寄存器之前在堆棧中保存舊的值和在函數返回之前恢復那個值。X64保留X86體系下的非易失性寄存器,並新增了R12-R15。在基於寄存器進行傳參的調用來說,這些寄存器是非常重要的。易失寄存器是由調用方假想的臨時寄存器,並要在調用過程中銷毀。非易失寄存器需要在整個函數調用過程中保留其值,並且一旦使用,則必須由被調用方保存,並在調用結束之后恢復,這樣對於調用者來說,原先的值保持不變,並未丟失。
- 快速調用通過寄存器進行傳參。快速調用是X64架構下默認的調用約定,函數的前四個參數通過RCX, RDX, R8, R9進行傳遞。RAX 用來傳遞返回值,RCX用來傳遞this指針,RDX用來配合RAX,傳遞超長返回值。
- RBP不再作為棧幀指針,即某一調用過程在棧中的開始地址。它作為和RBX、RCX等通用寄存器一樣的存在。調試器再也無法通過RBP去回溯函數調用堆棧。
- 在X86架構下, FS段寄存器用來存儲線程塊和內核進程控制區域。但是在X64架構下, GS寄存器在user mode的時候存儲的是線程塊,而在kernelmode 存儲的是內核進程控制區域。WOW64 (Windows-on-Windows 64-bit)是一個Windows操作系統的子系統, 它為現有的 32 位應用程序提供了 32 位的模擬,可以使大多數 32 位應用程序在無需修改的情況下運行在 Windows 64 位版本上。所以WOW64程序,FS寄存器仍然用來存儲32位程序的線程塊。
Trapframe保存的都是一些系統關鍵的寄存器。nt!_KTRAP_FRAME 在X64架構下無法顯示非易失性寄存器的有效值。如果子函數需要復寫非易失性寄存器的值,該函數的前置指令會保存即將被重寫的寄存器的值至棧當中,調試器可以棧當中獲取這些寄存器的值,無需通過 the trap frame去獲取。X64 kernel模式的debugging, 如下圖所示,.trap 會給出提示,通過該命令獲取的寄存器是不准確的,當然也有一些例外,比如從user mode 轉換為Kernel mode的trap frames 是能夠正確獲取所有寄存器的值。
1: kd> kv Child-SP RetAddr : Args to Child . nt!KiDoubleFaultAbort+0xb8 (TrapFrame @ fffffa60`005f1bb0) . 1: kd> .trap fffffa60`005f1bb0 NOTE: The trap frame does not contain all registers. Some register values may be zeroed or incorrect
函數內聯
如果滿足特定的條件,X64編譯器會執行內聯展開,將函數調用的地方直接替換為函數本體。雖然內聯不是X64特有的,但是X64非常熱衷於使用內聯函數。內聯的優勢是無需設置棧、直接切換至被調用函數以及返回調用者。當然內聯會對代碼進行復制,生成的可執行文件會變大,函數的擴展會導致Cache miss,從而引起更多的Page faults。在調試的時候,如果對一個編譯器選擇為內聯的函數設置斷點的時候,調試器無法找到該內聯函數對應的symbol。源文件基本的內聯控制可以通過編譯選項 /OB來控制。編譯器的內聯可以通過__declspec(noinline)來禁用。圖1顯示了Function2和Function3在Fuction1內被內聯化。
Figure 1 :Function In-lining
尾調用消除
X64編譯器會將函數的最后一個函數調用替換為一個到被調用函數的跳轉指令,以此來減少被調用函數棧幀的開銷。調用者和被調用者共享同一個棧幀,被調用者直接返回調用者的調用者。但調用者和被調用者具有相同參數的時候,這種方式好處是非常明顯的,因為相關參數已經在寄存器當中進行設置,而且沒有變更,無需重新加載。 圖2顯示了在Function1內調用Function4所進行的尾調用消除。Function1直接跳轉至Function4, 但Function4執行完畢,它直接返回調用者Function1。
Figure 2 : Tail Call Elimination
框架指針省略
X86 CPU體系采用EBP作為棧頂,用來獲取當前調用棧當中的參數和局部變量,而X64 並不會利用RBP實現同樣的功能,即RBP不會存放棧幀地址,更多的內容會在下個主題進行介紹。所以在X64下,RBP從它作為棧管理的職責解放出來,做為一個通用寄存器。有一種特殊情況,有些函數利用alloca()來動態分配棧空間,這些函數利用RBP作為棧幀,就像EBP寄存器在X86體系下所做的事情一樣。在X86體系下,ESP在函數運行時會不斷的變化,所以在進入某個函數的時候保持ESP到EBP中會方便程序員訪問參數和局部變量,而且還方便調試器分析函數調用過程中的堆棧情況。EBP不是必須要有的,使用esp來訪問函數參數和局部變量也是可行的,只是會很麻煩。
下圖是X86 函數 KERNELBASE!Sleep的匯編代碼。EBP被作為棧頂。SleepEX是參數被壓入堆棧,然后通過Call指令進行調用。
0:009> uf KERNELBASE!Sleep KERNELBASE!Sleep: 75ed3511 8bff mov edi,edi 75ed3513 55 push ebp 75ed3514 8bec mov ebp,esp 75ed3516 6a00 push 0 75ed3518 ff7508 push dword ptr [ebp+8] 75ed351b e8cbf6ffff call KERNELBASE!SleepEx (75ed2beb) 75ed3520 5d pop ebp 75ed3521 c20400 ret 4.
下面的代碼片段是X64平台下的kernelbase!Sleep()。最大的不同是X64版本非常簡潔,因為其沒有對RBP進行任何的存儲、恢復和設置操作。棧幀指針被省略了,所以沒有任何關於被調用者棧幀的設置。事實上Sleep()和SleepEx()最終采用相同的棧幀,一個典型的尾調用優化的例子。
0:000> uf KERNELBASE!Sleep KERNELBASE!Sleep: 000007fe`fdd21140 xor edx,edx 000007fe`fdd21142 jmp KERNELBASE!SleepEx (000007fe`fdd21150)
在X86下, EBP(擴展基址指針寄存器(extended base pointer) 其內存放一個指針,該指針指向系統棧最上面一個棧幀的底部)最大的作用就是用來獲取參數和局部變量。前面說過,在X64下,RBP並不會指向當前函數的棧幀。所以,X64下RSP寄存器既要做為棧指針,又要做為棧幀的指針,所有棧的操作都基於RSP來執行。因此,X64函數依賴於RSP寄存器在整個函數體內都保持不變,用來做為棧幀的索引來獲取局部變量和參數。因為Push 和Pop指令會改變RSP指針,X64函數將Push和Pop操作限制在函數的前置調用和尾端調用之內。如此一來,RSP會在函數前置端和尾端調用之間保持不變。如圖3所示,函數的前置調用和尾端調用是X64函數的一大特色。
Figure 3 : Static Stack Pointer
下面是user32!DrawTestExW完整的匯編代碼。該函數的前置代碼以"sub rsp, 48h"結束,尾端調用以"add rsp, 48h"開始。因為在兩者之間如果需要操作棧的話,需要用到RSP寄存器,可以發現在函數體內沒有任何Push和Pop操作。
0:000> uf user32!DrawTextExW user32!DrawTextExW: 00000000`779c9c64 sub rsp,48h 00000000`779c9c68 mov rax,qword ptr [rsp+78h] 00000000`779c9c6d or dword ptr [rsp+30h],0FFFFFFFFh 00000000`779c9c72 mov qword ptr [rsp+28h],rax 00000000`779c9c77 mov eax,dword ptr [rsp+70h] 00000000`779c9c7b mov dword ptr [rsp+20h],eax 00000000`779c9c7f call user32!DrawTextExWorker (00000000`779ca944) 00000000`779c9c84 add rsp,48h 00000000`779c9c88 ret
RUNTIME_FUNCTION結構
X64可執行文件格式是X86PE文件格式的一個變種,叫做PE32+。這種文件格式多了一個叫做".pdata"或者異常目錄的節來保存異常處理需要的相關信息。異常目錄為可執行文件內每一個非葉子函數建立一個Runtime_Function結構,非葉子函數是指那些會在函數體內調用其他函數的函數。每一個Runtime_Function結構包含對應函數第一個指令和最后一個指令的偏移,以及描述函數調用棧在發生異常時展開信息結構體的指針。圖4 Module模塊的Runtime_Fuction包含所有函數的開始和結束偏移。
Figure 4 : RUNTIME_FUNCTION
下面的匯編代碼顯示X86和X64異常處理的一些不同點。在X86下,當高級語言(c/c++)存在像__try/__except結構化的異常處理塊的時候,編譯器會在函數始端和末端產生一些特殊代碼,這些代碼用來在函數運行時在棧當中構建一個異常處理幀,如下面代碼ntdll!_SEH_prolog4 and ntdll!_SEH_epilog4當中所展示的那樣。
0:009> uf ntdll!__RtlUserThreadStart ntdll!__RtlUserThreadStart: 77009d4b push 14h 77009d4d push offset ntdll! ?? ::FNODOBFM::`string'+0xb5e (76ffc3d0) 77009d52 call ntdll!_SEH_prolog4 (76ffdd64) 77009d57 and dword ptr [ebp-4],0 77009d5b mov eax,dword ptr [ntdll!Kernel32ThreadInitThunkFunction (770d4224)] 77009d60 push dword ptr [ebp+0Ch] 77009d63 test eax,eax 77009d65 je ntdll!__RtlUserThreadStart+0x25 (77057075) ntdll!__RtlUserThreadStart+0x1c: 77009d6b mov edx,dword ptr [ebp+8] 77009d6e xor ecx,ecx 77009d70 call eax 77009d72 mov dword ptr [ebp-4],0FFFFFFFEh 77009d79 call ntdll!_SEH_epilog4 (76ffdda9) 77009d7e ret 8
然而對於X64的函數,從匯編代碼無法發現函數使用結構化異常的跡象,因為沒有基於棧的運行時異常處理代碼幀。RIP寄存器當中指令對應的Runtime_Function結構用來定位的是可執行文件當中對應的異常處理信息,跟具體函數無關。也就說當發生異常的時候,系統會根據當前RIP的地址找到對應的異常處理函數,並調用。
0:000> uf ntdll!RtlUserThreadStart Flow analysis was incomplete, some code may be missing ntdll!RtlUserThreadStart: 00000000`77c03260 sub rsp,48h 00000000`77c03264 mov r9,rcx 00000000`77c03267 mov rax,qword ptr [ntdll!Kernel32ThreadInitThunkFunction (00000000`77d08e20)] 00000000`77c0326e test rax,rax 00000000`77c03271 je ntdll!RtlUserThreadStart+0x1f (00000000`77c339c5) ntdll!RtlUserThreadStart+0x13: 00000000`77c03277 mov r8,rdx 00000000`77c0327a mov rdx,rcx 00000000`77c0327d xor ecx,ecx 00000000`77c0327f call rax 00000000`77c03281 jmp ntdll!RtlUserThreadStart+0x39 (00000000`77c03283) ntdll!RtlUserThreadStart+0x39: 00000000`77c03283 add rsp,48h 00000000`77c03287 ret ntdll!RtlUserThreadStart+0x1f: 00000000`77c339c5 mov rcx,rdx 00000000`77c339c8 call r9 00000000`77c339cb mov ecx,eax 00000000`77c339cd call ntdll!RtlExitUserThread (00000000`77bf7130) 00000000`77c339d2 nop 00000000`77c339d3 jmp ntdll!RtlUserThreadStart+0x2c (00000000`77c53923)
回溯信息和回溯代碼
Runtime_Function當中的BeginAddress 和 EndAddress分別包含了函數起始地址和結束地址相對於模塊基地址在虛擬內存當中的偏移。當函數發生異常的時候,操作系統掃描PE文件鏡像當中尋找包含當前指令的的Runtime_Function結構, Runtime_Function結構包含了Unwind_Info結構體的指針,Unwind_Info告訴操作系統如何進行棧展開。在運行時期間從函數調用棧中刪除函數實體,稱為棧展開。棧展開通常用於異常處理。在C++中,如果一個異常發生了,會線性的搜索函數調用棧,來尋找異常處理者,並且帶有異常處理的函數之前的所有實體都會從函數調用棧中刪除。所以,如果異常沒有在拋出它的函數中被處理,則會激活棧展開。Unwind_Info包含了一系列的Unwind_Code結構體,這些結構體能夠還原函數始端代碼對棧所進行的操作,其實就是去除當前函數體,使其對上層調用者無影響。
對於動態生成的代碼, 操作系統提供了RtlAddFunctionTable() 和RtlInstallFunctionTableCallback(),用來在運行時創建Runtime_Function信息。
圖5展示了Runtime_Function和Unwind_Info直接的關系,以及函數在內存中的位置。
Figure 5 : Unwind Information
.Fnet命令顯示了指定函數對應的Runtime_Function結構。下面展示了ntdll!RtlUserThreadStart 函數對應的”.fnet”輸出。
0:000> .fnent ntdll!RtlUserThreadStart Debugger function entry 00000000`03be6580 for: (00000000`77c03260) ntdll!RtlUserThreadStart | (00000000`77c03290) ntdll!RtlRunOnceExecuteOnce Exact matches: ntdll!RtlUserThreadStart = BeginAddress = 00000000`00033260 EndAddress = 00000000`00033290 UnwindInfoAddress = 00000000`00128654 Unwind info at 00000000`77cf8654, 10 bytes version 1, flags 1, prolog 4, codes 1 frame reg 0, frame offs 0 handler routine: ntdll!_C_specific_handler (00000000`77be50ac), data 3 00: offs 4, unwind op 2, op info 8 UWOP_ALLOC_SMALL
上面輸出中的BeginAddress加上ntdll(RtlUserThreadStart在該dll中)的基地址,最后的結果0x0000000077c03260就是RtlUserThreadStart函數的地址。
0:000> ?ntdll+00000000`00033260 Evaluate expression: 2009084512 = 00000000`77c03260 0:000> u ntdll+00000000`00033260 ntdll!RtlUserThreadStart: 00000000`77c03260 sub rsp,48h 00000000`77c03264 mov r9,rcx 00000000`77c03267 mov rax,qword ptr [ntdll!Kernel32ThreadInitThunkFunction (00000000`77d08e20)] 00000000`77c0326e test rax,rax 00000000`77c03271 je ntdll!RtlUserThreadStart+0x1f (00000000`77c339c5) 00000000`77c03277 mov r8,rdx 00000000`77c0327a mov rdx,rcx 00000000`77c0327d xor ecx,ecx
同理,EndAddress也是同樣的使用方法,結果如下面代碼所示。
0:000> ?ntdll+00000000`00033290 Evaluate expression: 2009084560 = 00000000`77c03290 0:000> ub 00000000`77c03290 L10 ntdll!RtlUserThreadStart+0x11: 00000000`77c03271 je ntdll!RtlUserThreadStart+0x1f (00000000`77c339c5) 00000000`77c03277 mov r8,rdx 00000000`77c0327a mov rdx,rcx 00000000`77c0327d xor ecx,ecx 00000000`77c0327f call rax 00000000`77c03281 jmp ntdll!RtlUserThreadStart+0x39 (00000000`77c03283) 00000000`77c03283 add rsp,48h 00000000`77c03287 ret 00000000`77c03288 nop 00000000`77c03289 nop 00000000`77c0328a nop 00000000`77c0328b nop 00000000`77c0328c nop 00000000`77c0328d nop 00000000`77c0328e nop 00000000`77c0328f nop
Runtime_Function結構的BeginAddress和EndAddress用來定位內存中對應的異常處理函數。當然,這不是必須的,可以在鏈接的時候運用該信息進行優化,這種優化會改變我們上面的看法,待會兒再討論。
雖然Unwind_Info和Unwind_Code的主要作用是在發生異常的時候進行棧展開,但是調試器也可以利用該信息在沒有模塊symbols的時候遍歷堆棧。每一個Unwind_Code對應着函數始端函數的一項操作。
- SAVE_NONVOL – 保存非易失性寄存器的值至堆棧。
- PUSH_NONVOL – 將非易失性寄存器的值壓棧。
- ALLOC_SMALL –在棧上分配空間(最多128字節).
- ALLOC_LARGE – 在棧上分配空間(最多4G).
從本質上講,Unwind_Code是函數前置函數的元數據表示。
圖6展示函數前置函數執行的棧相關的操作以及這些操作對應的Unwind_Code結構。Unwind_Code結構出現的順序和它對應的前置指令出現順序相反。因為當異常發生的時候,棧展開的順序與其創建順序是相反的。
Figure 6 : Unwind Code
下面是X64系統自帶的Notepad.exe的”.pdata”節的信息。"virtual address"字段顯示.pdata節在相對於EXE文件0x13000的地方。
T:\link -dump -headers c:\windows\system32\notepad.exe . . . SECTION HEADER #4 .pdata name 6B4 virtual size 13000 virtual address (0000000100013000 to 00000001000136B3) 800 size of raw data F800 file pointer to raw data (0000F800 to 0000FFFF) 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 40000040 flags Initialized Data Read Only
下圖展示了Notepad.exe的Unwind_Info和Unwind_Code相關的信息。Unwind_Code描述了函數前端執行的PUSH_NONVOL 或者 ALLOC_SMALL,這些操作必須在棧展開的時候被回退。”.fnet” 命令顯示這些結構體的信息,但是"link -dump -unwindinfo"展示的是Unwind_Code所有的信息,而”.Fnet”只顯示了部分的信息。
T:\link -dump -unwindinfo c:\windows\system32\notepad.exe . . . 00000018 00001234 0000129F 0000EF68 Unwind version: 1 Unwind flags: None Size of prologue: 0x12 Count of codes: 5 Unwind codes: 12: ALLOC_SMALL, size=0x28 0E: PUSH_NONVOL, register=rdi 0D: PUSH_NONVOL, register=rsi 0C: PUSH_NONVOL, register=rbp 0B: PUSH_NONVOL, register=rbx. .
上面輸出當中的ALLOC_SMALL 代表着函數前端當中的sub指令,該指令分配了0x28字節的棧空間。PUSH_NONVOL 代表着函數前端指令當中的push操作,該操作將非易失寄存器的內容保存至棧上,然后在函數尾端指令中通過pop操作恢復。如下圖所示:這些指令在函數反匯編0x1234偏移處。
0:000> ln notepad+1234 (00000000`ff971234) notepad!StringCchPrintfW | (00000000`ff971364) notepad!CheckSave Exact matches: notepad!StringCchPrintfW = notepad!StringCchPrintfW = 0:000> uf notepad!StringCchPrintfW notepad!StringCchPrintfW: 00000001`00001234 mov qword ptr [rsp+18h],r8 00000001`00001239 mov qword ptr [rsp+20h],r9 00000001`0000123e push rbx 00000001`0000123f push rbp 00000001`00001240 push rsi 00000001`00001241 push rdi 00000001`00001242 sub rsp,28h 00000001`00001246 xor ebp,ebp 00000001`00001248 mov rsi,rcx 00000001`0000124b mov ebx,ebp 00000001`0000124d cmp rdx,rbp 00000001`00001250 je notepad!StringCchPrintfW+0x27 (00000001`000077b5) ... notepad!StringCchPrintfW+0x5c: 00000001`00001294 mov eax,ebx 00000001`00001296 add rsp,28h 00000001`0000129a pop rdi 00000001`0000129b pop rsi 00000001`0000129c pop rbp 00000001`0000129d pop rbx 00000001`0000129e ret
Windows操作系統的二進制文件受到一種叫做BBT的PGO技術的影響。 PGO技術是先對程序最常用的代碼和函數進行分析,然后利用分析結果對最常用的代碼進行優化,謀求最佳的優化效果。而BBT是編譯器處理代碼片段的最小單元,沒有別的分支,只有唯一的入口和出口,BBT會增加代碼的空間局部性。空間局部性(Spatial Locality)是指在最近的將來將用到的信息很可能與現在正在使用的信息在空間地址上是臨近的。有可能會經常執行的代碼會保持在一起,盡量在一個頁空間內,而其他不經常執行的代碼放置到其他地方。這會減少常用代碼在運行時所需的內存頁的數量,進而減少其所需的物理內存的大小。為了采用這種優化技術,程序會被鏈接、執行然后分析,最后會根據分析數據當中程序的執行頻率重新安排函數的地址。
最終生成的函數中,一些函數代碼被移動到了預先由Runtime_Function擴展定義的函數主體外面,由於代碼塊的移動,函數主體被分為若干獨立的部分,導致鏈接器預生成的Runtime_Function結構體再也無法精確識別這些函數的擴展。為了定位這些問題,BBT程序為每一個鄰近的還有優化函數的代碼塊生成一個Runtime_Function結構。這些Runtime_Function被鏈在一起,結構鏈會停止在BeginAddress指向函數開始地方的Runtime_Function結構。
圖7展示了有三個基本塊組成的函數。當采用BBT只有 Block#2從函數主體內移出,導致原先的Runtime_Function失效。BBT 會創建一個新的Runtime_Function結構然后鏈到第一個Runtime_Function結構,用來描述整個函數。
Figure 7 : Performance Optimization : Basic Block Tools
當前版本調試器不會遍歷鏈上所有的Runtime_Function結構,因此調試器無法正確展示一個優化函數的名字,該優化函數的返回地址與移動到主函數體外面代碼塊相對應。
下圖調用堆棧當中的函數就無法正確顯示函數名。函數名會顯示為
"ntdll! ?? ::FNODOBFM::`string'。調試器錯誤的將返回地址為0x0000000077c17623的幀0x0c翻譯為"ntdll! ??
::FNODOBFM::`string'+0x2bea0".
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0029e4b8 000007fe`fdd21726 ntdll! ?? ::FNODOBFM::`string'+0x6474 01 00000000`0029e4c0 000007fe`fdd2dab6 KERNELBASE!BaseSetLastNTError+0x16 02 00000000`0029e4f0 00000000`77ad108f KERNELBASE!AccessCheck+0x64 03 00000000`0029e550 00000000`77ad0d46 kernel32!BasepIsServiceSidBlocked+0x24f 04 00000000`0029e670 00000000`779cd161 kernel32!LoadAppInitDlls+0x36 05 00000000`0029e6e0 00000000`779cd42d user32!ClientThreadSetup+0x22e 06 00000000`0029e950 00000000`77c1fdf5 user32!_ClientThreadSetup+0x9 07 00000000`0029e980 000007fe`ffe7527a ntdll!KiUserCallbackDispatcherContinue 08 00000000`0029e9d8 000007fe`ffe75139 gdi32!ZwGdiInit+0xa 09 00000000`0029e9e0 00000000`779ccd1f gdi32!GdiDllInitialize+0x11b 0a 00000000`0029eb40 00000000`77c0c3b8 user32!UserClientDllInitialize+0x465 0b 00000000`0029f270 00000000`77c18368 ntdll!LdrpRunInitializeRoutines+0x1fe 0c 00000000`0029f440 00000000`77c17623 ntdll!LdrpInitializeProcess+0x1c9b 0d 00000000`0029f940 00000000`77c0308e ntdll! ?? ::FNODOBFM::`string'+0x2bea0 0e 00000000`0029f9b0 00000000`00000000 ntdll!LdrInitializeThunk+0xe
下面的例子利用返回地址0x0000000077c17623顯示函數帶有錯誤的名字,以及函數對應的Runtime_Function,Unwind_Info以及Unwind_Codes。如果有"Chained Info:"這個目錄,說明函數體內的一些代碼塊被移至函數體外。
0:000> .fnent 00000000`77c17623 Debugger function entry 00000000`03b35da0 for: (00000000`77c55420) ntdll! ?? ::FNODOBFM::`string'+0x2bea0 | (00000000`77c55440) ntdll! ?? ::FNODOBFM::`string' BeginAddress = 00000000`000475d3 EndAddress = 00000000`00047650 UnwindInfoAddress = 00000000`0012eac0 Unwind info at 00000000`77cfeac0, 10 bytes version 1, flags 4, prolog 0, codes 0 frame reg 0, frame offs 0 Chained info: BeginAddress = 00000000`000330f0 EndAddress = 00000000`000331c0 UnwindInfoAddress = 00000000`0011d08c Unwind info at 00000000`77ced08c, 20 bytes version 1, flags 1, prolog 17, codes a frame reg 0, frame offs 0 handler routine: 00000000`79a2e560, data 0 00: offs f0, unwind op 0, op info 3 UWOP_PUSH_NONVOL 01: offs 3, unwind op 0, op info 0 UWOP_PUSH_NONVOL 02: offs c0, unwind op 1, op info 3 UWOP_ALLOC_LARGE FrameOffset: d08c0003 04: offs 8c, unwind op 0, op info d UWOP_PUSH_NONVOL 05: offs 11, unwind op 0, op info 0 UWOP_PUSH_NONVOL 06: offs 28, unwind op 0, op info 0 UWOP_PUSH_NONVOL 07: offs 0, unwind op 0, op info 0 UWOP_PUSH_NONVOL 08: offs 0, unwind op 0, op info 0 UWOP_PUSH_NONVOL 09: offs 0, unwind op 0, op info 0 UWOP_PUSH_NONVOL
“Chained Info”后面的BeingAddress顯示的是原始函數的起始地址。”ln”命令顯示原型函數的名字是ntdll!LdrpInitialize.
0:000> ln ntdll+000330f0 (00000000`77c030f0) ntdll!LdrpInitialize | (00000000`77c031c0) ntdll!LdrpAllocateTls Exact matches: ntdll!LdrpInitialize =
“uf”命令顯示給定地址函數的所有匯編代碼,該命令通過尋找代碼塊當中的jmp/jcc指令遍歷函數當中所有不同的代碼塊。下面的輸出展示了ntdll!LdrpInitialize所有的匯編代碼。函數主體從00000000`77c030f0開始,00000000`77c031b3結束。然而函數的一個代碼塊的地址是00000000`77bfd1a4,這是BBT 優化的結果。調試器嘗試去映射這個地址值最近的符號,這就是前面發現的"ntdll! ??
::FNODOBFM::`string'+0x2c01c"的由來。
0:000> uf 00000000`77c030f0 ntdll! ?? ::FNODOBFM::`string'+0x2c01c: 00000000`77bfd1a4 48c7842488000000206cfbff mov qword ptr [rsp+88h],0FFFFFFFFFFFB6C20h 00000000`77bfd1b0 443935655e1000 cmp dword ptr [ntdll!LdrpProcessInitialized (00000000`77d0301c)],r14d 00000000`77bfd1b7 0f856c5f0000 jne ntdll!LdrpInitialize+0x39 (00000000`77c03129) . . . ntdll!LdrpInitialize: 00000000`77c030f0 48895c2408 mov qword ptr [rsp+8],rbx 00000000`77c030f5 4889742410 mov qword ptr [rsp+10h],rsi 00000000`77c030fa 57 push rdi 00000000`77c030fb 4154 push r12 00000000`77c030fd 4155 push r13 00000000`77c030ff 4156 push r14 00000000`77c03101 4157 push r15 00000000`77c03103 4883ec40 sub rsp,40h 00000000`77c03107 4c8bea mov r13,rdx 00000000`77c0310a 4c8be1 mov r12,rcx . . . ntdll!LdrpInitialize+0xac: 00000000`77c0319c 488b5c2470 mov rbx,qword ptr [rsp+70h] 00000000`77c031a1 488b742478 mov rsi,qword ptr [rsp+78h] 00000000`77c031a6 4883c440 add rsp,40h 00000000`77c031aa 415f pop r15 00000000`77c031ac 415e pop r14 00000000`77c031ae 415d pop r13 00000000`77c031b0 415c pop r12 00000000`77c031b2 5f pop rdi 00000000`77c031b3 c3 ret
進行過BBT優化的模塊可以通過"!lmi"命令的輸出加以識別,在"!lmi"命令輸出的"Characteristics"字段中會含有"perf"。
000> !lmi notepad Loaded Module Info: [notepad] Module: notepad Base Address: 00000000ff4f0000 Image Name: notepad.exe Machine Type: 34404 (X64) Time Stamp: 4a5bc9b3 Mon Jul 13 16:56:35 2009 Size: 35000 CheckSum: 3e749 Characteristics: 22 perf Debug Data Dirs: Type Size VA Pointer CODEVIEW 24, b74c, ad4c RSDS - GUID: {36CFD5F9-888C-4483-B522-B9DB242D8478} Age: 2, Pdb: notepad.pdb CLSID 4, b748, ad48 [Data not mapped] Image Type: MEMORY - Image read successfully from loaded memory. Symbol Type: PDB - Symbols loaded successfully from symbol server. c:\symsrv\notepad.pdb\36CFD5F9888C4483B522B9DB242D84782\notepad.pdb Load Report: public symbols , not source indexed c:\symsrv\notepad.pdb\36CFD5F9888C4483B522B9DB242D84782\notepad.pdb
基於寄存器的傳參
在X64體系下,函數的前四個參數通過寄存器來傳遞,剩余的參數通過棧來進行傳遞。隨着函數的執行,寄存器的值會發生變化,會導致在函數執行過程中難以定位函數傳遞的參數的初始值。除了這點,X64的調試和X86的調試不存在任何差異。
圖8的匯編代碼描繪了參數如何從調用者傳遞給被調用者。
Figure 8 : Parameter Passing on X64
下面的堆棧展示的是kernel32!CreateFileWImplementation調用KERNELBASE!CreateFileW.
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d .
通過MSDN文檔,CreateFileW() 有七個參數,函數原型如下:
HANDLE WINAPI CreateFile( __in LPCTSTR lpFileName, __in DWORD dwDesiredAccess, __in DWORD dwShareMode, __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes, __in DWORD dwCreationDisposition, __in DWORD dwFlagsAndAttributes, __in_opt HANDLE hTemplateFile );
從前面的調用堆棧可以發現,包含函數KERNELBASE!CreateFileW那一幀的返回地址是00000000`77ac2aad。通過反匯編發現函數會在調用kernel32!CreateFileW 之前調用kernel32!CreateFileWImplementation。”mov rcx,rdi", "mov
edx,ebx", "mov r8d,ebp", "mov r9,rsi"這四個指令將kernel32!CreateFileW的前四個參數放入寄存器,"mov dword ptr [rsp+20h],eax",
"mov dword ptr [rsp+28h],eax" and "mov qword ptr
[rsp+30h],rax"這四個指令將剩余的四個參數放入堆棧。
0:000> ub 00000000`77ac2aad L10 kernel32!CreateFileWImplementation+0x35: 00000000`77ac2a65 lea rcx,[rsp+40h] 00000000`77ac2a6a mov edx,ebx 00000000`77ac2a6c call kernel32!BaseIsThisAConsoleName (00000000`77ad2ca0) 00000000`77ac2a71 test rax,rax 00000000`77ac2a74 jne kernel32!zzz_AsmCodeRange_End+0x54fc (00000000`77ae7bd0) 00000000`77ac2a7a mov rax,qword ptr [rsp+90h] 00000000`77ac2a82 mov r9,rsi 00000000`77ac2a85 mov r8d,ebp 00000000`77ac2a88 mov qword ptr [rsp+30h],rax 00000000`77ac2a8d mov eax,dword ptr [rsp+88h] 00000000`77ac2a94 mov edx,ebx 00000000`77ac2a96 mov dword ptr [rsp+28h],eax 00000000`77ac2a9a mov eax,dword ptr [rsp+80h] 00000000`77ac2aa1 mov rcx,rdi 00000000`77ac2aa4 mov dword ptr [rsp+20h],eax 00000000`77ac2aa8 call kernel32!CreateFileW (00000000`77ad2c88)
歸位空間
雖然函數的前四個參數通過寄存器來傳遞,X64仍然會在棧上預留這四個參數的空間,這空間叫做參數歸位空間(我自己定義的,不知道業界純正的翻譯是什么,將就着能用),這些空間在用來存儲參數,一種是在函數通過地址傳遞而非傳值,或者函數是通過/homeparams來編譯。歸位空間的最小大小是32個字節或者64位的slots,就算函數少於4個參數也是如此。當歸位空間未用來存儲參數的時候,編譯器用他來存儲非易失性寄存器。
圖9顯示的是基於寄存器參數的棧上的歸位空間以及函數的前端指令將非易失性寄存器的值保存至歸位空間。
Figure 9 : Parameter Homing Space
下面的例子中,函數前置指令中的"sub rsp, 20h"在棧上分配了32個字節,這對於4個64位值以及足夠了。例子的另外一個部分展示了msvcrt!malloc()是一個非葉子函數,在其中會調用一系列的其他函數。
0:000> uf msvcrt!malloc msvcrt!malloc: 000007fe`fe6612dc mov qword ptr [rsp+8],rbx 000007fe`fe6612e1 mov qword ptr [rsp+10h],rsi 000007fe`fe6612e6 push rdi 000007fe`fe6612e7 sub rsp,20h 000007fe`fe6612eb cmp qword ptr [msvcrt!crtheap (000007fe`fe6f1100)],0 000007fe`fe6612f3 mov rbx,rcx 000007fe`fe6612f6 je msvcrt!malloc+0x1c (000007fe`fe677f74) .0:000> uf /c msvcrt!malloc msvcrt!malloc (000007fe`fe6612dc) msvcrt!malloc+0x6a (000007fe`fe66132c): call to ntdll!RtlAllocateHeap (00000000`77c21b70) msvcrt!malloc+0x1c (000007fe`fe677f74): call to msvcrt!core_crt_dll_init (000007fe`fe66a0ec) msvcrt!malloc+0x45 (000007fe`fe677f83): call to msvcrt!FF_MSGBANNER (000007fe`fe6ace0c) msvcrt!malloc+0x4f (000007fe`fe677f8d): call to msvcrt!NMSG_WRITE (000007fe`fe6acc10) msvcrt!malloc+0x59 (000007fe`fe677f97): call to msvcrt!_crtExitProcess (000007fe`fe6ac030) msvcrt!malloc+0x83 (000007fe`fe677fad): call to msvcrt!callnewh (000007fe`fe696ad0) msvcrt!malloc+0x8e (000007fe`fe677fbb): call to msvcrt!errno (000007fe`fe661918)
下面關於Winmain前置指令的匯編代碼展示了四個非易失性寄存器的值被存儲在棧上的參數歸位空間。
0:000> u notepad!WinMain notepad!WinMain: 00000000`ff4f34b8 mov rax,rsp 00000000`ff4f34bb mov qword ptr [rax+8],rbx 00000000`ff4f34bf mov qword ptr [rax+10h],rbp 00000000`ff4f34c3 mov qword ptr [rax+18h],rsi 00000000`ff4f34c7 mov qword ptr [rax+20h],rdi 00000000`ff4f34cb push r12 00000000`ff4f34cd sub rsp,70h 00000000`ff4f34d1 xor r12d,r12d
參數歸位
前面描述過,X64的非葉子函數會在棧幀上預留參數歸位空間。對於X64的任意一種調用約定,調用者會通過寄存器來傳遞前四個參數。參數歸位可以通過編譯選項/homeparams開啟,該選項只會影響被調用者。該選項在采用Windows開發包環境構建出來的驗證或者調式版本的二進制文件中默認開啟。被調用者從寄存器當中讀取參數的值,然后將其保存至棧上的歸位空間。
圖10的匯編代碼顯示調用者將參數移至特定的寄存器。被調用者由於開啟了/homeparams選項,會將參數值歸位至棧空間。被調用者的前置指令讀取從寄存器當中讀取參數的值,然后將其保存至棧上的歸位空間。
Figure 10 : Parameter Homing
下面的代碼片段展示了寄存器的值被移動到printf的調用者在棧上申請的歸位空間。
0:000> uf msvcrt!printf msvcrt!printf: 000007fe`fe667e28 mov rax,rsp 000007fe`fe667e2b mov qword ptr [rax+8],rcx 000007fe`fe667e2f mov qword ptr [rax+10h],rdx 000007fe`fe667e33 mov qword ptr [rax+18h],r8 000007fe`fe667e37 mov qword ptr [rax+20h],r9 000007fe`fe667e3b push rbx 000007fe`fe667e3c push rsi 000007fe`fe667e3d sub rsp,38h 000007fe`fe667e41 xor eax,eax 000007fe`fe667e43 test rcx,rcx 000007fe`fe667e46 setne al
stene al的意思如下:
if ZF=1 then al=0
if ZF=0 then al=1
al是ax寄存器的低8位,ah是ax寄存器的高8位
000007fe`fe667e49 test eax,eax 000007fe`fe667e4b je msvcrt!printf+0x25 (000007fe`fe67d74b)
棧的使用說明
X64函數的棧幀包含下面的元素:
- 函數返回地址
- 用來將非易失性寄存器的值放入堆棧的函數前置指令
- 函數的局部變量
- 基於棧的參數傳遞
- 基於寄存器傳遞的參數的歸位空間
除了函數返回地址,其他的元素均通過函數前置指令來放置。局部變量、被調用者的棧參數以及參數的歸位空間都通過"sub rsp, xxx"來分配。基於棧的參數預留空間為了滿足被調用者采用了最大參數來進行設置。而基於寄存器的參數歸位空間僅僅是針對非葉子函數。就算被調用的函數少於四個參數,但是棧上仍然會預留四個參數的歸位空間。
圖11展示了X64 CPU體系的函數棧幀分布。圖中RSP的位置是函數執行完前置指令的狀態。
Figure 11 : Stack Usage
0:000> knf # Memory Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 8 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 160 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 03 60 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd 04 a0 00000000`0029be60 000007fe`fe5534af usp10!InitUnistor+0x1d8
下面的匯編代碼展示了CreateFileW的前置指令,首先將R8D和EDX放入參數的歸位空間,然后將rbx,rbp,esi,edi入棧,再為自己的局部變量和參數分配0x138字節的棧空間。
0:000> uf KERNELBASE!CreateFileW KERNELBASE!CreateFileW: 000007fe`fdd24ac0 mov dword ptr [rsp+18h],r8d 000007fe`fdd24ac5 mov dword ptr [rsp+10h],edx 000007fe`fdd24ac9 push rbx 000007fe`fdd24aca push rbp 000007fe`fdd24acb push rsi 000007fe`fdd24acc push rdi 000007fe`fdd24acd sub rsp,138h 000007fe`fdd24ad4 mov edi,dword ptr [rsp+180h] 000007fe`fdd24adb mov rsi,r9 000007fe`fdd24ade mov rbx,rcx 000007fe`fdd24ae1 mov ebp,2 000007fe`fdd24ae6 cmp edi,3 000007fe`fdd24ae9 jne KERNELBASE!CreateFileW+0x449 (000007fe`fdd255ff)
子函數棧空間的起始地址
"k"命令輸出的Child-SP寄存器的值代表了當前棧幀RSP的值,此時函數已經執行完它的前置指令。下一個入棧的值應該就是函數調用之后的返回地址。因為X64函數在執行完其前置指令之后就不會修改RSP的值,所以之后的任何棧的操作都可以通過與RSP的相對地址來尋址,比如棧當中的參數和局部變量的訪問。
圖12顯示了f2的棧幀以及其和“k”命令輸出的RSP值之間的關系。返回地址RA1是函數f2中調用f1的下一條指令,當f1返回之后,就會執行f2當中的該指令。調用棧中的這個返回地址緊挨着RSP2。
Figure 12 :Relationship between Child-SP and function frames
下圖的堆棧中,frame #01的Chind-SP是00000000`0029bc00,該值是CreateFileW()的前置指令執行完畢之后RSP的值。
0:000> knf # Memory Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 8 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 160 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 03 60 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd 04 a0 00000000`0029be60 000007fe`fe5534af usp10!InitUnistor+0x1d8 . . .
從前面的討論可以看出,00000000`0029bc00 前面的內容就是和KERNELBASE!CreateFileW+0x2cd一致的函數返回地址, 該地址是在調用ntdll!NtCreateFile的時候壓入堆棧的。
0:000> dps 00000000`0029bc00-8 L1 00000000`0029bbf8 000007fe`fdd24d76 KERNELBASE!CreateFileW+0x2cd
棧遍歷
在X86體系下, 調試器利用棧幀鏈去獲取整個調用棧,從棧底到棧頂,無需獲取函數所處模塊的符號文件。但是這種棧幀指針鏈在某些情況下會失效,比如函數經過框架指針優化的情況。在這種情況下,編譯器需要模塊的符號來遍歷整個堆棧。
然而X64函數不使用RBP寄存器作為棧幀指針,所以調試器無法使用該指針來遍歷堆棧。相對應的,調試器采用棧指針和棧幀的大小來遍歷堆棧。調試器通過Runtime_Function,Unwind_Info和Unwind_Codeto計算調用堆棧中每一個函數使用的棧空間的大小,將這些值與當前的Child-SPs進行累加用來計算隨后的Child-SPs。
圖13顯示了函數棧幀的分布。當前棧幀的大小可以通過將返回地址、非易失性寄存器、局部變量、基於棧傳遞參數空間大小、以及基於寄存器傳遞的參數的歸位空間的大小累加得到。Unwind_Code結構用來描述入棧的非易失性寄存器的個數以及分配給局部變量和參數的空間。
Figure 13 : Walking the x64 call stack
在下面的棧回溯中,Frame#1 CreateFileW消耗的棧空間是0x160字節。下一節會討論如何計算消耗的棧空間以及調試器如何利用這個大小去計算Frame#2的Child-SP。請注意Frame#1當中函數消耗的棧空間顯示在Frame#2對應的”Memory”列下面。
0:000> knf # Memory Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 8 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 160 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 03 60 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd 04 a0 00000000`0029be60 000007fe`fe5534af usp10!InitUnistor+0x1d8
下面的輸出描述了Unwind_Code對應的操作。有四個非易失性寄存器進行了入棧操作,而且為局部變量和參數分配了0x138字節的棧空間。被移動的非易失性寄存器的值,與入棧操作相反,不會使用任何的棧空間。
0:000> .fnent kernelbase!CreateFileW Debugger function entry 00000000`03be6580 for: (000007fe`fdd24ac0) KERNELBASE!CreateFileW | (000007fe`fdd24e2c) KERNELBASE!SbSelectProcedure Exact matches: KERNELBASE!CreateFileW = BeginAddress = 00000000`00004ac0 EndAddress = 00000000`00004b18 UnwindInfoAddress = 00000000`00059a48 Unwind info at 000007fe`fdd79a48, 10 bytes version 1, flags 0, prolog 14, codes 6 frame reg 0, frame offs 0 00: offs 14, unwind op 1, op info 0 UWOP_ALLOC_LARGE FrameOffset: 138 02: offs d, unwind op 0, op info 7 UWOP_PUSH_NONVOL 03: offs c, unwind op 0, op info 6 UWOP_PUSH_NONVOL 04: offs b, unwind op 0, op info 5 UWOP_PUSH_NONVOL 05: offs a, unwind op 0, op info 3 UWOP_PUSH_NONVOL
將上面所有使用的空間大小進行累加,最終的棧空間消耗是0x158bytes.
0:000> ?138+(8*4) Evaluate expression: 344 = 00000000`00000158
將上面的大小加上返回地址所占的空間就是棧的大小0x160 bytes,這個與前面”knf”顯示的大小是一致的。
0:000> ?158+8 Evaluate expression: 352 = 00000000`00000160
根據”knf”的輸出,調試器將Frame#01的Child-SP的值00000000`0029bc00 加上棧幀大小0x160最終得到Frame#02的值00000000`0029bd60.
0:000> ?00000000`0029bc00+160 Evaluate expression: 2735456 = 00000000`0029bd60
所以棧的每一幀的大小我們可以通過PE文件當中的Runtime_Function、Unwind_Info、Unwind_Code結構體進行計算,所以調試器可以不依賴於符號文件對堆棧進行遍歷。下面的顯示了”vmswitch”模塊的堆棧信息,雖然該模塊的符號信息無法從官網上獲取,但是編譯器仍然可以遍歷棧中該模塊的調用信息,從而得出一個結論,X64的調用棧可以不依賴於符號文件進行遍歷。
1: kd> kn # Child-SP RetAddr Call Site 00 fffffa60`005f1a68 fffff800`01ab70ee nt!KeBugCheckEx 01 fffffa60`005f1a70 fffff800`01ab5938 nt!KiBugCheckDispatch+0x6e . . . 21 fffffa60`01718840 fffffa60`0340b69e vmswitch+0x5fba 22 fffffa60`017188f0 fffffa60`0340d5cc vmswitch+0x769e 23 fffffa60`01718ae0 fffffa60`0340e615 vmswitch+0x95cc 24 fffffa60`01718d10 fffffa60`009ae31a vmswitch+0xa615 . . . 44 fffffa60`0171aed0 fffffa60`0340b69e vmswitch+0x1d286 45 fffffa60`0171af60 fffffa60`0340d4af vmswitch+0x769e 46 fffffa60`0171b150 fffffa60`034255a0 vmswitch+0x94af 47 fffffa60`0171b380 fffffa60`009ac33c vmswitch+0x215a0
上一節中,通過研究調試器棧回溯輸出的每一個細節解釋了X64的棧是如何工作的。下節中,這些理論會用來獲取X64函數基於寄存器傳遞的參數,不幸的是,X64下沒有任何靈丹妙葯來查找參數,所有的技巧都嚴重依賴於編譯器產生的匯編代碼,如果參數在不可達的內存中,就沒有任何方式可以獲取到它,就算你有棧中函數和模塊的私有符號也無法幫助太多,私有符號告知函數參數的類型和數量,但是並不會告訴你這些參數的值是什么。
技術概要
本節假設X64 函數均不采用/homeparams選項來編譯。因為如果采用/homeparams來編譯,獲取基於寄存器傳遞的參數以及不那么重要了,因為這些參數最終會被歸位至棧空間。無論是否開啟/homeparams,第五或者更多的參數會直接采用棧來傳遞,所以獲取基於棧傳遞的參數在任何情況下都不應該存在問題。
現場調試過程中,在函數的開始處設置斷點是獲取函數參數的最簡單的方法,因為在函數的前置指令執行期間,RCX、RDX、R8、R9分別存儲的前四個參數都是有效的。然而代碼執行到函數體內的時候,參數寄存器的值被改變,導致初始值被復寫了。所以,在函數執行期間去查找基於寄存器傳遞的參數值的時候,必須搞清楚-參數值是從何處讀取的,被寫到什么地方去了。這些問題的答案可以通過執行調試器的指令序列得到,總結如下:
- 確認參數是否是從內存加載進入寄存器的,如果是,通過這些內存地址可以發現參數值。
- 確認參數是否從非易失性寄存器讀取的,並且這些寄存器的值被被調用的函數保存。如果是這樣,被保存的非易失性寄存器的值可以用來決定參數值。
- 確認參數是否從寄存器保存至內存,如果是這樣,參數可以從內存當中進行獲取。
- 如果參數被保存入非易失性寄存器,這些寄存器的值被函數保存了。如果是這樣的話,參數的值可以通過被保存的非易失性寄存器的值來決定。
在接下來的幾節中,會通過示例來展示每一種技巧的用法。每一種技術都需要反匯編參數傳遞經過的調用和被調用函數。圖14中,為了找到函數F2的參數,Frame2需要被反匯編來查找參數的來源,而Frame0需要被反匯編來定位參數的去處。
Figure 14 : Finding Register Based Parameters
確定參數的來源
這種技術用來定位寫入參數寄存器的值的來源,包括不限於常量值,全局數據結構、棧的地址、棧當中存儲的值。
如圖15所示,反匯編調用函數(X64Caller)將寄存器當中的值放入RCX,RDX,R8和R9 作為X64Callee的參數,只要寄存器當中的值沒變,就可以通過這些寄存器來確定參數的來源。
Figure 15 : Identifying parameter sources
下面的示例通過這種技術來查找NtCreateFile()的第三個參數。
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d
從NtCreateFile()的函數原型發現,該函數的第三個參數是POBJECT_ATTRIBUTES。
NTSTATUS NtCreateFile( __out PHANDLE FileHandle, __in ACCESS_MASK DesiredAccess, __in POBJECT_ATTRIBUTES ObjectAttributes, __out PIO_STATUS_BLOCK IoStatusBlock, );
對Frame#0的返回地址進行反匯編發現,函數的第三個參數R8寄存器中的值是Rsp+0xc8.前面“kn”命令顯示了Kernelbase!CreateFileW執行中Rsp寄存器的值是00000000`0029bc00。
0:000> ub 000007fe`fdd24d76 KERNELBASE!CreateFileW+0x29d: 000007fe`fdd24d46 and ebx,7FA7h 000007fe`fdd24d4c lea r9,[rsp+88h] 000007fe`fdd24d54 lea r8,[rsp+0C8h] 000007fe`fdd24d5c lea rcx,[rsp+78h] 000007fe`fdd24d61 mov edx,ebp 000007fe`fdd24d63 mov dword ptr [rsp+28h],ebx 000007fe`fdd24d67 mov qword ptr [rsp+20h],0 000007fe`fdd24d70 call qword ptr [KERNELBASE!_imp_NtCreateFile]
通過上面找到的被R8寄存器加載的值,可以重建OBJECT_ATTRIBUTE,如果下面的輸出所示。
0:000> dt ntdll!_OBJECT_ATTRIBUTES 00000000`0029bc00+c8 +0x000 Length : 0x30 +0x008 RootDirectory : (null) +0x010 ObjectName : 0x00000000`0029bcb0 _UNICODE_STRING "\??\C:\Windows\Fonts\staticcache.dat" +0x018 Attributes : 0x40 +0x020 SecurityDescriptor : (null) +0x028 SecurityQualityOfService : 0x00000000`0029bc68
非易失寄存器做為參數來源
如果參數寄存器的值來源於非易失性寄存器,而非易失性寄存器的值被保存在了棧上,那么可以采用下面的技術來定位參數值。
圖16顯示了調用函數(X64caller)和被調用者(X64Callee)的反匯編。X64Caller調用X64Callee前面的指令表明參數寄存器(RCX,RDX,R8,R9)的值來源於非易失性寄存器(RDI,R12,RBX,RBP). 右邊圖中X64Callee的前置指令將非易失性寄存器的值保存至棧上,這些值可以被找到,其實就是原先被寫入參數寄存器的值。
Figure 16 : Non-Volatile Registers as parameter sources
下面的例子采用上面的技術來查找CreateFileW()的第一個參數值。
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d .
CreateFile的第一個參數類型是LPCTSTR.
HANDLE WINAPI CreateFile( __in LPCTSTR lpFileName, __in DWORD dwDesiredAccess, __in DWORD dwShareMode, __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes, . . . );
反匯編Frame#1當中的函數返回值得到如下指令。參數1對應的寄存器RCX的值來自非易失性寄存器RDI,下一步就是需要確認CreateFileW()是否保存了EDI的值。
0:000> ub 00000000`77ac2aad L B kernel32!CreateFileWImplementation+0x4a: 00000000`77ac2a7a mov rax,qword ptr [rsp+90h] 00000000`77ac2a82 mov r9,rsi 00000000`77ac2a85 mov r8d,ebp 00000000`77ac2a88 mov qword ptr [rsp+30h],rax 00000000`77ac2a8d mov eax,dword ptr [rsp+88h] 00000000`77ac2a94 mov edx,ebx 00000000`77ac2a96 mov dword ptr [rsp+28h],eax 00000000`77ac2a9a mov eax,dword ptr [rsp+80h] 00000000`77ac2aa1 mov rcx,rdi 00000000`77ac2aa4 mov dword ptr [rsp+20h],eax 00000000`77ac2aa8 call kernel32!CreateFileW (00000000`77ad2c88)
反匯編CreateFileW可以查看該函數的前置指令。RDI寄存器的值同步“push rdi”保存至棧中,該值和參數寄存器ECX當中的值應該是保持一致的,下一步就是要從棧當中找到RDI的值。
0:000> u KERNELBASE!CreateFileW KERNELBASE!CreateFileW: 000007fe`fdd24ac0 mov dword ptr [rsp+18h],r8d 000007fe`fdd24ac5 mov dword ptr [rsp+10h],edx 000007fe`fdd24ac9 push rbx 000007fe`fdd24aca push rbp 000007fe`fdd24acb push rsi 000007fe`fdd24acc push rdi 000007fe`fdd24acd sub rsp,138h 000007fe`fdd24ad4 mov edi,dword ptr [rsp+180h]
“.frame /r”命令可以在特定函數執行過程中顯示非易失性寄存器的值,是通過獲取被調用函數前置指令保存在棧中的值來實現的。可以看到當CreateFileWImplementation()調用CreateFileW()時,EDI的值是000000000029beb0。該值可以用來展示傳遞給CreateFile的第一個參數。
0:000> .frame /r 2 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d rax=0000000000000005 rbx=0000000080000000 rcx=000000000029bc78 rdx=0000000080100080 rsi=0000000000000000 rdi=000000000029beb0 rip=0000000077ac2aad rsp=000000000029bd60 rbp=0000000000000005 r8=000000000029bcc8 r9=000000000029bc88 r10=0057005c003a0043 r11=00000000003ab0d8 r12=0000000000000000 r13=ffffffffb6011c12 r14=0000000000000000 r15=0000000000000000 0:000> du /c 100 000000000029beb0 00000000`0029beb0 "C:\Windows\Fonts\staticcache.dat"
確定參數的最終地址
該技術用來確認參數寄存器中的值是否被寫入內存。當函數采用/homeparams來編譯的時候,函數前置指令默認會將參數寄存器的值寫入棧上的參數歸位空間。然而當函數未采用/homeparams來編譯的時候,參數寄存器的內容可能會在函數體內被寫入任何地址。
圖17中函數將寄存器RCX,RDX,R8,R9的值寫入了棧空間。參數值可以通過當前幀的棧指針的值定位的內存空間的值來定位。
Figure 17 : Identifying parameter destinations
下面的例子通過上面的技術來確定DispatchClientMessage的第三和第四個參數。
0:000> kn # Child-SP RetAddr Call Site 26 00000000`0029dc70 00000000`779ca01b user32!UserCallWinProcCheckWow+0x1ad 27 00000000`0029dd30 00000000`779c2b0c user32!DispatchClientMessage+0xc3 28 00000000`0029dd90 00000000`77c1fdf5 user32!_fnINOUTNCCALCSIZE+0x3c 29 00000000`0029ddf0 00000000`779c255a ntdll!KiUserCallbackDispatcherContinue
DispatchClientMessage的第三和第四個參數分別位於寄存器R8和R9當中,反匯編該函數,找到任何將R8或者R9寄存器內容寫入內存的操作,會發現有如下兩個指令"mov qword ptr [rsp+28h], r9"
and "mov qword ptr [rsp+20h], r8",表明寄存器的值被寫入棧中。這些指令不是函數的前置指令,但卻在整個大函數體中。需要關注的是,R8和R9的值在被寫入寄存器之前是否被修改。雖然對於DispatchClientMessage來說這種情況沒有發生,但是采用這種技術的時候需要時刻關注參數寄存器是否被復寫。
0:000> uf user32!DispatchClientMessage user32!DispatchClientMessage: 00000000`779c9fbc sub rsp,58h 00000000`779c9fc0 mov rax,qword ptr gs:[30h] 00000000`779c9fc9 mov r10,qword ptr [rax+840h] 00000000`779c9fd0 mov r11,qword ptr [rax+850h] 00000000`779c9fd7 xor eax,eax 00000000`779c9fd9 mov qword ptr [rsp+40h],rax 00000000`779c9fde cmp edx,113h 00000000`779c9fe4 je user32!DispatchClientMessage+0x2a (00000000`779d7fe3) user32!DispatchClientMessage+0x92: 00000000`779c9fea lea rax,[rcx+28h] 00000000`779c9fee mov dword ptr [rsp+38h],1 00000000`779c9ff6 mov qword ptr [rsp+30h],rax 00000000`779c9ffb mov qword ptr [rsp+28h],r9 00000000`779ca000 mov qword ptr [rsp+20h],r8 00000000`779ca005 mov r9d,edx 00000000`779ca008 mov r8,r10 00000000`779ca00b mov rdx,qword ptr [rsp+80h] 00000000`779ca013 mov rcx,r11 00000000`779ca016 call user32!UserCallWinProcCheckWow (00000000`779cc2a4)
從上面”kn”的輸出,發現Frame#27的棧指針(RSP)的值是00000000`0029dd30, 加上偏移得到R8寄存器的是0000000`00000000,這便是DispatchClientMessage()第三個參數的值。
0:000> dp 00000000`0029dd30+20 L1 00000000`0029dd50 00000000`00000000
同理可以得到R9的值為00000000`0029de70,該值為DispatchClientMessage()的第四個參數。
0:000> dp 00000000`0029dd30+28 L1 00000000`0029dd58 00000000`0029de70
非易失性寄存器作為參數最終目的地
該技術討論的是參數寄存器的內容被函數被保存至非易失性寄存器中,隨后該寄存器的值被函數保存至棧中的情況。
圖18顯示的是調用者(X64Caller)和被調用者(X64Callee)。基於寄存器的參數的值被傳遞給函數X64Caller.X64Caller包含將參數寄存器(RCX,RDX,R8,R9)的值保存至非易失性寄存器(RDI,RSI,RBX,RBP).X64Callee的前置指令將這些非易失性寄存器的值保存至棧中,這樣的話可以方便的獲取他們的值,再由這些值可以輕松的得到參數寄存器的值。
Figure 18 : Non-Volatile Registers as Parameter Destinations
下面的例子采用這個技術來查找CreateFileWImplementation()的四個基於寄存器傳遞的參數。
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 03 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd
通過CreateFileWImplementation()的反匯編代碼可以發現,在函數前置指令執行之后,"mov ebx,edx", "mov rdi,rcx", mov rsi,r9" 和 "mov ebp,r8d"將參數寄存器的值保存入非易失性寄存器。必須檢查到下一個函數調用之前的指令,確保非易失性寄存器的值沒有被復寫。雖然這里沒有顯示表明,這條規則已經通過檢查從CreateFileWImplementation()到CreateFileW()的所有代碼來保證。下一步就是通過反匯編CreateFileW()的前置指令來確定這些非易失性寄存器的值是否被保存至棧中。
0:000> uf kernel32!CreateFileWImplementation kernel32!CreateFileWImplementation: 00000000`77ac2a30 mov qword ptr [rsp+8],rbx 00000000`77ac2a35 mov qword ptr [rsp+10h],rbp 00000000`77ac2a3a mov qword ptr [rsp+18h],rsi 00000000`77ac2a3f push rdi 00000000`77ac2a40 sub rsp,50h 00000000`77ac2a44 mov ebx,edx 00000000`77ac2a46 mov rdi,rcx 00000000`77ac2a49 mov rdx,rcx 00000000`77ac2a4c lea rcx,[rsp+40h] 00000000`77ac2a51 mov rsi,r9 00000000`77ac2a54 mov ebp,r8d 00000000`77ac2a57 call qword ptr [kernel32!_imp_RtlInitUnicodeStringEx (00000000`77b4cb90)] 00000000`77ac2a5d test eax,eax 00000000`77ac2a5f js kernel32!zzz_AsmCodeRange_End+0x54ec (00000000`77ae7bc0)
下面的輸出顯示了CreateFileW()將非易失性寄存器(rbx, rbp, rsi and edi)的值寫入堆棧,使得可以通過“.frame /r”命令去顯示他們的值。
0:000> u KERNELBASE!CreateFileW KERNELBASE!CreateFileW: 000007fe`fdd24ac0 mov dword ptr [rsp+18h],r8d 000007fe`fdd24ac5 mov dword ptr [rsp+10h],edx 000007fe`fdd24ac9 push rbx 000007fe`fdd24aca push rbp 000007fe`fdd24acb push rsi 000007fe`fdd24acc push rdi 000007fe`fdd24acd sub rsp,138h 000007fe`fdd24ad4 mov edi,dword ptr [rsp+180h]
在包含CreateFileWImplementation() 的frame#2上運行“.frame /r”可以顯示該幀被激活時這些寄存器的值。
0:000> .frame /r 02 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d rax=0000000000000005 rbx=0000000080000000 rcx=000000000029bc78 rdx=0000000080100080 rsi=0000000000000000 rdi=000000000029beb0 rip=0000000077ac2aad rsp=000000000029bd60 rbp=0000000000000005 r8=000000000029bcc8 r9=000000000029bc88 r10=0057005c003a0043 r11=00000000003ab0d8 r12=0000000000000000 r13=ffffffffb6011c12 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei pl zr na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000244 kernel32!CreateFileWImplementation+0x7d: 00000000`77ac2aad mov rbx,qword ptr [rsp+60h] ss:00000000`0029bdc0={usp10!UspFreeForUniStore (000007fe`fe55d8a0)}
通過mov 指令來映射非易失性寄存器的值和參數寄存器的值可以得到下面的結果:
- P1 = RCX = RDI = 000000000029beb0
- P2 = EDX = EBX = 0000000080000000
- P3 = R8D = EBP = 0000000000000005
- P4 = R9 = RSI = 0000000000000000
通過上面的四種技術來獲取X64調用棧當中的參數是非常耗時和繁瑣的。CodeMachine提供了一個擴展命令(這里注意,這個擴展命令我嘗試使用過,貌似返回值還是錯誤的,有興趣的同學可以嘗試)可以自動完成這些操作,該命令嘗試獲取並顯示線程棧當中所有函數的參數。在Usermode調試的時候,如果想獲取指定線程的參數,可以通過"~s"命令來切換線程,這與在Kernel mode調試的時候采用".thread"來切換線程是類似的。
本文涵蓋了X64編譯器的優化功能,這些優化使得其與X86存在着極大差異。介紹了X64的異常處理機制,並詳細解釋了可執行文件格式和數據結構是如何來支持該特性的。討論了X64是如何在運行時建立棧幀的,並利用該理論去獲取函數基於寄存器傳遞的參數值,並最終克服了X64體系參數尋值的這一頑疾。
參考文獻:
1、 http://www.codemachine.com/article_x64deepdive.html
2、 http://blog.csdn.net/xbgprogrammer/article/details/38752651
3、 http://blog.csdn.net/xbgprogrammer/article/details/45220885
4、 http://blog.csdn.net/woxiaohahaa/article/details/50564517
5、 框架指針省略FPO
https://www.cnblogs.com/awpatp/archive/2009/11/04/1595988.html