WinDbg: 関数呼び出しの IN と OUT をトレースする方法

WinDbg で、特定の関数が呼び出されたこと、また、そこからリターンしたことをトレースする方法について説明します。故障解析などで Win32 API 関数や内部関数の呼び出し状況を把握したい場合に役立ちます。

動作確認環境

  • Windows 11 Home 22H2
  • Visual Studio Community 2022 (Visual C++)
  • WinDbg 10.0

結論

いきなり結論ですが、次のようなブレークポイントを仕掛けることで目的が達成できます。

bp <モジュール名>!<関数名> "<関数呼び出し時に実行する命令>; bp @$ra \"<関数リターン時に実行する命令>; gc\"; gc"

ここで、

  • <モジュール名> は、 exe や dll の名前(ベースネーム)
  • <関数名> は、文字通り関数名
  • <関数呼び出し時に実行する命令> は、レジスタの表示やスタックの表示など、関数呼び出し時に実行したい任意の命令
  • <関数リターン時に実行する命令> は、同様に、関数リターン時に実行したい任意の命令

です。

また、

  • bp は、ブレークポイントを仕掛ける命令
  • bp の後ろのダブルクォーテーションで囲まれたテキストは、ブレークポイントにヒットしたときに実行する命令
  • bp @$ra は、当該関数のリターンアドレスにブレークポイントを仕掛ける、の意
  • \” は、すでにそのテキストがダブルクォーテーションで囲まれているがゆえのエスケープ
  • gc は、ブレークポイントヒット前と同じ方式(g:実行, p:ステップ実行, t:トレース、のいずれか)で処理を続行する命令

になります。

例 1. Windows API 関数のトレース

実際に試してみましょう。
Windows の API である GetComputerName 関数を呼び出してコンピューター名を取得するプログラムを作ります。

#include <windows.h>
#include <stdio.h>

int main()
{
    char pcBuf[MAX_COMPUTERNAME_LENGTH + 1] = { '\0' };
    DWORD dwSize = _countof(pcBuf);

    BOOL bRet = GetComputerName(pcBuf, &dwSize);

    printf("ret=%d, buf=%s, size=%d\n", bRet, pcBuf, dwSize);

    return 0;
}

32bit 版の Visual C++ でビルドします。「/Zi」はデバッグ情報を付加する、の意味です。

C:\tmp> cl /Zi test1.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x86
......

生成された「test1.exe」を 32bit 版の WinDbg から開きます。

シンボルの準備をします。「!sym noisy」でシンボルファイルのダウンロード状況を表示するよう指示し、「.symfix c:\symbols」でシンボルファイルの格納場所を指定し、「.reload /f」でシンボルファイルの即時ロードを指示しています。

0:000> !sym noisy
noisy mode - symbol prompts on

0:000> .symfix c:\symbols
DBGHELP: Symbol Search Path: SRV*c:\symbols*https://msdl.microsoft.com/download/symbols
......

0:000> .reload /f
Reloading current modules
......

GetComputerName 関数の場所を探します。「x」はシンボルを検索するコマンド、「!GetComputerName」内のアスタリスクはワイルドカードです。

0:000> x *!GetComputerName*
00368706    test1!GetComputerNameW (_GetComputerNameW@8)
003671d1    test1!GetComputerNameA (_GetComputerNameA@8)
75beecd0    KERNELBASE!GetComputerNameExA (_GetComputerNameExA@12)
75afc220    KERNELBASE!GetComputerNameExW (_GetComputerNameExW@12)
75dad920    KERNEL32!GetComputerNameExWStub (_GetComputerNameExWStub@12)
75dbff10    KERNEL32!GetComputerNameExAStub (_GetComputerNameExAStub@12)
75de5d90    KERNEL32!GetComputerNameW (_GetComputerNameW@8)
75de5cb0    KERNEL32!GetComputerNameA (_GetComputerNameA@8)

関数名の末尾の A は Ansi 版を、W は Unicode 版を意味しており、今回は Ansi 版でビルドしたので(/D “_UNICODE” /D “UNICODE” を付けずにビルドしたので)、「KERNEL32!GetComputerNameA」が目的の関数になります。

逆アセンブルしてみましょう。

