[Debugging] Windbg を使ってご機嫌ナナメな彼女の心を激しくデバッグ!(3) /3 ~ デバッガでブレーク ポイントを設定するときの Tips ~

皆様ごきげんよう、ごきげんよう。ういこです。

 

最近しばれる日が続いてるんですがあいかわらず病んでいます。そしてどうも楽しいことがなくて、もにょりもにょりとしております。

今日は RSS 読者様には絶賛不人気の [Windbg] のねたつづきです。お得意様に喜んでもらえるコンテンツは明日あたりお父さんが書いてくれると思います。Technet なのに、空気読めてなくてごめんなさい

でもこれで不機嫌な彼女もひいひい言わすことができますよ!たぶん。

過去の連載は以下!

[Debugging] Windbg を使ってご機嫌ナナメな彼女の心を激しくデバッグ!(1)
https://blogs.technet.com/jpilmblg/archive/2009/02/21/debugging-windbg-1.aspx

[Debugging] Windbg を使ってご機嫌ナナメな彼女の心を激しくデバッグ!(2) Ver 1.1
https://blogs.technet.com/jpilmblg/archive/2009/02/25/debugging-windbg-2.aspx

[Debugging] Windbg を使ってご機嫌ナナメな彼女の心を激しくデバッグ!(3) ★ ← 今回
https://blogs.technet.com/jpilmblg/archive/2009/03/06/debugging-windbg-3-tips.aspx 

 

【今日のお題】

デバッガでブレークポイントを設定するときの Tips

Windbg でブレーク ポイントを設定する際に、ただブレークを指定するだけではなくて、こんなことってあると思います。

・ある関数が呼ばれてるはずだということを確認したいが、すごい回数呼ばれるので、呼ばれたことだけ確認して自動的に続行させたい

・呼ばれたのを確認したうえで、コールスタックなどを表示させた後続行させたい

・特定のレジスタ、アドレスの値(たとえば、if 文の分岐の際のフラグなど)によって、ブレーク後にしたい処理が異なる

など。

こうした場合、ただ bp / ba などのブレーク ポイントを設定するコマンドだけ実行しても、毎回とまるたびに手でしたい処理を確認したりするのって面倒ですよね。

たとえば、10000 回ループするプログラムがあるとします。ループ内で問題が起こってるとかの調査で、10000 回もブレークポイントで止まるたびに手で処理続行していくなんてリアルに死にます

前に同僚様に教えていただいた方法ですが、とっても重宝してる(同僚様ありがとう)こんなときに便利なブレークのさせ方をひとつ。

Case 1 : 特定の関数などでブレーク後、無条件に何か処理をして続行させたい

たとえば、adsldp!CLDAPNamespace::OpenDSObject() API で止まったら、コールスタックを出して実行したいとしましょう。

bp adsldp!CLDAPNamespace::OpenDSObject "!runaway;kvn;.echo OpenDSObject Yes;g"

これを実行するとどうなるか。

1) adsldp!CLDAPNamespace::OpenDSObject が呼ばれた場合、以下の処理がされる

 2) スレッドの実行時間を表示する ( !runaway)

 3) スタック トレースをフレーム番号つきで表示させる (kvn)

 4) "OpenDSObject Yes" という文言を激しく出力

 5) 処理続行される (g)

Case 2 : ブレークした後、条件によって処理を分岐させたい

分岐する際は、j コマンドを使用します。

 形式 : j 条件文 ' 実行したいコマンド1' ; ' 実行したいコマンド2'

 ※ 実際の関数でとめるときは、<モジュール名>!<関数名> の後、j コマンドの先頭から実行したいコマンド 2 までをダブルクオートでくくってください。

たとえば、2 つの引数を持つ MyDll.dll の関数 MyFunc() をデバッグしていて、以下の条件のときにだけブレークさせたいとします。

a. MyFunc() の第 2 引数が null の場合にブレークさせたい

b. 戻り値が 1 のときにブレークさせたい

<< a. の場合 >>

まず、関数が実行された直後、スタックの状態はこんなことになっています。

・esp レジスタ … リターン アドレス

・esp + 0x4  … 関数の第 1 引数

・esp + 0x8  … 関数の第 2 引数

これを確認して、今回は第 2 引数を見たいので、esp + 0x8 を見てあげればよいことになります。

bp myDll!MyFunc "j poi(esp+0x8)==0 '.echo 2nd arg is NULL';'g'"

j poi(esp+0x8)==0 で MyFunc() の第 2 引数が NULL かどうかを確認しています。

