如何通過WinDbg獲取方法引數值

2022-06-29 12:01:07

引入

我們在偵錯的過程中,經常會通過檢視方法的輸入與輸出來確定這個方法是否異常。那麼我們要怎麼通過 WinDbg 來獲取方法的引數值呢?

WinDbg 中主要包含三種命令:標準命令、元命令(以 . 開始)和擴充套件命令(以 ! 開始)。

通過標準命令獲取引數值

k 命令可以獲取棧回溯。

其中 kP 可以把引數和引數值都以函數原型格式顯示出來,但是需要有符號。如下:

0:000> kP
 # Child-SP          RetAddr               Call Site
00 0000001b`7b0fdb78 00007ffc`718366fb     ntdll!NtCreateUserProcess
01 0000001b`7b0fdb80 00007ffc`718732f6     KERNELBASE!CreateProcessInternalW+0x115b
02 0000001b`7b0ff510 00007ffc`728560c4     KERNELBASE!CreateProcessW+0x66
03 0000001b`7b0ff580 00007ff6`14a61960     KERNEL32!CreateProcessWStub+0x54
04 0000001b`7b0ff5e0 00007ff6`14a62419     CreateProcessWithCpp!main(
			int argc = 0n1, 
			wchar_t ** argv = 0x00000208`0b637d00)+0xe0 [C:\Users\frend\source\repos\debug-test\AdavageDebug\CreateProcessWithCpp\CreateProcessWithCpp.cpp @ 20] 
05 0000001b`7b0ff800 00007ff6`14a622be     CreateProcessWithCpp!invoke_main(void)+0x39 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 79] 
06 0000001b`7b0ff850 00007ff6`14a6217e     CreateProcessWithCpp!__scrt_common_main_seh(void)+0x12e [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 
07 0000001b`7b0ff8c0 00007ff6`14a624ae     CreateProcessWithCpp!__scrt_common_main(void)+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331] 
08 0000001b`7b0ff8f0 00007ffc`7285244d     CreateProcessWithCpp!mainCRTStartup(
			void * __formal = 0x0000001b`7aeca000)+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17] 
09 0000001b`7b0ff920 00007ffc`740cdf88     KERNEL32!BaseThreadInitThunk+0x1d
0a 0000001b`7b0ff950 00000000`00000000     ntdll!RtlUserThreadStart+0x28

0:000> dc 0x00000208`0b637d00
00000208`0b637d00  0b637d10 00000208 00000000 00000000  .}c.............
00000208`0b637d10  555c3a43 73726573 6572665c 735c646e  C:\Users\frend\s
00000208`0b637d20  6372756f 65725c65 5c736f70 75626564  ource\repos\debu
00000208`0b637d30  65742d67 415c7473 61766164 65446567  g-test\AdavageDe
00000208`0b637d40  5c677562 5c343678 75626544 72435c67  bug\x64\Debug\Cr
00000208`0b637d50  65746165 636f7250 57737365 43687469  eateProcessWithC
00000208`0b637d60  652e7070 fd006578 abfdfdfd abababab  pp.exe..........
00000208`0b637d70  abababab abababab feababab feeefeee  ................

可以看到,部分方法的引數和對應的值都顯示出來了,這裡用 CreateProcessWithCpp!main 為例。

同時,也可以看到部分方法儘管有有符號,也不一定能顯示出來。比如 ntdll!NtCreateUserProcess

如果我們就要看 ntdll!NtCreateUserProcess 的引數值呢?

還可以通過 kv 命令 顯示出前面的三個引數。例如:

0:000> kv L
 # Child-SP          RetAddr               : Args to Child                                                           : Call Site
00 0000001b`7b0fdb78 00007ffc`718366fb     : 0000001b`7b0fe1f8 0000001b`7b0fe3f0 0000001b`00000001 0000001b`7b0fdf34 : ntdll!NtCreateUserProcess
01 0000001b`7b0fdb80 00007ffc`718732f6     : 00000000`00000000 00000000`00000000 00007ff6`14a610eb 580000ff`ec77c5b6 : KERNELBASE!CreateProcessInternalW+0x115b
02 0000001b`7b0ff510 00007ffc`728560c4     : 0000001b`7b0ff588 00760065`0044005c 005c0065`00630069 00640072`00610048 : KERNELBASE!CreateProcessW+0x66
03 0000001b`7b0ff580 00007ff6`14a61960     : 00007ff6`14a710ac 00620065`0064005c 0074002d`00670075 005c0074`00730065 : KERNEL32!CreateProcessWStub+0x54
04 0000001b`7b0ff5e0 00007ff6`14a62419     : 00007891`00000001 00000208`0b637d00 00000000`00000000 00007ff6`14a63aed : CreateProcessWithCpp!main+0xe0
05 0000001b`7b0ff800 00007ff6`14a622be     : 00007ff6`14a69000 00007ff6`14a69220 00000000`00000000 00000000`00000000 : CreateProcessWithCpp!invoke_main+0x39
06 0000001b`7b0ff850 00007ff6`14a6217e     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : CreateProcessWithCpp!__scrt_common_main_seh+0x12e
07 0000001b`7b0ff8c0 00007ff6`14a624ae     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : CreateProcessWithCpp!__scrt_common_main+0xe
08 0000001b`7b0ff8f0 00007ffc`7285244d     : 0000001b`7aeca000 00000000`00000000 00000000`00000000 00000000`00000000 : CreateProcessWithCpp!mainCRTStartup+0xe
09 0000001b`7b0ff920 00007ffc`740cdf88     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x1d
0a 0000001b`7b0ff950 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x28

