ASP.NET 運用デバッグ入門 3) メモ帳をデバッグ

こんにちは、d99 です。

さて、前回の連載エントリでデバッガ(windbg.exe)の動作確認まで行いました。予告通り、引き続いてメモ帳をデバッグしてみましょう。

- 前提条件

1) Debugging Tools for Windows がインストールされている

インストール方法については [以前のエントリ] を参照してください。

2) インターネットにアクセスできる

今回はシンボルを使いますが、それをインターネット経由でダウンロードします。ですので、デバッグするマシンはインターネット(HTTP)にアクセスできる状態にしておいてください。

3) 二つのファイルを用意

テキストファイルを二つ用意します。C ドライブのルートに test.txt (C:\test.txt)、D ドライブのルートにも test.txt (D:\test.txt)を用意しましょう。中身はそれぞれのファイルのフルパスを書いて、区別が付くようにしてください。なお、1 ドライブ環境であれば、どこかのフォルダを共有して、それをネットワークドライブで適当なドライブ(例えば Y: とか) に割り当てておくといいと思います。その場合は以降の D: をそのドライブ名に読み替えてください。

- デバッグ手順

1) メモ帳を起動します。

2) デバッガを起動し、アタッチします。プログラムメニューから、[Debugging Tools for Windows] - [windbg] を起動します。[File] メニューから [Attach to a Process] を選択し、出てきたダイアログで、[notepad.exe] を選び、[OK] を押します。

3) [Save information for workspace] といったダイアログが出る場合がありますが、[No] で構いません。

4) デバッガがプロセスにアタッチされます。ブレークしますので、[GO] します。前回解説したように、[Command] 子ウィンドウを最大化してから 0:0nn>(n は任意の数字)と出ているプロンプトに g と入力して Enter を押下しましょう。

5) メモ帳を操作します。[ファイル] - [開く] で c:test.txt を選びますが、[開く] ボタンはまだ押しません。

さて、今回どのようにメモ帳を改造するかというと、「C ドライブのファイルを開こうとしても D ドライブのファイルが開かれる」 という状態を目指します。

そもそも論になってしまうのですが、アプリケーションとはシステムコールをしながら動作しています。つまり、非常におおざっぱに言うと、アプリケーションが直接 HDD のドライバやコントローラーに命令してファイルを開いているのではなく、OS に対して 「オイ、このファイルくれよ」 とシステムコール = Windows API を呼び、その結果ファイルを受け取っています。これは Windows 上のアプリケーションであれば、どんな言語で書いても結局同じです。ですので 「ファイルを開く」 という時に使われる Windows API を知っておけば、そのアプリケーションが C で書かれていようが、VB で書かれていようが、C# で書かれていようがそのあたりをデバッグできる事になります。

そこで今回着目するのは、「CreateFile」 という API です。詳細は下記リファレンスを見ていただくとして、要はファイルを開く時にそのファイルハンドルを得るための API です。イマイチよくわからなければ 「ファイルを開く時はこの API が呼ばれるんだな」 くらいの認識で構いません。

プラットフォーム SDK - CreateFile 関数
https://msdn.microsoft.com/ja-jp/library/cc429198.aspx

デバッガでメモ帳がこの API を呼ぶところをひっつかまえ、この第一引数のファイル名をちょちょいと書き換えてやろう、というのが今回の目標になります。

6) デバッガをブレークします。上部にある ポーズ マークのボタンをクリックしてもいいですし、Ctrl+Break キーを押下してもかまいません。

7) まずはシンボルを設定します。シンボルはこれもまた大雑把に言うと :-)、バイナリファイルの内部情報を記したファイルです。今回の作業では実際には不要ですが、今後のためにここでやっておきましょう。デバッガで [File] - [Symbol File Path] を選び、出てきたウィンドウに以下を入力し、[Reload] にチェックを入れて、[OK] を押します。この操作によって、Windows 関連等のシンボルが自動的にインターネットからダウンロードされます。毎回ダウンロードしなくて済むように、C:\Windows\Symbols にキャッシュされます。もし C ドライブの空き容量が少ない場合は適宜変更してください。

SRV*c:\windows\symbols*https://msdl.microsoft.com/download/symbols

8) 次に、先ほど g と入力した [Command] 子ウィンドウにて以下のコマンドを入力します。特にエラーが出なければ問題ありません。CreateFile じゃなくて CreateFileW なの?という疑問を持たれた方もいらっしゃるかと思いますが、Unicode 版と ANSI 版があって、Unicode 版が W が付いてる、という大雑把な理解で構いません。

 bp kernel32!CreateFileW

9) g コマンドを入力し、メモ帳を動かします。先ほど開いていたダイアログで [開く] を押してください。

10) すると、デバッガがブレークします。つまり CreateFileW で止まった、という事です。メモ帳はデバッガでブレークされるので、画面描画が更新されなくなります。"Breakpoiint 0 Hit" と出て、kernel32!CreateFileW で止まった事が示されます。引数を見てみましょう。kvn とコマンドを入力してください。以下のようなスタックバックトレースが表示されます。

 0:000> kvn
 # ChildEBP RetAddr  Args to Child              