'.echo 2nd arg is NULL' は  poi(esp+0x8) が NULL (ここの 0 がそう) という条件文が TRUE のときに実行されます。

.echo で文字を出力します。今回の場合、2nd arg is NULL と表示します。

poi(esp+0x8) が NULL (ここの 0 がそう) では無い場合 (FALSE)、g コマンドが実行され、処理が継続されます。

<< b. の場合 >>

さて、ブレークポイントを指定したいとき、そもそもブレークさせたい関数が呼び出される前に指定するブレークポイントで、戻り値によって処理分岐ってどういうこと? って思いますよね。

こうしたときは、関数の戻り値のアドレス(リターンアドレス)にブレークポイントを指定してやれば OK です。戻り値は eax レジスタに格納されます。

この eax レジスタの内容を評価し、j コマンドで処理分岐させれば良いのです。…って、どうするのかって?

こうすればよいのです。

 bp myDll!MyFunc "bp poi(esp) \"j (eax==1) '.echo ret is 1';'r eax;g'\";g"

bp poi(esp) で、リターン アドレスにブレークポイントを設定します。

j(eax==1) で戻り値が 1 かどうかを判定しています。

eax が 1 のとき ( "j(eax==1)"TRUE のとき) .echo コマンドで "ret is 1" とコメントがデバッガ上に出力されます。

eax が 1 のとき ( "j(eax==1)"FALSE のとき) eax レジスタの内容を出力し (r コマンド + 表示対象レジスタ指定なので、今回は eax レジスタなので r eax になるですねぇ) 表示して、g コマンドで処理続行します。

ちなみに、このままだとリターン アドレスに対するブレークポイントがクリアされずに残ってしまうので、それが気になる人は bc コマンド + < ブレークポイントの ID> で消すことができるのを応用して、設定と同時に消すなんてこともできます。え、ブレークポイントの ID どうするかって?設定時に番号指定しちゃうんですよ。

以下は ブレークポイントを ID 1 番に指定して (bp 1) 、指定した ID 1 番を消す (bc 1) という荒業です。

 bp myDll!MyFunc "bp 1 poi(esp) \"bc 1;j (eax==1) '.echo ret is 1';'r eax;g'\";g"

設定したブレークポイントのリストを見るには以下のコマンドを実行します。

bl

ポイント : 記述の際の注意

ダブルクオートをエスケープするために \ で内容をはさむことに注意してください\ を除くとダブルクオートを記号として扱わないからです。

\"j (eax==6) '.echo IDYES';'reax;g'\" を "j (eax==6) '.echo IDYES';'reax;g'" と、 \ 記号を除いて指定することはできません

Case 3 : 特定のモジュールがロードされたらブレークしたい。シングルステップ例外はうざいからシカトしたい

さて、特定のモジュールがロードされるかだけ知りたいときとかあります。

こういうとき、モジュールがロードされたらブレークするということを設定することができます。

sxe ld adsldp.dll

上記の例では、adsldp.dll が読まれたタイミングでデバッガがブレークするという設定です。

sxe = Set Exception Enabled

Windbg の便利なところのひとつとして、イベント フィルタリングという機能?があります。これが超便利!

デバッグをしていると、対象のモジュールによっては大量に発生するシングル ステップの例外を無視したいなあと思うときがありますよね。これをフィルタできちゃうのです。

sx コマンドのお友達、sxd = Set Exception Disabled を使うと、例外の発生時に無効化することができます。sse は、勘のいいアナタならピンとこられたかとおもいますが、Single Step Exception です。

sxd -c "gn" sse

上記は、sxd コマンドに引数として -c のあとに gn とありますが、これは "Go with Exception Not Handled" という意味です。そのまま処理継続させることができます。

ちなみにシングル ステップ実行を再度有効化したいときは逆に enable にしてやりましょう。(sxe コマンド)

sxe -c "" sse

そして、sx コマンドで設定したブレークすべきポイントは、現在設定されているブレークポイントのリストを表示させるコマンド (bl) でも出てこないです。

この場合は、単純に sx と実行してやれば、設定がわかります。

*******

いかがでしょうか。

とりあえずこれで、ブレークポイントがかなり使えると思います。

私はこれだけ押さえてれば、ヘビーなデバッグはともかく、ちょっとしたデバッグならばかなり困らなくなると思います。実際あんまり私困ってないですし。

ちなみに困ったらどうするかって?そりゃ、神様(もしくはデバッグセレブ様がた)に伺うんですよ…。知ってる人に聞くのが一番!! いなかったら別のアプローチを考えます。

それではまた!

 

~ ういこう@RSS 読者様に愛されるコンテンツって遠いなぁ ~