於是我們可以看到所有方法的引數值了。但遺憾的是:只能看到三個引數。

既然 WinDbg 能獲取到,那我們是不是也可以在記憶體中找到對應的引數。

在找引數在記憶體中的位置之前,我們需要了解方法呼叫的一些約定,針對這些約定,我們叫它:呼叫協定

呼叫協定

定義

  • 函數呼叫約定,是指當一個函數被呼叫時,函數的引數會被傳遞給被呼叫的函數和返回值會被返回給呼叫函數。
  • 函數的呼叫約定就是描述引數是怎麼傳遞和由誰平衡堆疊的,當然還有返回值

分類

cdecl 約定

c/c++ 預設的呼叫約定。

規則:

  • 引數採用棧傳遞
  • 從右到左入棧
  • 引數由呼叫方清理
  • 由 eax 作為方法返回值

stdcall 約定

startard call 的縮寫。微軟的標準約定,大多數 Win32 api 採用的都是 stdcall

規則:

  • 引數採用棧傳遞
  • 從右到左入棧
  • 引數由被呼叫方清理
  • 由 eax 作為方法返回值

fastCall 約定

fastCall 採用 ecx 和 edx 兩個暫存器來傳遞引數,優化效率

規則:

  • 前兩個引數分別採用 ecx edx 傳遞,其他引數仍然採用棧傳遞
  • 從右到左入棧
  • 引數由被呼叫方清理
  • 由 eax 作為方法返回值

X64 約定

針對 64 位平臺的 fastcall 變種,採用 ecx, edx, r8, r9 四個暫存器來傳遞方法的前四個引數

規則:

  • 前四個引數分別採用 ecx, edx, r8, r9 傳遞,其他引數仍然採用棧傳遞
  • 從右到左入棧
  • 引數由被呼叫方清理
  • 由 eax 作為方法返回值

記憶體佈局

我們偵錯一下程式碼,將程式碼停在 getSum → auto sum = a + b ,我們看看當前棧和引數,以及目前 ebp 所在記憶體地址的值。

0:000> kv L
 # ChildEBP RetAddr      Args to Child              
00 0111f708 010119a0     0000000a 0000000c 01011023 Example_4_1_2!getsum+0x25 (FPO: [Non-Fpo]) (CONV: cdecl)
01 0111f808 01012173     00000001 013db990 013dc6f8 Example_4_1_2!main+0x40 (FPO: [Non-Fpo]) (CONV: cdecl)
02 0111f828 01011fc7     037e2288 01011023 01011023 Example_4_1_2!invoke_main+0x33 (FPO: [Non-Fpo]) (CONV: cdecl)
03 0111f884 01011e5d     0111f894 010121f8 0111f8a4 Example_4_1_2!__scrt_common_main_seh+0x157 (FPO: [Non-Fpo]) (CONV: cdecl)
04 0111f88c 010121f8     0111f8a4 76267ba9 00e8c000 Example_4_1_2!__scrt_common_main+0xd (FPO: [Non-Fpo]) (CONV: cdecl)
05 0111f894 76267ba9     00e8c000 76267b90 0111f8fc Example_4_1_2!mainCRTStartup+0x8 (FPO: [Non-Fpo]) (CONV: cdecl)
06 0111f8a4 771eb7db     00e8c000 f2ae73dd 00000000 KERNEL32!BaseThreadInitThunk+0x19 (FPO: [Non-Fpo])
07 0111f8fc 771eb75f     ffffffff 7721869e 00000000 ntdll!__RtlUserThreadStart+0x2b (FPO: [Non-Fpo])
08 0111f90c 00000000     01011023 00e8c000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])
0:000> dp ebp
0111f708  0111f808 010119a0 0000000a 0000000c
0111f718  01011023 01011023 00e8c000 010118b1
0111f728  01011023 01011023 00e8c000 0111f750
0111f738  0111f750 5cb4259c cb13e9ed fffffffe
0111f748  0111f758 5cb3fa93 0edf5aca 0000001d
0111f758  0111f774 0111f774 5cb4259c 0111f77c
0111f768  5cb42c02 76fad650 0111f788 771e0559
0111f778  0111f788 5cb3fa93 0edf5aca 0000001d

可以看到,ebp 在記憶體中對應的值即是呼叫方的 ChildEBP ,也就是其中的0111f808;ebp + 4 即對應著當前方法的返回地址,也就是 010119a0 ;而後面則是當前方法的引數值,也是跟 kv 命令輸出的是一致的。