00 0007fb14 01002dd0 0007fb98 80000000 00000003 kernel32!CreateFileW (FPO: [7,23,0]) 
01 0007fda4 01003947 00050126 00000002 00000000 notepad!NPCommand+0x229 (FPO: [3,152,4])
02 0007fdc8 77eab6e3 00050126 00000111 00000002 notepad!NPWndProc+0x4fe (FPO: [4,2,0])
03 0007fdf4 77eab874 01003449 00050126 00000111 USER32!InternalCallWinProc+0x28
04 0007fe6c 77eaba92 00000000 01003449 00050126 USER32!UserCallWinProcCheckWow+0x151 (FPO: [SEH])
05 0007fed4 77eabad0 0007fefc 00000000 0007ff1c USER32!DispatchMessageWorker+0x327 (FPO: [SEH])
06 0007fee4 01002a32 0007fefc 00000000 ffffffff USER32!DispatchMessageW+0xf (FPO: [1,0,0])
07 0007ff1c 01007527 01000000 00000000 000a24b2 notepad!WinMain+0xdc (FPO: [4,8,0])
08 0007ffc0 7c82f23b 00000000 00000000 7ffdf000 notepad!WinMainCRTStartup+0x182 (FPO: [SEH])
09 0007fff0 00000000 010073a5 00000000 78746341 kernel32!BaseProcessStart+0x23 (FPO: [SEH])

これがブレークした際の、スタックバックトレース(呼び出し履歴)です。下から上に呼び出しが行われています。一番下が BaseProcessStart でプロセスが始まってる感じ(w、ですね。そして色々あってから notepad から CreateFileW API が呼び出されて、ブレークという感じです。まだ今は 「へー」 くらいで構いません。また、引数は [Args to Child] というところに出ています。先ほどの CreateFile API のリファレンスを見ると、第一引数がファイル名らしいので、上記で言えば [0007fb98] に Unicode の文字列が入っているはずです。中身を見てみましょう。

11) du [第一引数のアドレス] と入力します(Unicode 文字列を表示するコマンドです)。C ドライブの test.txt を開こうとしています。しめしめ

 0:000> du 0007fb98 
0007fb98  "C:\test.txt"

12) おもむろにこの文字列冒頭を書き換えてしまいましょう。eu [書き換えるアドレス] [文字列] と入力します。NullTerminate する必要はありません。

 0:000> eu 0007fb98 "D:"

13) 一応先ほどのコマンドで、書き換わっている事を確認します。確認できたら g します。

 0:000> du 0007fb98 
0007fb98  "D:\test.txt"

0:000> g

いかがでしょうか?C ドライブのテキストファイルを開こうとしたのに、D ドライブのファイルが開かれていませんか?当チームでも、新人さんにデバッガの使い方を教える時は 「まずメモ帳を開いてみろ」 から始まる事が多いので、あんまり ASP.NET とは関係ないのですが、紹介してみました。ついでにオマケをつけておきます。

14) 一旦デバッガでブレークして、以下のコマンドを入力します。同じブレークポイントなので redefine = 再定義されます。bp = ブレークポイントを設定するコマンドには 「ブレークした時にどんなコマンドを実行するか」 を引数で設定できます。CreateFileW で止まるたびにメモリを書き換えているので、このコマンドを打った後はずっとメモ帳は 「C ドライブのファイルを開こうとしても D ドライブのファイルが開かれてしまう」 という状態になります。

 0:001> bp kernel32!CreateFileW "eu eax \"D:\";g"
breakpoint 0 redefined

eax は何かって?レジスタ、というとお分りいただける方もいらっしゃるかと思いますが、ピンとこない方は r というコマンドの結果と、先ほどの kvn の結果をよく見比べてみてください。同じアドレスが出ているところがありませんか?正確に説明すると呼び出し規約とかコマカイところになってしまうので、今はこれも 「へー」 でも構いません。また、実行ファイル自体を書き換えているわけではないので、デバッガとメモ帳を終了してしまえば元通りです。ご安心を。

- まとめ

ざっと windbg の使い方というか、使っている風景、をご覧いただけたかなと思います。実際にはライブデバッグ(プロセスが動いている状態) なら、Visual Studio 使った方が速いのですが、まぁ windbg 君の本領発揮はまだまだこれから、という事でご期待いただければ、と。

なお 「あれ?これって言わばメモ帳を改造しちゃってるんじゃね?」 と思われた方もいらっしゃるかと思います。そうですね、改造というとおこがましいですが、そんな感じかもしれません。そのため、同じように 「プロセスにデバッガをアタッチして××するとこんな事が!」 というネタは結構紹介されていたりします。今回はスペースの都合上詳しい手順はご紹介しませんが、もし興味のある方は下記リンクのマインスイーパーをデバッグしちゃうのなんか、面白いと思いますよ :-)

[Windbg Script] Playing with Minesweeper
https://blogs.msdn.com/debuggingtoolbox/archive/2007/03/28/windbg-script-playing-with-minesweeper.aspx

- 次回予告

次は、いよいよ ASP.NET でもうすこし実践的な事例を取り上げます。そして、なぜ Visual Studio ではなくて windbg が ASP.NET の役にたつのか、をご紹介する予定です。

ではまた。
d99 でした。