本篇原文為 X64 Deep Dive,如果有良好的英文基礎的能力,可以點擊該鏈接進行閱讀。本文為我個人:寂靜的羽夏(wingsummer) 中文翻譯,非機翻,著作權歸原作者所有。
由於原文十分冗長,也十分干貨,采用機翻輔助,人工閱讀比對修改的方式進行,如有翻譯不得當的地方,歡迎批評指正。翻譯不易,如有閑錢,歡迎支持。注意在轉載文章時注意保留原文的作者鏈接,我(譯者)的相關信息。話不多說,正文開始:
關於 X64 平台執行和調試關鍵方面的深度教程,例如編譯器優化、異常處理、參數傳遞、堆棧布局以及參數的獲取。
深入 x64
本教程討論在X64 CPU
上執行代碼的一些重要內容,例如編譯器優化、異常處理、參數傳遞和參數的獲取,並解釋它們的密切聯系。本篇文章涵蓋了重要調試器命令,並介紹理解這些命令的輸出結果的必要前置知識,強調X64 CPU
與X86 CPU
的不同之處以及它如何影響X64
上的調試。篇末我們還會將所有內容串在一起,說明如何利用這些知識從X64
調用堆棧中獲取基於寄存器的參數,克服在調試X64
代碼時無法繞過的困難。本教程將逐步介紹上面所述內容,並利用圖表、反匯編和調試器輸出結果來深入了解關鍵點。希望讀者能夠很好地理解X86 CPU
上的工作原理,包括寄存器使用、堆棧使用和函數布局,以完成本教程的大部分內容。
編譯器優化
本節討論影響X64
代碼生成方式的編譯器一些優化。從X64
寄存器的說明開始,進而介紹編譯器優化方面的內容,如函數內聯、尾函數調用平棧、幀指針優化和基於堆棧指針的局部變量訪問。
寄存器的變化
X64 CPU
上的所有寄存器,除了段寄存器和EFlags
寄存器,都是64位的,這意味着從內存中提取的所有內容都是64位大小的。此外,X64
指令能夠一次處理64位,使得x64
能夠作為本機64位的處理器。此外,它還增添了八個新寄存器,即R8 - R15
。它們用數字標記,而不是用字母標記的其他寄存器。以下調試器輸出顯示了X64
上的寄存器:
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
來說,其中一些寄存器的用法也發生了變化。它們的變化可以用以下幾點來分組總結:
- 非易失性寄存器在跨函數調用時必須保存。 X64 有一個擴展的非易失性寄存器集,其中還包括所有舊的 X86 非易失性寄存器。這組中的新寄存器是 R12 到 R15。從獲取基於寄存器的函數參數的角度來看,這十分重要。
- 快速調用寄存器用於將參數傳遞給函數。 Fastcall 是 X64 上的默認調用約定,其中前 4 個參數通過寄存器 RCX、RDX、R8、R9 傳遞。
- RBP 不再用作幀指針。它現在是一個通用寄存器,就像任何其他寄存器(如 RBX、RCX 等)一樣。調試器不能再使用 RBP 寄存器來遍歷調用堆棧。
- 在 X86 CPU 上,FS 段寄存器指向線程環境塊 (TEB) 和處理器控制區域 (KPCR),但在 X64 上,GS 寄存器在用戶模式下指向 TEB,在內核中指向 KPCR 的新模式。然而,當運行 WOW64 應用程序(即 X64 系統上的 32 位應用程序)時,FS 寄存器繼續指向 32 位版本的 TEB。
X64
上的陷阱幀結構體nt!_KTRAP_FRAME
不包含非易失性寄存器的有效內容。如果打算修改非易失性寄存器,X64
函數的prolog
會保存非易失性寄存器的值。調試器始終可以從堆棧中提取這些非易失性寄存器的保存值,而不必從陷阱幀中獲取它們。 在X64
上的內核模式調試期間,.trap
命令的輸出會打印一條注釋,突出顯示從陷阱中檢索到的所有寄存器的值可能不准確的事實,如下所示。此規則有例外,例如,為用戶到內核模式轉換生成的陷阱幀確實包含所有寄存器的正確值。
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
的專屬,但它對內聯這東西十分關注。內聯的優點是它避免了提升堆棧、分配給被調用者使用,最后返回給調用者的開銷。內聯的缺點是由於代碼重復,可執行文件膨脹。並且,函數代碼的重復內聯擴展導致增大代碼緩存的缺失的數量和提高缺頁異常的頻數。函數內聯給調試帶來了不便,因為當嘗試在編譯器選擇內聯的函數上設置斷點時,調試器無法找到內聯函數的符號。源文件級別的內聯由編譯器的/Ob
標志控制,並且可以通過__declspec(noinline)
在每個函數上禁用內聯。在Function1
中內聯function2
和Function3
這兩個函數的示意圖如下:
尾函數調用平棧
X64
編譯器可以通過將函數替換為跳轉到被調用者來優化函數的最后一次調用。這避免了為被調用者配置堆棧幀的開銷。調用者和被調用者共享同一個棧幀,被調用者直接返回到調用者的調用函數。當調用者和被調用者具有相同的參數時,這尤其有用,因為如果相關參數已經在所需的寄存器中並且這些寄存器沒有更改,則不必重新加載它們。下圖展示了了在調用Function4
時Function1
中的尾函數調用平棧。Function1
跳轉到Function4
,當Function4
執行完畢后,直接返回給Function1
的調用者。
幀指針優化
與X86 CPU
使用EBP
寄存器訪問堆棧上的參數和局部變量不同,X64
函數不會使用RBP
寄存器來取參和局部變量,即不使用EBP
寄存器作為幀指針。相反,它使用RSP
寄存器作為堆棧指針和幀指針,在下一個話題中將詳細介紹它是如何工作的。因此,在X64
上,RBP
寄存器從堆棧的維護工作中解脫出來,可以作為通用寄存器使用。但有一個例外是使用alloca
函數,它的作用是在堆棧上動態分配空間。此類函數將使用RBP
寄存器作為幀指針,就像在X86
上使用EBP
一樣。
以下匯編代碼片段展示了X86
函數KERNELBASE!Sleep
。 對EBP
寄存器的引用表明它被用作幀指針。 在調用函數SleepEx
時,參數被壓入堆棧並通過call
指令調用SleepEx
。
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
寄存器的保存、恢復和配置,即幀指針的使用被省略,也沒有任何堆棧幀為被調用者負責,即SleepEx
。事實上,Sleep
和SleepEx
最終使用相同的堆棧幀,這是尾調用優化的一個示例。
0:000> uf KERNELBASE!Sleep
KERNELBASE!Sleep:
000007fe`fdd21140 xor edx,edx
000007fe`fdd21142 jmp KERNELBASE!SleepEx (000007fe`fdd21150)
基於堆棧指針的局部變量訪問
在X86 CPU
上,幀指針EBP
寄存器最重要的功能是提供對基於堆棧的參數和局部變量的訪問。如前所述,在X64 CPU
上,RBP
寄存器並不指向當前函數的堆棧幀。所以在X64
上,RSP
寄存器必須同時用作堆棧指針和幀指針。所以X64
上的所有堆棧引用都是基於RSP
執行的。因此,X64
上的函數依賴於整個函數體中的靜態RSP
寄存器,作為訪問局部變量和參數的參考。由於push
和pop
指令會改變堆棧指針,因此X64
函數將push
和pop
指令分別限制為函數prolog
和epilog
。堆棧指針在prolog
和epilog
之間一定保持不變這一事實是X64
函數的一個特征,如下圖所示:
以下代碼片段展示了函數user32!DrawTestExW
的完整內容。該函數的prolog
以指令sub rsp, 48h
結束,epilog
以指令add rsp, 48h
開始。由於prolog
和epilog
之間的指令使用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
異常處理
本節討論X64
函數用於異常處理的底層機制和數據結構,以及調試器如何利用這些結構來遍歷調用堆棧,並指出了X64
調用堆棧的一些特色。
RUNTIME_FUNCTION
X64
可執行文件使用一種文件格式,它是用於X86
的PE
文件格式的變體,稱為PE32+
。此類文件有一個稱為.pdata
或異常目錄的額外部分,其中包含用於處理異常的信息。該異常目錄包含可執行文件中每個非葉函數的RUNTIME_FUNCTION
結構。非葉函數是調用其他函數的函數,每個RUNTIME_FUNCTION
結構包含函數中第一條和最后一條指令的偏移量(即函數范圍)和一個指向展開信息結構的指針,該結構描述了在發生異常時如何展開函數的調用堆棧。下圖展示了一個模塊的RUNTIME_FUNCTION
結構,該結構包含該模塊中函數開頭和結尾的偏移量。
以下匯編代碼片段展示了與X86
和X64
上的異常處理相關的代碼生成方面的一些差異。在x86
上,當高級語言C/C++
代碼包含結構化異常處理結構,如__try
/__except
時,編譯器會在運行時在堆棧上構建異常幀的函數的prolog
和epilog
中生成特殊代碼。這可以在下面調用ntdll!_SEH_prolog4
和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
版本中,沒有跡象表明該函數使用結構化異常處理,因為在運行時沒有構建基於堆棧的異常幀。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)
UNWIND_INFO 和 UNWIND_CODE
RUNTIME_FUNCTION
結構的BeginAddress
和EndAddress
字段分別存儲函數代碼在虛擬內存中從模塊開始的偏移量。當函數產生異常時,操作系統會掃描PE
文件的內存映射副本,尋找包含當前指令地址的RUNTIME_FUNCTION
結構體。RUNTIME_FUNCTION
結構的UnwindData
字段包含另一個結構的偏移量,它告訴操作系統運行時它應該如何展開堆棧,這是UNWIND_INFO
結構。UNWIND_INFO
結構包含數量不定的UNWIND_CODE
結構,每個結構都存儲着回滾恢復函數prolog
執行的單個堆棧相關操作影響的信息。
對於動態生成的代碼,操作系統支持函數RtlAddFunctionTable
和RtlInstallFunctionTableCallback
用於在運行時創建RUNTIME_FUNCTION
信息。
下圖展示了RUNTIME_FUNCTION
和UNWIND_INFO
結構之間的關系以及函數在內存中的位置:
調試器的.fnent
命令顯示有關給定函數的RUNTIME_FUNCTION
結構的信息。以下示例顯示函數ntdll!RtlUserThreadStart
的.fnent
命令的輸出。
0:000> .fnent ntdll!RtlUserThreadStart
Debugger function entry 00000000`03be6580 for:
(00000000`77c03260) ntdll!RtlUserThreadStart | (00000000`77c03290) ntdll!RtlRunOnceExecuteOnce
Exact matches:
ntdll!RtlUserThreadStart = <no type information>
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
添加到模塊的基部,即包含函數RtlUserThreadStart
的ntdll.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
結構的主要目的是描述堆棧在異常期間是如何展開的,但調試器使用此信息來遍歷調用堆棧,而無需訪問模塊的符號。每個UNWIND_CODE
結構都可以描述函數prolog
執行的以下操作之一:
- SAVE_NONVOL - 在堆棧上保存一個非易失性寄存器。
- PUSH_NONVOL - 將非易失性寄存器壓入堆棧。
- ALLOC_SMALL - 在堆棧上分配空間(最多 128 個字節)。
- ALLOC_LARGE - 在堆棧上分配空間(最多 4GB)。
因此,本質上UNWIND_CODE
是函數prolog
的元數據表示。
下圖展示函數prolog
執行的與堆棧相關的操作之間的關系以及這些操作在UNWIND_CODE
結構中的描述。UNWIND_CODE
結構以它們所代表的指令的相反順序出現,因此在異常期間,堆棧可以在它創建的相反方向上展開。
以下示例顯示X64
系統上notepad.exe
本機版本的PE
文件中的.pdata
節頭部。VirtualAddress
字段表明.pdata
段位於可執行文件開頭的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
結構都描述了函數的prolog
執行的類似PUSH_NONVOL
或ALLOC_SMALL
的操作,並且在堆棧展開時必須回滾,如下所示。調試器的.fnent
命令也顯示了這兩個結構的內容。但是,link -dump -unwindinfo
的輸出解碼了.fnent
沒有的UNWIND_CODE
結構的全部內容。
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
代表函數prolog
中的sub
指令,它分配0x28
字節的堆棧空間。每個PUSH_NONVOL
對應於函數序言中的push
指令,該指令將非易失性寄存器保存在堆棧上,並由函數Epilog
中的pop
指令恢復。這些指令可以在偏移量0x1234
處的函數反匯編中看到,如下所示:
0:000> ln notepad+1234
(00000000`ff971234) notepad!StringCchPrintfW | (00000000`ff971364) notepad!CheckSave
Exact matches:
notepad!StringCchPrintfW = <no type information>
notepad!StringCchPrintfW = <no type information>
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
操作系統二進制文件經過稱為基本塊工具 (Basic Block Tools
,BBT
) 的配置文件引導優化,這增加了代碼的空間局部性。經常執行的功能部分被保存在一起,可能在同一頁面中,不經常使用的部分被移動到其他位置。這減少了需要為最常執行的代碼路徑保留在內存中的頁面數量,最終導致整體工作集減少。為了應用這種優化,二進制文件被鏈接、執行、分析,然后分析數據用於根據執行頻率重新排列函數的各個部分。
在最終的函數中,函數的一些代碼塊被移到函數主體之外,該主體最初由RUNTIME_FUNCTION
結構的范圍定義。由於代碼塊的移動,函數體被分解為多個不連續的部分,因此最初由鏈接器生成的RUNTIME_FUNCTION
結構不再能夠准確識別這些函數的范圍。為了解決這個問題,BBT
進程添加了多個新的RUNTIME_FUNCTION
結構,每個結構定義了一個具有優化功能的連續代碼塊。這些RUNTIME_FUNCTION
結構與終止於原始RUNTIME_FUNCTION
結構的鏈連接在一起,該結構的BeginAddress
始終指向函數的開頭。
下圖展示由三個基本塊組成的函數。應用BBT
進程塊#2
后移出函數體,導致原始RUNTIME_FUNCTION
中的信息變為無效。因此,BBT
進程創建了第二個RUNTIME_FUNCTION
結構並將其鏈接到第一個結構,從而描述了整個函數。
當前公共版本的調試器不會遍歷完整的 RUNTIME_FUNCTION 結構鏈。 因此調試器無法顯示優化函數的正確名稱,其中返回地址映射到已移出主函數體的代碼塊。
以下示例顯示了調用堆棧中名稱顯示不正確的函數。 相反,名稱以ntdll! ?? ::FNODOBFM::'string'
的形式顯示。調試器錯誤地將幀0x0c
中的返回地址0x0000000077c17623
轉換為名稱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_CODE
。顯示的信息包含一個標題為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
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
后面顯示的BeginAddress
指向原函數的開頭。下面ln
命令的輸出顯示,打亂的函數名實際上是ntdll!LdrpInitialize
。
0:000> ln ntdll+000330f0
(00000000`77c030f0) ntdll!LdrpInitialize | (00000000`77c031c0) ntdll!LdrpAllocateTls
Exact matches:
ntdll!LdrpInitialize = <no type information>
調試器的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
命令輸出的Characteristics
字段中的perf
一詞來識別,如下所示。
0: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
上,前4
個參數始終通過寄存器傳遞,其余參數通過堆棧傳遞。這是調試期間比較頭痛的主要原因之一,因為寄存器值往往會隨着函數的執行而改變,並且很難確定傳遞給函數的原始參數值,在其執行過程中,除了獲取參數的這一問題之外,x64
調試與x86
調試沒有什么不同。
下圖展示了X64
匯編代碼,描述了調用者如何將參數傳遞給被調用者。
以下調用堆棧顯示了調用KERNELBASE!CreateFileW
的函數kernel32!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
.
.
.
從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!CreateFileWImplementation
中的指令,就在調用kernel32!CreateFileW
之前。指令mov rcx,rdi
、mov edx,ebx
、mov r8d,ebp
、mov r9,rsi
顯示前4
個參數被移動到寄存器中,為調用kernel32!CreateFileW
做准備。類似地,指令mov dword ptr [rsp+20h],eax
、mov dword ptr [rsp+28h],eax
和mov qword ptr [rsp+30h],rax
顯示其余參數,即5
到7
被放到棧中。
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)
預留空間
雖然前四個參數是通過寄存器傳遞的,但堆棧上仍然為這四個參數分配了空間。這稱為參數預留空間 (homing space),如果函數通過地址而不是值訪問參數,或者如果使用/homeparams
標志編譯函數,則用於存儲參數值。這個預留空間的最小大小是0x20
字節或四個64
位插槽,即使該函數采用少於4
個參數也是如此。當預留空間不用於存儲參數值時,編譯器使用它來保存非易失性寄存器。
下圖展示了堆棧上基於寄存器的參數的歸位空間,以及函數prolog
如何在此參數預留空間中存儲非易失性寄存器。
在下面的示例中,sub rsp, 20h
指令顯示了在堆棧上分配0x20
字節的函數的prolog
,這對於四個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
的prolog
的匯編代碼片段展示了四個非易失性寄存器保存在指定為參數預留區域的堆棧上的位置。
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
調用約定,調用者將始終使用寄存器將前4
個參數傳遞給被調用者。當使用編譯器的/homeparams
標志啟用參數歸位時,只有被調用者的代碼受到影響。在使用Windows
驅動程序工具包 (WDK) 構建環境構建的二進制文件的檢查調試版本中始終啟用此標志。被調用者的prolog
從寄存器中讀取參數值並將這些值存儲在堆棧中的參數預留區域中。
下圖顯示了調用者的匯編代碼,其中將參數值移動到相應的寄存器中。它還顯示已使用/homeparams
標志編譯的被調用者的prolog
,這會導致它將參數值按照指定預留位置到堆棧中。被調用者的prolog
從寄存器中讀取參數值並將這些值存儲在參數預留區域的堆棧中。
下面的代碼片段展示了寄存器值被移動到由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
000007fe`fe667e49 test eax,eax
000007fe`fe667e4b je msvcrt!printf+0x25 (000007fe`fe67d74b)
.
.
.
堆棧使用
X64
函數的堆棧幀包含以下項目:
- 調用者的返回地址。
- 由函數 prolog 壓入堆棧的非易失性寄存器。
- 函數使用的局部變量。
- 傳遞給被調用者的基於堆棧的參數。
- 傳遞給被調用者的基於寄存器的參數的預留空間。
除了返回地址之外,堆棧上的所有項目都由函數的prolog
放置在那里。局部變量占用的堆棧空間、被調用者的基於堆棧的參數以及參數的預留空間都在單個sub rsp, xxx
指令中分配。為基於堆棧的參數保留的空間適合具有最多參數的被調用者。基於寄存器的參數預留空間僅存在於非葉函數中。即使沒有一個被調用者接受這么多參數,它也包含四個參數的空間。
下圖顯示了X64 CPU
上函數堆棧幀的布局。在函數prolog
完成執行后,RSP
寄存器指向圖中所示的位置。
調試器的knf
命令顯示調用堆棧以及堆棧中每一幀使用的堆棧空間量,此堆棧空間利用率列在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
下面的匯編代碼片段顯示了函數CreateFileW
的prolog
,它將非易失性寄存器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)
Child-SP
調試器的k
命令顯示的Child-SP
寄存器的值表示堆棧指針 (RSP) 指向的地址,作為該幀中顯示的函數的點,已完成其prolog
的執行。將被壓入堆棧的下一項將是函數調用其被調用者時的返回地址。由於X64
函數不會在函數序言之后修改RSP
的值,因此函數其余部分執行的任何堆棧訪問都是相對於堆棧指針的此位置完成的。這包括訪問基於堆棧的參數和局部變量。
下圖顯示了函數f2
的堆棧幀及其與堆棧k
命令輸出中顯示的RSP
寄存器的關系。返回地址RA1
指向函數f2
中call f1
指令之后的指令。此返回地址出現在RSP2
指向的位置旁邊的調用堆棧上。
在下面的調用堆棧中,幀#01
的Child-SP
的值為00000000'0029bc00
。這是在CreateFileW
的prolog
剛剛完成時執行點的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
之前的堆棧內容是返回地址000007fe'fdd24d76
,它對應於KERNELBASE!CreateFileW+0x2cd
,並通過調用ntdll!NtCreateFile
被推送到那里。
0:000> dps 00000000`0029bc00-8 L1
00000000`0029bbf8 000007fe`fdd24d76 KERNELBASE!CreateFileW+0x2cd
跟蹤調用堆棧
在X86 CPU
上,調試器遵循幀指針 (EBP) 鏈將調用堆棧從最近的函數幀遍歷到最近的函數幀。調試器通常可以做到這一點,而無需訪問其函數出現在堆棧上的模塊的符號。但是,在某些情況下,可能會破壞此幀指針鏈,例如當函數省略其幀指針 (frame pointer omitted
,FPO) 時。在這些情況下,調試器需要模塊的符號才能准確地遍歷調用堆棧。
另一方面,X64
函數不使用RBP
寄存器作為幀指針,因此調試器沒有可遵循的幀指針鏈。相反,調試器使用堆棧指針和堆棧幀的大小來遍歷堆棧。調試器定位RUNTIME_FUNCTION
、UNWIND_INFO
和UNWIND_CODE
結構來計算調用堆棧中每個函數的堆棧空間利用率,並將這些值添加到Child-SP
以計算后續Child-SP
的值。
下圖顯示了函數堆棧框架的布局。堆棧幀的總大小(或堆棧空間利用率)可以通過將返回地址的大小(8 字節)和非易失性寄存器、局部變量、基於堆棧的占用的堆棧空間量相加來計算被調用者的參數和為四個基於寄存器的參數(0x20字節)分配的歸位空間。UNWIND_CODE
結構表示被壓入堆棧的非易失性寄存器的數量以及為局部變量和參數分配的空間量。
在下面的堆棧跟蹤中,第1
幀(即CreateFileW
)中函數消耗的堆棧空間量為0x160
字節。下一節展示了如何計算這個數字以及調試器如何使用它來計算第2
幀的Child-SP
的值。請注意,第1
幀中列出的函數占用的堆棧空間顯示在第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
結構描述的操作。總共有4
個非易失性寄存器被壓入堆棧,並為局部變量和參數分配0x138
字節。被移動數據的非易失性寄存器 (UWOP_SAVE_NONVOL),與壓入堆棧的 (UWOP_PUSH_NONVOL) 不同,不會消耗堆棧空間。
0:000> .fnent kernelbase!CreateFileW
Debugger function entry 00000000`03be6580 for:
(000007fe`fdd24ac0) KERNELBASE!CreateFileW | (000007fe`fdd24e2c) KERNELBASE!SbSelectProcedure
Exact matches:
KERNELBASE!CreateFileW = <no type information>
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
將上面列出的大小相加會產生0x138 + (8*4) = 0x158
字節的堆棧空間消耗。
0:000> ?138+(8*4)
Evaluate expression: 344 = 00000000`00000158
將返回地址的大小(8 字節)與上述數字相加,總堆棧幀大小為0x160
字節。這與前面顯示的調試器的knf
命令顯示的數字相同。
0:000> ?158+8
Evaluate expression: 352 = 00000000`00000160
參考knf
命令的輸出,調試器將幀大小(0x160)添加到幀#01
中的Child-SP
值,即00000000'0029bc00
,以獲得幀#02
中的Child-SP
值,即00000000'0029bd60
。
0:000> ?00000000`0029bc00+160
Evaluate expression: 2735456 = 00000000`0029bd60
因此,可以使用RUNTIME_FUNCTION
、UNWIND_INFO
和UNWIND_CODE
結構從PE
文件本身中的信息計算為每個幀在堆棧上分配的空間。因此,調試器可以遍歷調用堆棧,而不需要堆棧上存在的模塊的符號(公共或私有)。以下調用堆棧顯示模塊vmswitch
,其符號在Microsoft
的公共符號服務器上不可用,但不會阻止調試器准確顯示調用堆棧,這是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
編譯,第五個和更高編號的參數始終通過堆棧傳遞,因此在任何情況下獲取這些參數都不應該成為問題。
在實時調試期間,在函數開頭設置斷點是檢索調用者傳入的參數的最簡單方法,因為在函數的序言期間,前4
個參數保證在寄存器RCX
、RDX
、分別為R8
和R9
。
但是,隨着函數體中的執行,參數寄存器的內容會發生變化,並且初始參數值會被覆蓋。因此,要在函數執行期間的任何時候確定這些基於寄存器的參數的值,需要明確從哪里讀取參數的值以及寫入的參數值在哪里。可以通過在調試器中執行一系列步驟來找到這些問題的答案,這些步驟可以分為如下:
- 確定參數是否從內存加載到寄存器中。如果是這樣,可以檢查內存位置以確定參數值。
- 確定參數是否從非易失性寄存器加載,以及這些寄存器是否由被調用者保存。如果是這樣,可以檢查保存的非易失性寄存器值以確定參數值。
- 確定參數是否從寄存器保存到內存中。如果是這樣,可以檢查內存位置以確定參數值。
- 確定參數是否保存到非易失性寄存器中,以及這些寄存器是否由被調用者保存。如果是這樣,可以檢查保存的非易失性寄存器值以確定參數值。
在接下來的幾節中,將詳細描述上述每一種技術,並通過示例說明如何使用它們。每一種技術都需要分解參數傳遞中涉及的調用者和被調用者函數。在下圖中,如果要查找傳遞給函數f2
的參數,則必須反匯編第2
幀以從源中查找參數,並且必須反匯編第0
幀以從其目標中查找參數。
確認參數源
該技術涉及確定加載到參數寄存器中的值的來源。它適用於常量值、全局數據結構、堆棧地址、存儲在堆棧上的值等源。
如下圖所示,反匯編調用程序 (X64caller) 顯示正在加載到RCX
、RDX
、R8
和R9
中以作為參數傳遞給函數X64callee
的值是從可以在調試器中檢查的源加載的,只要值沒有改變。
下面的示例應用此技術來查找函數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,
.
.
. );
使用#0
幀中的返回地址反匯編調用程序顯示以下指令。加載到R8
的值,即分配給參數3
的寄存器是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
非易失性寄存器作為參數源
該技術涉及查找是否正在從非易失性寄存器中讀取加載到參數寄存器中的值,以及是否正在將非易失性寄存器保存在堆棧中。
下圖展示了調用者(X64caller)和被調用者(X64Callee)的反匯編。 調用者調用被調用者之前的指令(左側)顯示正在加載到參數寄存器(RCX、RDX、R8 和 R9)的值正在從非易失性寄存器(RDI、R12、RBX 、R9)中讀取。被調用者的prolog
(圖右側)中的指令顯示這些非易失性寄存器正在保存到堆棧中,可以獲取這些保存的值,從而間接產生之前加載到參數寄存器中的值。
以下示例應用此技術來查找函數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,
.
.
. );
使用第1
幀中的返回地址反匯編調用程序顯示以下說明。加載到RCX
中的值,即分配給參數1
的寄存器正在從非易失性寄存器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)
反匯編被調用者會在函數的序言中顯示以下說明。RDI
寄存器被指令push rdi
保存在堆棧中。保存的值與加載到RCX
中的值相同。下一步是查找EDI
保存的內容。
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
編譯函數時,函數的prolog
將始終將參數寄存器的內容保存到堆棧上的參數歸位區域。但是,對於未使用/homeparams
編譯的函數,參數寄存器的內容可以寫入函數體中的任何位置的內存。
下圖展示了函數體的反匯編,其中寄存器RCX
、RDX
、R8
和R9
中的參數值被寫入堆棧。可以通過使用當前幀的堆棧指針的值顯示內存位置的內容來確定參數。
以下示例應用此技術來查找函數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
.
.
.
函數的第三個和第四個參數分別在R8
和R9
寄存器中。反匯編函數DispatchClientMessage
並查找從R8
或R9
到內存的任何寫入,會導致指令mov qword ptr [rsp+28h], r9
和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)
.
.
.
使用#27
幀的堆棧指針 (RSP) 的值,即00000000'0029dd30
,來自上面kn
命令的輸出,並添加存儲R8
寄存器的偏移量,顯示00000000'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
非易失性寄存器作為參數目標
該技術涉及查找參數寄存器的內容是否由所討論的函數保存到非易失性寄存器中,然后這些非易失性寄存器是否由被調用者保存在堆棧中。
下圖顯示了調用者 (X64Caller) 和被調用者 (X64Callee) 的反匯編。目的是查找傳遞給函數X64Caller
的基於寄存器的參數的值。函數X64Caller
的主體(顯示在左側)包含將參數寄存器(RCX、RDX、R8 和 R9)保存到非易失性寄存器(RDI、RSI、RBX、RBP)中的指令。函數X64Callee
的序言包含將這些非易失性寄存器保存到堆棧中的指令(顯示在右側),從而可以檢索它們的值,從而間接產生參數寄存器的值。
以下示例應用此技術來查找函數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()
的完全反匯編表明,在函數prolog
之后,參數寄存器通過mov ebx,edx
、mov rdi,rcx
、mov rsi,r9
和mov ebp,r8d
保存到非易失性寄存器中。重點檢查指令直到調用下一個函數,即CreateFileW
,以確定這些非易失性寄存器沒有被覆蓋。雖然這里沒有明確顯示,但這個已經通過檢查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 和 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
的第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
提供了一個調試器擴展命令!cmkd.stack -p
來自動化整個過程。此命令嘗試檢索並顯示出現在線程的X64
調用堆棧上的所有函數的參數。為了在用戶模式調試期間使用該命令檢索任何線程的參數,請使用~s
命令切換到該特定線程。同樣在內核模式調試期間使用.thread
命令。
本文介紹了編譯器在X64
上執行的一些優化,這些優化使生成的代碼與在X86
上生成的代碼大不相同。它討論了X64
上的異常處理機制,並展示了如何修改可執行文件格式和數據結構以支持此功能。然后討論了如何在運行時構建X64
堆棧幀,以及如何應用這些知識來檢索傳遞給X64
函數的基於寄存器的函數參數,從而克服X64
上的這個痛苦障礙。