深入 x64

寂靜的羽夏發表於2022-03-30

  本篇原文為 X64 Deep Dive,如果有良好的英文基礎的能力,可以點選該連結進行閱讀。本文為我個人:寂靜的羽夏(wingsummer) 中文翻譯,非機翻,著作權歸原作者所有。
  由於原文十分冗長,也十分乾貨,採用機翻輔助,人工閱讀比對修改的方式進行,如有翻譯不得當的地方,歡迎批評指正。翻譯不易,如有閒錢,歡迎支援。注意在轉載文章時注意保留原文的作者連結,我(譯者)的相關資訊。話不多說,正文開始:

關於 X64 平臺執行和除錯關鍵方面的深度教程,例如編譯器優化、異常處理、引數傳遞、堆疊佈局以及引數的獲取。

深入 x64

  本教程討論在X64 CPU上執行程式碼的一些重要內容,例如編譯器優化、異常處理、引數傳遞和引數的獲取,並解釋它們的密切聯絡。本篇文章涵蓋了重要偵錯程式命令,並介紹理解這些命令的輸出結果的必要前置知識,強調X64 CPUX86 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中內聯function2Function3這兩個函式的示意圖如下:

深入 x64

尾函式呼叫平棧

  X64編譯器可以通過將函式替換為跳轉到被呼叫者來優化函式的最後一次呼叫。這避免了為被呼叫者配置堆疊幀的開銷。呼叫者和被呼叫者共享同一個棧幀,被呼叫者直接返回到呼叫者的呼叫函式。當呼叫者和被呼叫者具有相同的引數時,這尤其有用,因為如果相關引數已經在所需的暫存器中並且這些暫存器沒有更改,則不必重新載入它們。下圖展示了了在呼叫Function4Function1中的尾函式呼叫平棧。Function1跳轉到Function4,當Function4執行完畢後,直接返回給Function1的呼叫者。

深入 x64

幀指標優化

  與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。事實上,SleepSleepEx最終使用相同的堆疊幀,這是尾呼叫優化的一個示例。

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暫存器,作為訪問區域性變數和引數的參考。由於pushpop指令會改變堆疊指標,因此X64函式將pushpop指令分別限制為函式prologepilog。堆疊指標在prologepilog之間一定保持不變這一事實是X64函式的一個特徵,如下圖所示:

深入 x64

  以下程式碼片段展示了函式user32!DrawTestExW的完整內容。該函式的prolog以指令sub rsp, 48h結束,epilog以指令add rsp, 48h開始。由於prologepilog之間的指令使用RSP作為參考訪問堆疊內容,因此函式體中間沒有pushpop指令。

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可執行檔案使用一種檔案格式,它是用於X86PE檔案格式的變體,稱為PE32+。此類檔案有一個稱為.pdata或異常目錄的額外部分,其中包含用於處理異常的資訊。該異常目錄包含可執行檔案中每個非葉函式的RUNTIME_FUNCTION結構。非葉函式是呼叫其他函式的函式,每個RUNTIME_FUNCTION結構包含函式中第一條和最後一條指令的偏移量(即函式範圍)和一個指向展開資訊結構的指標,該結構描述了在發生異常時如何展開函式的呼叫堆疊。下圖展示了一個模組的RUNTIME_FUNCTION結構,該結構包含該模組中函式開頭和結尾的偏移量。

深入 x64

  以下彙編程式碼片段展示了與X86X64上的異常處理相關的程式碼生成方面的一些差異。在x86上,當高階語言C/C++程式碼包含結構化異常處理結構,如__try/__except時,編譯器會在執行時在堆疊上構建異常幀的函式的prologepilog中生成特殊程式碼。這可以在下面呼叫ntdll!_SEH_prolog4ntdll!_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結構的BeginAddressEndAddress欄位分別儲存函式程式碼在虛擬記憶體中從模組開始的偏移量。當函式產生異常時,作業系統會掃描PE檔案的記憶體對映副本,尋找包含當前指令地址的RUNTIME_FUNCTION結構體。RUNTIME_FUNCTION結構的UnwindData欄位包含另一個結構的偏移量,它告訴作業系統執行時它應該如何展開堆疊,這是UNWIND_INFO結構。UNWIND_INFO結構包含數量不定的UNWIND_CODE結構,每個結構都儲存著回滾恢復函式prolog執行的單個堆疊相關操作影響的資訊。
  對於動態生成的程式碼,作業系統支援函式RtlAddFunctionTableRtlInstallFunctionTableCallback用於在執行時建立RUNTIME_FUNCTION資訊。
  下圖展示了RUNTIME_FUNCTIONUNWIND_INFO結構之間的關係以及函式在記憶體中的位置:

