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

frendguo發表於2022-06-29

引入

我們在除錯的過程中,經常會通過檢視方法的輸入與輸出來確定這個方法是否異常。那麼我們要怎麼通過 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 命令輸出的是一致的。

於是我們就可以返回到怎麼找到 ntdll!NtCreateUserProcess 的引數值了。

直接去記憶體上去找

由於ntdll!NtCreateUserProcess 沒有官方文件來描述它的介面定義,所以這裡不用它來驗證了。採用有文件可以驗證的方法:KERNELBASE!CreateProcessW 。其 Microsoft Docs 地址:CreateProcessW function (processthreadsapi.h) - Win32 apps | Microsoft Docs

從文件中把 KERNELBASE!CreateProcessW 的定義抄下來:

BOOL CreateProcessW(
  [in, optional]      LPCWSTR               lpApplicationName,
  [in, out, optional] LPWSTR                lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCWSTR               lpCurrentDirectory,
  [in]                LPSTARTUPINFOW        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);

我們先把斷點斷在 KERNELBASE!CreateProcessW ,然後再來看棧和記憶體。這裡我們以找 lpCommandLine (第二個引數)為例:

對於 32bit 的應用

x86 的 Win32 應用採用的是 stdcall 的呼叫約束。所以我們需要去棧中找:

0:000> bu KERNELBASE!CreateProcessW
0:000> g
Breakpoint 0 hit
KERNELBASE!CreateProcessW:
76fc4eb0 8bff            mov     edi,edi
0:000> k L
 # ChildEBP RetAddr      
00 00cffa04 008c1915     KERNELBASE!CreateProcessW
01 00cffb88 008c2213     CreateProcessWithCpp!main+0xb5
02 00cffba8 008c2067     CreateProcessWithCpp!invoke_main+0x33
03 00cffc04 008c1efd     CreateProcessWithCpp!__scrt_common_main_seh+0x157
04 00cffc0c 008c2298     CreateProcessWithCpp!__scrt_common_main+0xd
05 00cffc14 76267ba9     CreateProcessWithCpp!mainCRTStartup+0x8
06 00cffc24 771eb7db     KERNEL32!BaseThreadInitThunk+0x19
07 00cffc7c 771eb75f     ntdll!__RtlUserThreadStart+0x2b
08 00cffc8c 00000000     ntdll!_RtlUserThreadStart+0x1b

然後我們先看看暫存器上的值。

0:000> r
eax=00cffb24 ebx=00a03000 ecx=00cffb3c edx=00cffb04 esi=00cffa34 edi=00cffb88
eip=76fc4eb0 esp=00cffa08 ebp=00cffb88 iopl=0         nv up ei pl nz ac po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000212
KERNELBASE!CreateProcessW:
76fc4eb0 8bff            mov     edi,edi

再來看看記憶體上的值

0:000> dp esp
00cffa08  008c1915 00000000 **00cffb04** 00000000
00cffa18  00000000 00000000 00000000 00000000
00cffa28  00000000 00cffb3c 00cffb24 008c1023
00cffa38  008c1023 00a03000 008c1023 00a03000
00cffa48  00cffa60 e8824047 00cffa5c 689407f5
00cffa58  68a24080 00cffa9c 00000000 00cffa70
00cffa68  008c1023 008c1023 00a03000 00cffa98
00cffa78  6890c88f 00cffa88 689407f5 68a24080
0:000> dc **00cffb04 L8**
00cffb04  006f006e 00650074 00610070 002e0064  n.o.t.e.p.a.d...
00cffb14  00780065 00000065 cccccccc cccccccc  e.x.e...........

對於 64bit 應用

X64 應用中,呼叫約束採用的是 X64 的約束。也就是前四個引數會分別存在 ecx, edx, r8, r9 中。我們這裡要找的是第二個引數,所以我們直接去看 edx(rdx) 就可以了(當然,這裡斷點需要斷在棧幀首,避免被修改)

0:000> k L
 # Child-SP          RetAddr               Call Site
