WinDbg: 条件付きブレークポイントで「ある関数を経由してきたか」をチェックする方法
条件付きブレークポイント (Conditional Breakpoint) では、よく「引数や変数がある値になっているか」をチェックしますが、「ある関数を経由してきたか」はどうやってチェックすればいいのでしょうか。
動作確認環境
- Windows 11 Home 23H2
- Visual Studio Community 2022 (Visual C++)
- WinDbg 10.0
「ある関数を経由してきたか」とは
下図のように、関数 common に至るルートが 4 通りあったとします。
単純に関数 common にブレークポイントを仕掛けると、① から ④ のどのルートを通ってきてもブレークポイントにヒットし一時停止します。
そうではなく、関数 B を経由して関数 common に到達した場合にのみ一時停止させたい、つまり ③ または ④ のルートを通ってきた場合のにみ一時停止させたい、これがやりたいことです。
実現方法 1
条件付きブレークポイントの書き方には、次の 2 種類があります。前者が新しいスタイルで、後者が昔ながらのスタイルです。
- bp /w “<条件式>” <停止アドレス>
- bp <停止アドレス> “<コマンド>”
前者のスタイルで「ある関数を経由してきたか」をチェックできないか試行錯誤したところ、以下の方法で実現できました。「<関数名>」の後ろに 1 文字の半角空白がある点に注意してください。
bp /w "@$curstack.Frames.Any(p=>p.ToDisplayString().StartsWith(\"<モジュール名>!<関数名> \"))" <停止アドレス>
長いので折り返します。実際には 1 行で書く必要があります。
bp /w "@$curstack.Frames.Any(p=>
p.ToDisplayString().StartsWith(
\"<モジュール名>!<関数名> \"))"
<停止アドレス>
日本語に翻訳 (?) すると、「ブレークポイント (bp) を <停止アドレス> に仕掛ける。ただし、コールスタック (@$curstack) の各フレーム (.Frames) の情報 (p) を文字列化 (.ToDisplayString()) し、”<モジュール名>!<関数名> ” で前方一致検索 (.StartsWith()) した結果、マッチする行がひとつでも (.Any) 存在した場合 (/w) にのみ有効とする。」となります。(※ 場合によっては .StartsWith() ではなく .Contains() を使うとよい。)
たとえば、「test モジュールの関数 B」を経由して「test モジュールの関数 common」に到達した場合にのみ停止させるには、次のように書きます。
bp /w "@$curstack.Frames.Any(p=>
p.ToDisplayString().StartsWith(
\"test!B \"))"
test!common
実際に試してみましょう。
サンプルプログラム「c:\tmp\test.c」です。上に図示したルート ①, ②, ③, ④ を順次実行しています。
#include <windows.h>
#include <stdio.h>
void common()
{
printf("common\n");
}
void C()
{
printf("C -> ");
common();
}
void B(BOOL callC)
{
printf("B -> ");
callC ? C() : common();
}
void A()
{
printf("A -> ");
common();
}
int main()
{
printf("① main -> ");
common();
printf("② main -> ");
A();
printf("③ main -> ");
B(FALSE);
printf("④ main -> ");
B(TRUE);
return 0;
}
Visual C++ のコマンドプロンプトを起動し、
ビルドします。「/Od」は最適化抑止(デフォルト)、「/Zi」はデバッグ情報を生成、の意味です。
C:\tmp> cl /Od /Zi test.c
「test.exe」が生成されました。
「test.exe」を普通に実行すると、各ルートが表示されます。
C:\tmp> test.exe
① main -> common
② main -> A -> common
③ main -> B -> common
④ main -> B -> C -> common
次に「test.exe」をデバッガ上で実行します。
WinDbg を起動し、メニューの [File]-[Open Executable] から「test.exe」を開きます。
「test モジュールの関数 B」を経由して「test モジュールの関数 common」に到達した場合にのみ停止するよう、WinDbg の [Command] ウィンドウからブレークポイントを仕掛けます。
0:000> bp /w "@$curstack.Frames.Any(p=>p.ToDisplayString().StartsWith(\"test!B \"))" test!common
実行を継続します。
0:000> g
ブレークポイントにヒットしました。
Breakpoint 0 hit
test!common:
00007ff6`448d7190 4883ec28 sub rsp,28h
プログラムの実行画面には次のように表示されています。
① main -> common
② main -> A -> common
③ main -> B ->
画面キャプチャーです。
ルート ① とルート ② では止まらず、ルート ③ の common に入ったところで止まっています
dx コマンドでコールスタックを見ると、test!B から test!common が呼び出されていることが確認できます。
0:000> dx @$curstack.Frames
@$curstack.Frames
[0x0] : test!common [Switch To]
[0x1] : test!B + 0x2f [Switch To]
[0x2] : test!main + 0x39 [Switch To]
[0x3] : test!invoke_main + 0x22 [Switch To]
[0x4] : test!__scrt_common_main_seh + 0x10c [Switch To]
[0x5] : KERNEL32!BaseThreadInitThunk + 0x1d [Switch To]
[0x6] : ntdll!RtlUserThreadStart + 0x28 [Switch To]
実行を継続します。
0:000> g
再びブレークポイントにヒットしました。
Breakpoint 0 hit
test!common:
00007ff6`448d7190 4883ec28 sub rsp,28h
プログラムの実行画面には次のように表示されています。
① main -> common
② main -> A -> common
③ main -> B -> common
④ main -> B -> C ->
ルート ④ の common に入ったところで止まっています。
dx コマンドでコールスタックを見ると、test!B を経由して test!common に到達していることが確認できます。
0:000> dx @$curstack.Frames
@$curstack.Frames
[0x0] : test!common [Switch To]
[0x1] : test!C + 0x15 [Switch To]
[0x2] : test!B + 0x20 [Switch To]
[0x3] : test!main + 0x4f [Switch To]
[0x4] : test!invoke_main + 0x22 [Switch To]
[0x5] : test!__scrt_common_main_seh + 0x10c [Switch To]
[0x6] : KERNEL32!BaseThreadInitThunk + 0x1d [Switch To]
[0x7] : ntdll!RtlUserThreadStart + 0x28 [Switch To]
以上のように、想定通りの条件で停止しました。
なお、「dx @$curstack.Frames」コマンドでコールスタックを見ると各フレームに [0x2] などのインデックスや [Switch To] といった DML が表示されますが、文字列化 (ToDisplayString()) すると消えます。次のコマンドで分かります。
0:000> dx @$curstack.Frames[2].ToDisplayString()
@$curstack.Frames[2].ToDisplayString() : test!B + 0x20
Length : 0xd
実現方法 2
昔ながらのスタイルで実現する方法についても考えてみました。
その結果です。
bp <停止アドレス> "r $t1=0; .foreach /pS4 /ps1 (v {kc}) { .if $scmp(\"${v}\", \"<モジュール名>!<関数名>\")=0 { r $t1=1 }}; .if ($t1=0) { gc }"
長いので折り返します。実際には 1 行で書く必要があります。
bp <停止アドレス>
"r $t1=0;
.foreach /pS4 /ps1 (v {kc}) {
.if $scmp(\"${v}\", \"<モジュール名>!<関数名>\")=0 {
r $t1=1
}
};
.if ($t1=0) { gc }"
日本語に翻訳 (?) すると、「ブレークポイント (bp) を <停止アドレス> に仕掛ける。ただし停止時、以下のコマンドを実行する。(1) 疑似レジスタ $t1 に、0 を代入する。(2) コールスタックの情報を出力 (kc) し、最初の 4 語を除いた (/pS4) 後、1 語おき (/ps1) に “<モジュール名>!<関数名>” という文字列を探し ($scmp()=0)、見つかったら疑似レジスタ $t1 に 1 を代入する。(3) 疑似レジスタ $t1 の値が 0 だったら実行を再開し、そうでなかったら停止したままとする。」となります。(※ $scmp()=0 ではなく $spat() を使うのもよい。)
たとえば、「test モジュールの関数 B」を経由して「test モジュールの関数 common」に到達した場合にのみ停止させるには、次のように書きます。
bp test!common
"r $t1=0;
.foreach /pS4 /ps1 (v {kc}) {
.if $scmp(\"${v}\", \"test!B\")=0 {
r $t1=1
}
};
.if ($t1=0) { gc }"
先のプログラムで実際に試してみましょう。
WinDbg を起動し、メニューの [File]-[Open Executable] から「test.exe」を開きます。
そして、[Command] ウィンドウから目的のブレークポイントを仕掛けます。
0:000> bp test!common "r $t1=0; .foreach /pS4 /ps1 (v {kc}) { .if $scmp(\"${v}\", \"test!B\")=0 { r $t1=1 }}; .if ($t1=0) { gc }"
実行を継続します。
0:000> g
ブレークポイントにヒットしました。
test!common:
00007ff6`67db7190 4883ec28 sub rsp,28h
プログラムの実行画面には次のように表示されています。
① main -> common
② main -> A -> common
③ main -> B ->
ルート ① とルート ② では止まらず、ルート ③ の common に入ったところで止まっています。
kc コマンドでコールスタックを見ると、test!B から test!common が呼び出されていることが確認できます。
0:000> kc
# Call Site
00 test!common
01 test!B
02 test!main
03 test!invoke_main
04 test!__scrt_common_main_seh
05 KERNEL32!BaseThreadInitThunk
06 ntdll!RtlUserThreadStart
実行を継続します。
0:000> g
再びブレークポイントにヒットしました。
test!common:
00007ff6`67db7190 4883ec28 sub rsp,28h
プログラムの実行画面には次のように表示されています。
① main -> common
② main -> A -> common
③ main -> B -> common
④ main -> B -> C ->
ルート ④ の common に入ったところで止まっています。
kc コマンドでコールスタックを見ると、test!B を経由して test!common に到達していることが確認できます。
0:000> kc
# Call Site
00 test!common
01 test!C
02 test!B
03 test!main
04 test!invoke_main
05 test!__scrt_common_main_seh
06 KERNEL32!BaseThreadInitThunk
07 ntdll!RtlUserThreadStart
以上のように、想定通りの条件で停止しました。
なお、kc コマンドの出力から最初の 4 語を除いた (/pS4) のは、「#」「Call」「Site」「00」を検索対象外とするためです。その後、1 語おき (/ps1) に、「test!common」「test!C」… と検索していきます。
おわりに
条件付きブレークポイントで「ある関数を経由してきたか」をチェックする方法について見てきました。あらゆるパターンに対応可能なのか、この方式がベストなのかは分かりませんが、試した範囲ではうまく動作しています。デバッグ時の助けになればと思います。
参考文献
[1] マイクロソフト「デバッガー オブジェクトでの LINQ の使用」
https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/using-linq-with-the-debugger-objects