深入 x64

  偵錯程式的.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新增到模組的基部,即包含函式RtlUserThreadStartntdll.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結構的BeginAddressEndAddress欄位描述了相應函式在記憶體中的位置。然而,有一個優化可以在模組連結後應用於模組,這可能會改變上述觀察結果,稍後會詳細介紹。
  儘管UNWIND_INFOUNWIND_CODE結構的主要目的是描述堆疊在異常期間是如何展開的,但偵錯程式使用此資訊來遍歷呼叫堆疊,而無需訪問模組的符號。每個UNWIND_CODE結構都可以描述函式prolog執行的以下操作之一:

  • SAVE_NONVOL - 在堆疊上儲存一個非易失性暫存器。
  • PUSH_NONVOL - 將非易失性暫存器壓入堆疊。
  • ALLOC_SMALL - 在堆疊上分配空間(最多 128 個位元組)。
  • ALLOC_LARGE - 在堆疊上分配空間(最多 4GB)。

  因此,本質上UNWIND_CODE是函式prolog的後設資料表示。
  下圖展示函式prolog執行的與堆疊相關的操作之間的關係以及這些操作在UNWIND_CODE結構中的描述。UNWIND_CODE結構以它們所代表的指令的相反順序出現,因此在異常期間,堆疊可以在它建立的相反方向上展開。

深入 x64

  以下示例顯示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.exeUNWIND_INFOUNWIND_CODE結構。每個UNWIND_CODE結構都描述了函式的prolog執行的類似PUSH_NONVOLALLOC_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 ToolsBBT) 的配置檔案引導優化,這增加了程式碼的空間區域性性。經常執行的功能部分被儲存在一起,可能在同一頁面中,不經常使用的部分被移動到其他位置。這減少了需要為最常執行的程式碼路徑保留在記憶體中的頁面數量,最終導致整體工作集減少。為了應用這種優化,二進位制檔案被連結、執行、分析,然後分析資料用於根據執行頻率重新排列函式的各個部分。
  在最終的函式中,函式的一些程式碼塊被移到函式主體之外,該主體最初由RUNTIME_FUNCTION結構的範圍定義。由於程式碼塊的移動,函式體被分解為多個不連續的部分,因此最初由連結器生成的RUNTIME_FUNCTION結構不再能夠準確識別這些函式的範圍。為了解決這個問題,BBT程式新增了多個新的RUNTIME_FUNCTION結構,每個結構定義了一個具有優化功能的連續程式碼塊。這些RUNTIME_FUNCTION結構與終止於原始RUNTIME_FUNCTION結構的鏈連線在一起,該結構的BeginAddress始終指向函式的開頭。
  下圖展示由三個基本塊組成的函式。應用BBT程式塊#2後移出函式體,導致原始RUNTIME_FUNCTION中的資訊變為無效。因此,BBT程式建立了第二個RUNTIME_FUNCTION結構並將其連結到第一個結構,從而描述了整個函式。

深入 x64

  當前公共版本的偵錯程式不會遍歷完整的 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_FUNCTIONUNWIND_INFOUNWIND_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彙編程式碼,描述了呼叫者如何將引數傳遞給被呼叫者。

深入 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,rdimov edx,ebxmov r8d,ebpmov r9,rsi顯示前4個引數被移動到暫存器中,為呼叫kernel32!CreateFileW做準備。類似地,指令mov dword ptr [rsp+20h],eaxmov dword ptr [rsp+28h],eaxmov qword ptr [rsp+30h],rax顯示其餘引數,即57被放到棧中。

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如何在此引數預留空間中儲存非易失性暫存器。

深入 x64

  在下面的示例中,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)
.
.
.

  以下WinMainprolog的彙編程式碼片段展示了四個非易失性暫存器儲存在指定為引數預留區域的堆疊上的位置。

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從暫存器中讀取引數值並將這些值儲存在引數預留區域的堆疊中。

深入 x64

  下面的程式碼片段展示了暫存器值被移動到由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暫存器指向圖中所示的位置。

深入 x64

  偵錯程式的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

  下面的彙編程式碼片段顯示了函式CreateFileWprolog,它將非易失性暫存器r8dedx儲存到引數歸位區,將rbxrbpesiedi壓入堆疊,併為本地分配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指向函式f2call f1指令之後的指令。此返回地址出現在RSP2指向的位置旁邊的呼叫堆疊上。