00 000000ef`4073f768 00007ffc`728560c4     KERNELBASE!CreateProcessW
01 000000ef`4073f770 00007ff6`e3f91960     KERNEL32!CreateProcessWStub+0x54
02 000000ef`4073f7d0 00007ff6`e3f92419     CreateProcessWithCpp!main+0xe0
03 000000ef`4073f9f0 00007ff6`e3f922be     CreateProcessWithCpp!invoke_main+0x39
04 000000ef`4073fa40 00007ff6`e3f9217e     CreateProcessWithCpp!__scrt_common_main_seh+0x12e
05 000000ef`4073fab0 00007ff6`e3f924ae     CreateProcessWithCpp!__scrt_common_main+0xe
06 000000ef`4073fae0 00007ffc`7285244d     CreateProcessWithCpp!mainCRTStartup+0xe
07 000000ef`4073fb10 00007ffc`740cdf88     KERNEL32!BaseThreadInitThunk+0x1d
08 000000ef`4073fb40 00000000`00000000     ntdll!RtlUserThreadStart+0x28
0:000> r
rax=0000000000000000 rbx=0000000000000000 rcx=0000000000000000
rdx=000000ef4073f8e8 rsi=00007ff6e3f99d58 rdi=000000ef4073f900
rip=00007ffc71873290 rsp=000000ef4073f768 rbp=000000ef4073f820
 r8=0000000000000000  r9=0000000000000000 r10=00007ffba21c0000
r11=000000ef4073f7c8 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
KERNELBASE!CreateProcessW:
00007ffc`71873290 4c8bdc          mov     r11,rsp
0:000>  dc 000000ef4073f8e8 L8
000000ef`4073f8e8  006f006e 00650074 00610070 002e0064  n.o.t.e.p.a.d...
000000ef`4073f8f8  00780065 00000065 cccccccc cccccccc  e.x.e...........

於是我們就找到了 notepad.exe 也就是第二個引數。

其他引數類似。

總結

要想看方法的引數:

  1. 通常情況下,可以通過 kp 直接檢視到。但需要有符號且有引數資訊。
  2. 對於引數個數在三個以內的,可以通過 kv 顯示前三個引數。
  3. 對於多個引數的,只能手動通過 dp 去看 ebp/esp 所在地址,通過記憶體分佈,手動推算。
  4. 對於 fastcall/x64 這種會通過暫存器來傳參的,需要特別注意,避免暫存器被修改。

其他方式

總體下來,用 WinDbg 來檢視引數還是相對複雜了些。還有些其他工具,用起來就會直觀許多。

OllyDbg

OD 也是一款非常經典的 Debugger,因為它有比較好的 UI 互動,所以用來看函式引數值就相對比較簡單。這裡簡單介紹下:

  1. 開啟檔案。(因為 OD 支援 X86,所以這裡只用 X86 的執行檔案做演示,X64 的還是乖乖的用 windbg 吧)

  2. 滑鼠右鍵→search for→All intermodular calls→找到 KERNEL32.CreateProcessW 的呼叫。然後雙擊。

  3. 於是,就能看到一個這樣的介面。可以看到 [LOCAL.7] 就是我們要找的 CommandLine(第二個引數)

  4. 然後我們把游標放在 KERNEL32.CreateProcessW 那一行,F4一下。看看右下角的棧:

於是,我們就很快的看出來第二個引數的值。

附錄

  1. CreateProcessWithCpp.cpp
#include <windows.h>
#include <stdio.h>
#include <tchar.h>

void main(int argc, TCHAR* argv[])
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    ZeroMemory(&pi, sizeof(pi));

    si.cb = sizeof(si);

    wchar_t cmd[] = L"notepad.exe";

    if (!CreateProcess(NULL, cmd, NULL, NULL, false, 0, NULL, NULL, &si, &pi))
    {
        printf("CreateProcess failed (%d).\n", GetLastError());
        return;
    }

    WaitForSingleObject(pi.hProcess, INFINITE);

    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}
  1. CreateProcessW 定義:https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw

相關文章