0:000> uf KERNEL32!GetComputerNameA
KERNEL32!GetComputerNameA:
75de5cb0 8bff      mov     edi,edi
75de5cb2 55        push    ebp
75de5cb3 8bec      mov     ebp,esp
75de5cb5 83e4f8    and     esp,0FFFFFFF8h
75de5cb8 83ec2c    sub     esp,2Ch
......
75de5d82 8be5      mov     esp,ebp
75de5d84 5d        pop     ebp
75de5d85 c20800    ret     8

「push ebp; mov ebp, esp; ~ mov esp, ebp; pop ebp; ret 」という構造から、一般的な __stdcall 呼び出し規約であろうことが確認できます。

前述のとおり、ブレークポイントの仕掛け方は以下でした。

bp <モジュール名>!<関数名> "<関数呼び出し時に実行する命令>; bp @$ra \"<関数リターン時に実行する命令>; gc\"; gc"

<関数呼び出し時に実行する命令> は、今回は次のようにします。__stdcall 呼び出し規約(引数や戻り値の受け渡し方)の説明は省略しますが、第 2 引数で渡したポインタの中身(= バッファのサイズ)を表示するよう指示しています。

.echo *** GetComputerNameA enter ***
? poi(poi(@esp+8))

<関数リターン時に実行する命令> は、今回は次のようにします。関数の戻り値と、取得したコンピュータ名と、その長さを表示するよう指示しています。

.echo *** GetComputerNameA exit ***
r eax
da poi(@esp-8)
? poi(poi(@esp-4))

したがって、次のようにブレークポイントを仕掛けます。

0:000> bp KERNEL32!GetComputerNameA ".echo *** GetComputerNameA enter ***; ? poi(poi(@esp+8)); bp @$ra \".echo *** GetComputerNameA exit ***; r eax; da poi(@esp-8); ? poi(poi(@esp-4)); gc\"; gc"

プログラムを実行します。

0:000> g
*** GetComputerNameA enter ***
Evaluate expression: 16 = 00000010
*** GetComputerNameA exit ***
eax=00000001
0093fadc  "DELL201908"
Evaluate expression: 10 = 0000000a

画面キャプチャです。

期待通り GetComputerNameA 関数のトレース情報が、具体的には

  • GetComputerNameA 関数が呼び出されたこと
  • バッファのサイズは 16 桁だったこと
  • GetComputerNameA 関数から抜けたこと
  • 戻り値は 1 だったこと
  • コンピュータ名は “DELL201908” であったこと
  • コンピューター名の長さは 10 桁であったこと

が表示されました。

もう少し頑張って次のように書くと

0:000> bp KERNEL32!GetComputerNameA ".echo *** GetComputerNameA enter ***; .printf \"size=0n%d\\n\", poi(poi(@esp+8)); bp @$ra \".echo *** GetComputerNameA exit ***; .printf \\\"ret=0n%d\\\\n\\\", eax; .printf \\\"buf=%ma\\\\n\\\", poi(@esp-8); .printf \\\"size=0n%d\\\\n\\\", poi(poi(@esp-4)); gc\"; gc"

より分かりやすく表示されます。

0:000> g
*** GetComputerNameA enter ***
size=0n16
*** GetComputerNameA exit ***
ret=0n1
buf=DELL201908
size=0n10

例 2. 再帰のある関数のトレース

続いて、再帰していたり関数の出口が複数あったりしても正常にトレースできるか確認します。
作ったプログラムは以下です。sum 関数は、再帰を利用して 1 から n までの合計を求める関数です。ここでは 1 から 10 までの合計を求めています。

#include <stdio.h>

int sum(int n)
{
    if (n <= 0)
    {
        return 0;
    }

    return n + sum(n - 1);
}

int main()
{
    printf("%d\n", sum(10));

    return 0;
}

今度は 32bit 版ではなく 64bit 版の Visual C++ でビルドします。

C:\tmp> cl /Zi test2.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x64
......

生成された「test2.exe」を 64bit 版の WinDbg から開きます。

シンボルの準備をします。

0:000> !sym noisy
noisy mode - symbol prompts on

0:000> .symfix c:\symbols
DBGHELP: Symbol Search Path: SRV*c:\symbols*https://msdl.microsoft.com/download/symbols
......

0:000> .reload /f
Reloading current modules
......

sum 関数の場所を確認します。