深入 x64

  在下面的呼叫堆疊中,幀#01Child-SP的值為00000000'0029bc00。這是在CreateFileWprolog剛剛完成時執行點的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_FUNCTIONUNWIND_INFOUNWIND_CODE結構來計算呼叫堆疊中每個函式的堆疊空間利用率,並將這些值新增到Child-SP以計算後續Child-SP的值。
  下圖顯示了函式堆疊框架的佈局。堆疊幀的總大小(或堆疊空間利用率)可以通過將返回地址的大小(8 位元組)和非易失性暫存器、區域性變數、基於堆疊的佔用的堆疊空間量相加來計算被呼叫者的引數和為四個基於暫存器的引數(0x20位元組)分配的歸位空間。UNWIND_CODE結構表示被壓入堆疊的非易失性暫存器的數量以及為區域性變數和引數分配的空間量。

深入 x64

  在下面的堆疊跟蹤中,第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_FUNCTIONUNWIND_INFOUNWIND_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個引數保證在暫存器RCXRDX、分別為R8R9
  但是,隨著函式體中的執行,引數暫存器的內容會發生變化,並且初始引數值會被覆蓋。因此,要在函式執行期間的任何時候確定這些基於暫存器的引數的值,需要明確從哪裡讀取引數的值以及寫入的引數值在哪裡。可以通過在偵錯程式中執行一系列步驟來找到這些問題的答案,這些步驟可以分為如下:

  • 確定引數是否從記憶體載入到暫存器中。如果是這樣,可以檢查記憶體位置以確定引數值。
  • 確定引數是否從非易失性暫存器載入,以及這些暫存器是否由被呼叫者儲存。如果是這樣,可以檢查儲存的非易失性暫存器值以確定引數值。
  • 確定引數是否從暫存器儲存到記憶體中。如果是這樣,可以檢查記憶體位置以確定引數值。
  • 確定引數是否儲存到非易失性暫存器中,以及這些暫存器是否由被呼叫者儲存。如果是這樣,可以檢查儲存的非易失性暫存器值以確定引數值。

  在接下來的幾節中,將詳細描述上述每一種技術,並通過示例說明如何使用它們。每一種技術都需要分解引數傳遞中涉及的呼叫者和被呼叫者函式。在下圖中,如果要查詢傳遞給函式f2的引數,則必須反彙編第2幀以從源中查詢引數,並且必須反彙編第0幀以從其目標中查詢引數。

深入 x64

確認引數源

  該技術涉及確定載入到引數暫存器中的值的來源。它適用於常量值、全域性資料結構、堆疊地址、儲存在堆疊上的值等源。
  如下圖所示,反彙編呼叫程式 (X64caller) 顯示正在載入到RCXRDXR8R9中以作為引數傳遞給函式X64callee的值是從可以在偵錯程式中檢查的源載入的,只要值沒有改變。

深入 x64

  下面的示例應用此技術來查詢函式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(圖右側)中的指令顯示這些非易失性暫存器正在儲存到堆疊中,可以獲取這些儲存的值,從而間接產生之前載入到引數暫存器中的值。

深入 x64

  以下示例應用此技術來查詢函式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編譯的函式,引數暫存器的內容可以寫入函式體中的任何位置的記憶體。
  下圖展示了函式體的反彙編,其中暫存器RCXRDXR8R9中的引數值被寫入堆疊。可以通過使用當前幀的堆疊指標的值顯示記憶體位置的內容來確定引數。

深入 x64

  以下示例應用此技術來查詢函式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
. 
. 
.

  函式的第三個和第四個引數分別在R8R9暫存器中。反彙編函式DispatchClientMessage並查詢從R8R9到記憶體的任何寫入,會導致指令mov qword ptr [rsp+28h], r9mov qword ptr [rsp+20h], r8指示的第三個和第四個引數被寫入堆疊。這些指令不是函式序言的一部分,而是更大的函式體的一部分。請務必注意這一點,因為R8R9暫存器的值可能在寫入堆疊之前已被修改。儘管在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的序言包含將這些非易失性暫存器儲存到堆疊中的指令(顯示在右側),從而可以檢索它們的值,從而間接產生引數暫存器的值。

深入 x64

  以下示例應用此技術來查詢函式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,edxmov rdi,rcxmov rsi,r9mov 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上的這個痛苦障礙。

相關文章