0:000> x test2!sum*
00007ff7`7ba770d0 test2!sum (int)

sum 関数を逆アセンブルしてみましょう。

0:000> uf test2!sum
test2!sum [C:\tmp\test2.c @ 4]:
    4 00007ff7`7ba770d0 894c2408        mov     dword ptr [rsp+8],ecx
    4 00007ff7`7ba770d4 4883ec28        sub     rsp,28h
......
test2!sum+0x28 [C:\tmp\test2.c @ 11]:
   11 00007ff7`7ba770f8 4883c428        add     rsp,28h
   11 00007ff7`7ba770fc c3              ret

「sub rsp, ; ~ add rsp, ; ret」という構造から、一般的な x64 呼び出し規約であろうことが確認できます。

前述のとおり、ブレークポイントの仕掛け方は以下でした。

bp <モジュール名>!<関数名> "<関数呼び出し時に実行する命令>; bp @$ra \"<関数リターン時に実行する命令>; gc\"; gc"

<関数呼び出し時に実行する命令> は、今回は次のようにします。x64 呼び出し規約の説明は省略しますが、第 1 引数の値を表示するよう指示しています。

.echo *** sum enter ***
? @rcx

<関数リターン時に実行する命令> は、今回は次のようにします。32bit 整数の戻り値を表示するよう指示しています。

.echo *** sum exit ***
? @eax

したがって、次のようにブレークポイントを仕掛けます。

0:000> bp test2!sum ".echo *** sum enter ***; ? @rcx; bp @$ra \".echo *** sum exit ***; ? @eax; gc\"; gc"

プログラムを実行します。

0:000> g
*** sum enter ***
Evaluate expression: 10 = 00000000`0000000a
*** sum enter ***
Evaluate expression: 9 = 00000000`00000009
*** sum enter ***
Evaluate expression: 8 = 00000000`00000008
breakpoint 4 redefined
*** sum enter ***
Evaluate expression: 7 = 00000000`00000007
breakpoint 4 redefined
*** sum enter ***
Evaluate expression: 6 = 00000000`00000006
breakpoint 4 redefined
*** sum enter ***
Evaluate expression: 5 = 00000000`00000005
breakpoint 4 redefined
*** sum enter ***
Evaluate expression: 4 = 00000000`00000004
breakpoint 4 redefined
*** sum enter ***
Evaluate expression: 3 = 00000000`00000003
breakpoint 4 redefined
*** sum enter ***
Evaluate expression: 2 = 00000000`00000002
breakpoint 4 redefined
*** sum enter ***
Evaluate expression: 1 = 00000000`00000001
breakpoint 4 redefined
*** sum enter ***
Evaluate expression: 0 = 00000000`00000000
breakpoint 4 redefined
*** sum exit ***
Evaluate expression: 0 = 00000000`00000000
*** sum exit ***
Evaluate expression: 1 = 00000000`00000001
*** sum exit ***
Evaluate expression: 3 = 00000000`00000003
*** sum exit ***
Evaluate expression: 6 = 00000000`00000006
*** sum exit ***
Evaluate expression: 10 = 00000000`0000000a
*** sum exit ***
Evaluate expression: 15 = 00000000`0000000f
*** sum exit ***
Evaluate expression: 21 = 00000000`00000015
*** sum exit ***
Evaluate expression: 28 = 00000000`0000001c
*** sum exit ***
Evaluate expression: 36 = 00000000`00000024
*** sum exit ***
Evaluate expression: 45 = 00000000`0000002d
*** sum exit ***
Evaluate expression: 55 = 00000000`00000037

画面キャプチャです。

期待通り、再帰による計算の過程と、1 から 10 までの合計である 55 が表示されました。

おわりに

WinDbg で任意の関数のトレースを取る方法について説明しました。
GUI でポチポチというわけにはいきませんが、関数呼び出し時のコールスタックを表示したり (kP)、特定の条件を満たした場合に一時停止するようにしたりもできますし (.if (~) {~} .else {~})、ソースコードがなくても動作を追うことができますし、自由度の高い点が魅力です。

参考文献

[1] Windbg: “gu” command inside of a breakpoint causes warning, Stack Exchange,
https://reverseengineering.stackexchange.com/questions/24696/windbg-gu-command-inside-of-a-breakpoint-causes-warning

[2] dolduke: doldukeの日記, WinDbg にはまる,
https://dolduke.hatenadiary.org/entries/2010/09/16