Win32デバッグ(12)・・・SEH(Structured Exception Handling)

まぁ、どうしようか悩んだが、とりあえず、進める所まで進んでみますsad

ということで今回のお題目は例外。結論から言うとDelphiではtry-except文による例外処理にしろ、try-finally文の終了処理にしろ、WindowsのSEH(Structured Exception Handling)を使って実装されているのだが、そのSEHについて。

SEHはWindowsによって提供されている構造化例外処理のためのメカニズムのことであるが、その流れについて説明する。

まず、0による除算や不正なメモリへのアクセスなどのハードウェア例外にしろ、Win32 APIのRaiseException関数によるソフトウェア例外にしろ、スレッド内で例外が発生するとOSに制御が移る。そして、OSは例外が発生したスレッドのCPUレジスタなどのコンテキスト情報の保存など必要な処理を行った後、例外をハンドルするための例外ハンドラを検索し、呼び出す。

具体的には、まず、OSは例外が発生したスレッドのスレッド環境ブロック(TEB:Thread Environment Block)またはスレッド情報ブロック(TIB:Thread Information Block)と呼ばれる領域の先頭4バイト(at FS:[0])の値を取得する。この4バイトの値は例外ハンドラのリンクリストの先頭をポイントしているので、この値から先頭の例外ハンドラから順に呼び出していくのである(つまり、ここに例外ハンドラを登録すれば例外発生時に例外ハンドラが呼び出されるようになるのだが、通常は、Delphiにしろ、C++にしろ、言語提供の例外処理などを利用すれば、コンパイラによって登録するコードが挿入されるので、普通は自前で登録しないが・・)。

リンクリストのノードは次のようなEXCEPTION_REGISTRATION構造体になっている。

prevメンバは次のノードへのアドレス、handlerメンバーには例外ハンドラのアドレスつまりOSから呼び出されるコールバック関数のアドレスを表す。呼び出されるコールバック関数のプロトタイプは次のようになる。

第1引数はEXCEPTION_RECORD構造体のアドレスである。EXCEPTION_RECORD構造体は例外の種類を表すコードや例外発生時のアドレスなどの発生した例外に関する様々な情報が格納されているので、例外ハンドラはこれらの情報を見て例外を処理するかを決定する。Delphiのtry-except文では例外オブジェクトのクラスとexceptのon句に指定したクラス(on E: ExceptionClass doのExceptionClassの部分)を比較して判定している。

呼び出された例外ハンドラで例外を処理せず、OSに次の例外ハンドラを呼び出させる場合はExceptionSearchException(1)を返して例外ハンドラからOSにリターンする。

また、例外が発生したアドレスから例外の原因を修正(0による除算が発生した場合、除数を0以外の値に修正)するなどして、再実行する場合はExceptionContinueExecute(0)を返してOSにリターンする。Delphiではこれは行われない。

例外ハンドラで例外を処理する場合は、例外ハンドラからOSにリターンせず、通常は、Win32APIのRtlUnwindなどでアンワインド(巻き戻し)したり、スタックフレームを適切に設定し直して、処理を続行する。Delphiではexceptのon句の後に指定した処理から実行が再開される。

RtlUnwindによるアンワインドはMSDNのドキュメントみても何の事だかさっぱり理解できないと思うが、簡単に言うと、例外ハンドラのリンクリストから不必要になった例外ハンドラのノードを削除することである。また、このとき、例外ハンドラの2回目の呼び出しが行われる。この2回目の呼び出しのときに、通常は終了処理を行う。Delphiではtry-finally文のfinally句の後に指定した終了処理がこの2回目の呼び出しのときに、実行される。

とまぁ、おおざっぱに書くとこんなところであろうが、詳細は

すべて、英語で書かれているが、特に一番上の記事にはSEHについて分かりやすく、詳細に書かれているのでお勧めです。自分は頑張って読みましたdash

| | コメント (0) | トラックバック (0)

Win32デバッグ(11)・・・番外編

今回は寄り道。というより、前回までに示したコードに少し問題があった。

第1回で

  • ReadProcessMemory
  • WriteProcessMemory

を使えば、プロセスハンドルからそのプロセスのアドレス空間を読み書きできるし、また、

  • GetThreadContext
  • SetThreadContext

を使えば、スレッドハンドルからそのスレッドのコンテキスト(レジスタの値)を読み書きできる。

と書いたが、大切な事が抜けていたshock

ということで、例えば、あるスレッドが次に実行する命令を読み込むプログラムを上記のAPIを使って書いてみる。x86アキーテクチャではインストラクションポインタ(EIP)レジスタが次に実行される命令をポイントするので、例えば、次のようになる(繰り返すが、対象スレッドは停止してないとまずいと思う)。

x86命令は可変長の命令で何バイト読みこめばよいか?という問題があるが、上記のプログラムではとりあえず4バイト読み込んでいる。上記のプログラムを実際に動かすと正しく動いているように見えるが、問題があるのである。

何が問題かと言うと、ReadProcessMemory/WriteProcessMemoryの第2引数には読み書きするメモリの仮想アドレスを渡すのであり、上記のプログラムでは、EIPレジスタの値を渡しているのであるが、EIPレジスタの値は仮想アドレスの値ではないのである。EIPレジスタに格納されているのは確かにアドレスであるが、CSレジスタに格納されているセグメントセレクタによってポイントされるセグメント(コードセグメント)内の相対アドレス(segment-relative address)なのである。

ということで、仮想アドレスに変換する必要があるのであるが、そのために使うAPIがGetThreadSelectorEntryである。GetThreadSelectorEntryによって引数で指定したセグメントの仮想アドレス空間内のセグメントのベースアドレスが求まるので、それを使って正しく先ほどのプログラムを書き換えると次のようになる。

と、肝心な事を忘れたdash

ちなみに、最初に示したプログラムが動いてしまうのは、どうも、NT環境ではコードセグメントのベースアドレスが常に0になるからのようである。特にESPレジスタによってポイントされるスタックトップを読み書きするような場合、スレッド毎に異なるスタックセグメント(SSレジスタ)が割り当てられるので、なお更まずい。

| | コメント (0) | トラックバック (0)

Win32デバッグ(10)・・・泣

このネタも10回目に突入してしまったbearing

早速、前回やり残した事をやる。前回で逆アセンブラスクラスを設計したので、今回は問題となっていたデバッグ情報からルーチンの終了位置のアドレスを求める部分を逆アセンブルして求めるよう次のように書き換える。

で、再びドライバプログラムを実行してターゲットプログラムをプロファイルしてみたところ、ターゲットプログラムが正常に動作したhappy02。プロファイルしたターゲットプログラムはコンソールアプリで次の通り。

意味のないプログラムであるが、関数Calcが10回呼び出され、その結果がコンソールに表示されるだけである。また、関数Calcからは関数SumとMulが呼び出される。このターゲットプログラムのコールツリーは次のようになった。

Win32debug_10_1_3

上の画面から確かに関数Calcが呼び出され、関数Calcから関数SumとMulが呼び出されているのが分かるだが、呼び出された回数つまりヒットカウントが10ではなく1になってるのである・・・全部合計すると10回なのであるが、ノードが分かれている・・・

がーんsad

で、デバッグして原因を調べたところ、肝心な事をすっかり忘れていたsad。それは、最近の言語でたいていはサポートされている例外処理・終了処理である。Delphiでいうところのtry-except,try-finally文である。この実現のためにコンパイラが生成してるコードに対応できていなかったのである。つまり、今までの設計では、ルーチンは必ずCALL命令で呼び出されRET命令でリータンする、また、RET命令は必ずルーチン内で1箇所だけ(現状、1つのルーチンに対して開始位置と終了位置の2つだけのブレークポイントを作成してるため)という仕様?前提?が甘かったのである・・・(JMP命令なども無視してるし・・・)

はぁ。

どうしよう??

と、本来はすべてが上手くいき今回で最終回にする予定であったのだが。どうしよう。

また、ドライバプログラムも含めて動作する形でソースをアップロードする予定であったのだが、どうせおかしなコールグラフが表示されるのでやめておく。

と言う事で次回に続くかは本気で分からない・・ははは。

| | コメント (0) | トラックバック (0)

Win32デバッグ(9)・・・逆アセンブラ

前回で、どうもx86命令を逆アセンブルする必要が出てきたので今回は逆アセンブルについて。

まずは、逆アセンブラを自作する方向で進めていたのであるが、ご存知のようにx86命令は可変長であったり、SSE命令などの拡張命令がどんどん追加されていったり、面倒そうなので、とりあえず、既存の逆アセンブラライブラリを利用する事にした。

ということで既存の逆アセンブラライブラリをいくつか挙げてみる。

逆アセンブラライブラリ
名前 サイト ライセンス
BeaEngine http://beatrix2004.free.fr/ LGPL
diStorm64 http://www.ragestorm.net/distorm/ BSD
Hacker Disassembler Engine(HDE) http://patkov-site.narod.ru/ Free
libdisasm http://bastard.sourceforge.net/libdisasm.html Free/Open Source
SysDasm http://rootkit.com/newsread.php?newsid=208 Free/Open Source
Udis86 http://udis86.sourceforge.net/ Free/Open Source
VirtualBox Disassembler Library http://www.woodmann.com/forum/showthread.php?t=11904 ?

各逆アセンブラライブラリの詳細については、各ライブラリのサイトを参照。ここではどのライブラリを使うかだが、ここでは上記の1つであるHacker Disassembler Engine(HDE)を使うことにした。理由は最終更新日が最近(2009年3月)であるのと、軽量である点だ。他のライブラリは結構高機能で逆アセンブルした機械語をアセンブラのニーモニックのテキスト形式に変換できたりするが、今回はx86命令のRET命令を検出できればよいので、命令長やオプコードなど最小限の情報だけを取得できるHDEにした(ちなみに、上記BeaEngineのサイトにLength Disassemblerというものもあるようだ・・)。

次に、新しいクラスを設計するが、その前にx86命令の大まかな命令フォーマットを知らないと設計できないので、x86命令の命令フォーマットを少し。x86命令の命令フォーマットは次のようになる。

X86instformat_2

先程も述べたように可変長なのであるが、各フィールドの詳細はIntelのマニュアルを参照ということで。

まずは、抽象クラスで逆アセンブルを行うTDisassemblerクラス。

各プロパティは先ほどのx86命令フォーマットの各フィールドに対応する。そのうちのいくつかのプロパティの型がVariant型なのは、対応するフィールドが必ずしも存在するとは限らないからである。DoNextメソッドは現在位置からx86命令を解析し、次に命令の先頭にポインタを進める抽象メソッドであるので、下位クラスではDoNextメソッドをオーバーライドしなければいけない、また、解析が成功したらTrueを返す。

そして、TDisassemblerクラスを継承し、HDEを使った逆アセンブラクラスTHdeDisassemblerのDoNextメソッドを次のように実装する。

ここは、使用する各逆アセンブラライブラリに依存するので詳しくは省略・・

後、一息なのであるが、今回はここまでbearing

最後に、HDEはC言語のライブラリなので、Delphiで利用するためには、ヘッダを移植し、ライブラリ自体はCコンパイラでコンパイルし、Delphiの$Lコンパイラ指令を使ってリンクすればよいはず。

| | コメント (0) | トラックバック (0)

Win32デバッグ(8)・・・そして

今回は、前回までで必要なクラスの設計そして実装ができたので、約束通りドライバを作成して動かしてみる。

で、ドライバとTD32デバッグ情報付きの簡単なターゲットプログラムを用意して動かしてみたのだが・・・

ターゲットプログラムの動きがおかしい・・・weep

動的コールグラフを求めるにあたって、前回までに述べた通り、ブレークポイントを設定するためターゲットプログラムのプロセスのメモリ空間のコード領域を書き換えてるのだがどうもそこらへんの書き換えがおかしいっぽい・・・

で、調べる事数時間・・・

予期せぬ原因が分かったweep。やはり、ブレークポイントを設定する位置のアドレスの計算に問題があった・・・。再掲載するが現在は

のように、ルーチンの呼び出しを検出するために、ルーチンの開始位置と終了位置のアドレスをデバッグ情報から取得できるセクションインデックスと開始位置のオフセット・長さから求め、開始位置のアドレスの計算は合っているようなのだが、終了位置のアドレスの計算が間違っているようなのである。現在、終了位置のアドレスは上の25行目のように、

  終了位置のアドレス=開始位置のアドレス+ルーチンの長さ

で求めているのであるが、この式によって求めた終了位置のアドレスがルーチンによって想定外の位置を指してたりと・・・。要するに、上の式によって求めたルーチンの終了位置のアドレスは必ずx86命令のRET命令の先頭を指していると思っていたのだが、甘かった・・

そして、対応策を色々考えたのだが、どうも、まさかここまでする必要はないと思ってこのネタをやりだしたのだが、開始位置のアドレスは正しいようなので、開始位置のアドレスから自前で逆アセンブルして、x86命令のRET命令の位置を求めることに・・・(まぁ、必ずしもすべてのコンパイラがルーチンの呼び出し・リターンにx86命令のCALL・RET命令を使ったコードを生成するとは限らない?が、まぁ、そこらへんはよしとしよう・・)。

続く・・・

| | コメント (0) | トラックバック (0)

Win32デバッグ(7)・・・動的コールグラフ(クラス設計の続き)

今回は残りの必要なクラスをさっさと設計してまおうbearing。また、長くなりそうだが・・・

まずは、要となるクラス。動的グラフとはつまるところルーチンの呼び出し関係を表すグラフ理論?で言うところ有向グラフなのであるが、そのルーチンの呼び出しを表すノードを表すTCallGraphNodeクラスを次のように定義する。

Callerプロパティはこのノードが表すルーチンを呼び出したルーチンのノードをあらわす。

Calleesプロパティは、次に出てくるTCallGraphNodeListクラスのプロパティであるが、これはこのルーチンから呼び出されたルーチンのノードリストを表す。あるルーチンから同じルーチンが複数回呼び出される時、呼び出し毎にノードを作成しても良いがメモリ効率が悪そうなのでここでは1つにまとめることにする。

HitCountプロパティはこのノードが表すルーチンが呼び出された回数を表す(もちろん、前述のCallerプロパティのノードが表すルーチンによって呼び出された回数である)。

Symbolプロパティは呼び出されたルーチンを表す。

次に、TCallGraphNodeのリストを保持するTCallGraphNodeListクラス。

ここでは、1点だけ、先程、あるルーチン内から同じルーチンへの直接の呼び出しは1つにまとめると書いたが、そのため、このリストは同じシンボルを参照する複数のノードを追加しようとすると例外が発生するようにしてある。シンボルの等価性の判定には、ここでは単純にTSymbolクラスのインスタンスの参照を比較してるがまぁ、これで問題ないだろう・・

ふぅ。コーヒーブレイクcafe

次に、ここで定義したクラスをどう使うかであるが、対象プロセスがマルチスレッドアプリケーションの場合、その動的コールグラフってどうなるんだ??って事の解を探る気力が全くないbearing(最近、思考するのが疲れたので息抜きでブログ書いてるのであるが・・・)ので、ここでは、スレッド毎にコールグラフを作成する事にするのでTThreadクラスにいくつか修正を加えてみた。

CallGraphプロパティは動的コールグラフを表すTCallGraphクラスのプロパティである。初出のクラスであるが、これは動的コールグラフの先頭のヘッドノードのリストを管理するクラスである。なぜ、リストを管理する必要があるかと言うと、少し考えれば分かるのだが、ルーチンの呼び出し階層に現われるルーチンにすべてに対するシンボル情報が存在もしくては提供されるとは限らないので、動的コールグラフが切れる可能性があるためである。クラスの定義はここでは省略。

CallStackプロパティはコールスタックを表すスタックである。動的コールグラフを成長させる時、内部で一時的に使用する。ルーチンの実行の開始の検出時には、そのルーチンのノードをスタックにプッシュ、ルーチンの実行の終了の検出時には、スタックからノードをポップする。このようにして、現在実行されているルーチンを追跡する(スタックトップのノードのルーチンが現在実行されているルーチンになる)。まぁ、実際、スタックなんて大袈裟なものを容易しなくても、現在実行されているルーチンのノードを表す変数1個用意すればすむのだが・・・

いよいよ、終盤であるが。以上を踏まえて、実際に動的コールグラフを成長させるプログラムは例えば次のようになる。

以外にシンプルだった。以上。

次回以降は、実際に今まで定義したクラスを使うドライバプログラムを作成して実際に動かしてみようと思う。

ここまでのプログラムはいつものようにSkyDriveから。

| | コメント (0) | トラックバック (3)

Win32デバッグ(6)・・・動的コールグラフ(クラス設計)

今回はWin32デバッグの続きで、新たなクラスを設計してみるgood

まずは、動的コールグラフにおいて、シンボルを表すクラス。

Sectionプロパティはセクションインデックス、StartOffset・Lengthプロパティはシンボルというよりここでは関数や手続きもしくはメソッドつまりルーチンのセクション内での開始位置のオフセット・長さを表す。このクラスはDelphiの言語上厳密に、抽象クラスではないが、抽象クラス的な使い方をする。理由は後述。

お約束?通り、わざわざ専用のリストクラスを定義する必要ないかもしれないが、TSymbolクラスのリストを保持するTSymbolListクラスを次のように定義してみる。

このクラスのリストは実行を追跡するルーチンだけを保持するために使われる。つまり、コンパイラやリンカによって生成されるデバッグ情報のすべてのルーチンの実行を追跡するのではなく、必要に応じて選択できるようにする。

また、最終的なアウトプットとして動的コールグラフを画面に表示したりする必要があるのだが、その時、上記TSymbolクラスのセクションインデックスや開始位置のオフセット・長さなどを表示してもユーザーには区別できないので、ルーチンの名前など表示したりするわけだが、そのためには、例えば、TD32デバッグ情報の場合、例えば、

として、TSymbolListクラスのリストには実際にはこのクラスのインタンスを格納する事になる。デバッグ情報に格納されている情報はそれを生成したコンパイラやリンカもしくはフォーマットによって変わるので、このような設計にする。先ほどTSymbolクラスは抽象クラス的な使い方をすると書いたがこのためである(まぁ、ルーチンの名前はどのコンパイラ・リンカでも持ってると思うが・・・)。

次に、各ルーチンの実行の追跡においてブレークポイントを設定・解除する必要があるが、ここではそれ専用のクラスを設計する。まずは、1個のブレークポントを表すTBreakpointクラスを定義する。

Addressプロパティはブレークポイントのアドレスを表す、アドレスは上述したシンボルの情報を元に計算する。OrignalCodeプロパティはブレークポイントを設定する位置の元の命令コードを表す。ブレークポイントの設定では1バイトのINT 3命令(具体的には16進数の0xCC)で置き換えるので1バイトのByte型である。Symbolプロパティはブレークポイントの元になるTSymbolクラスのシンボルであり、Startプロパティはブレークポイントがその元になったシンボルの開始位置または終了位置のどちらに対応するかを表す。と、1つのTSymbolクラスのインタンスからルーチンの2つのTBreakpointクラスのインスタンスを作成する。

また、実際にブレークポイントを設定、解除するためのSetBreakpoint、ResetBreakpointメソッドをヘルパとして追加してある。実装はこんな感じ。

同じく、ブレークポイントのリストクラスであるTBreakpointListクラスを定義する。

リスト内のすべてのブレークポイントを設定・解除するSetAllBreakpointsメソッド、ResetAllBreakpointsメソッドをヘルパとして追加してある。

ふぅ。疲れたdespair

実際にこれらをどう使うかだが、デバッグ対象のプロセスのメインの実行可能モジュール(EXEファイル)内のルーチンだけ実行を追跡するならプロセスレベルのTProcessクラスでもよいのだが、前回に書いたように、DLLファイル内のルーチンの実行の追跡もできるように、TModuleクラスにTSymbolListクラスのSymbolsプロパティ、TBreakpontListクラスのBreakpointsプロパティを追加する事にする(もちろん、DLLのデバッグ情報が見つかればの話だが・・・)。

また、実際にブレークポイントを設定するために、CREATE_PROCESS_DEBUG_EVENT、LOAD_DLL_DEBUG_EVENTでブレークポイントを設定するように修正。

と長くなるので、今回はここまでで、2回にわけようと思うbearing。作ったソースは次回ダウンロードできるようにする。というより、クラス図書いた方が速いとか・・

| | コメント (0) | トラックバック (2)

Win32デバッグ(5)・・・動的コールグラフ

前回までで、Win32のデバッグAPIそしてTD32デバッグ情報について述べ、これで、色々できるようになったので、なんか、実用的というより現実的?なソフトウェアを作ってみるhappy02

お題はタイトルにもあるように動的コールグラフ生成ツール。動的コールグラフとは、実行時の各ルーチンの呼び出し関係を表すグラフの事であるが、これを生成するツールを作ってみる。

ということで開始するのであるが、今回はそのための基本的な事柄・設計や処理のおおまかな流れについて。

まず、動的コールグラフを作成するには各ルーチンの呼び出しを検出する必要があるが、この方法について。色々なやり方があるのかもしれないが、ここでは、x86系プロセッサに備わっているソフトウェアブレークポイントと呼ばれるINT 3命令(ブレークポイント命令)とシングルステップモードを使用する方法を採用する。

INT 3命令とは、x86プロセッサの命令の1つで、実行されるとブレークポイント(BP)例外というトラップクラスの例外が発生する。例外が発生するとCPUは指定された例外ハンドラと呼ばれるルーチンに制御を移すが、通常、その例外ハンドラはOSによって提供され、Windowsの場合、INT 3命令を実行したスレッドのプロセスがデバッグされていると、例外コードEXCEPTION_BREAKPOINTのDEBUG_EXCEPTIONイベントとして、通知されるのでこれをルーチンの開始と終了の検出に利用する。

具体的にはプロセスのアドレス空間内のルーチンのコードの開始と終了位置の命令をINT 3命令に置き換え、ブレークポイントを設定する。置き換えられた元の命令はルーチンの開始または終了の検出後に実行する必要があるので退避する。また、ルーチンのコードの開始と終了位置はロードされたモジュールのベースアドレスとデバッグ情報から求まる。

DEBUG_EVENTイベント発生時(ブレークポイント例外)には、ルーチンの開始または終了の検出に応じてコールグラフを成長させる。また、ContinueForDebugEvent関数による実行の再開において、INT 3命令で置き換えられた元の命令を実行する必要があるので、ブレークポイントを解除し、元の命令に戻すのであるが、この時、再び同じルーチンの呼び出しを検出するため、シングルステップモードで実行を再開する。シングルステップモードにするとCPUは1命令を実行した直後にフォルトクラスのデバッグ(DB)例外を発生させる。これはWindowsでは例外コードEXCEPTION_SINGLE_STEPのDEBUG_EXCEPTIOINイベントとして通知される。

DEBUG_EVENTイベント例外発生時(シングルステップ例外)には、上記の通り、該当のブレークポイントを再設定する。

これらをデバッグイベント別にまとめると次のようになる。

  • CREATE_PROCESS_EVENTイベント発生時
    • ロードされたモジュールにデバッグ情報があれば、必要に応じて各ルーチンのコードの開始・終了位置にブレークポイントを設定
  • LOAD_DLL_EVENTイベント発生時
    • ロードされたモジュールにデバッグ情報があれば、必要に応じて各ルーチンのコードの開始・終了位置にブレークポイントを設定
  • UNLOAD_DLL_EVENTイベント発生時
    • アンロードされたモジュールで設定されている全てのブレークポイントを解除
  • EXIT_PROCESS_EVENTイベント発生時
    • アンロードされたモジュールで設定されている全てのブレークポイントを解除
  • EXCEPTION_EVENTイベント発生時
    • ブレークポイント例外(例外コードEXCEPTION_BREAKPOINT)
      • ブレークポイント例外が発生したアドレスから自身が設定したブレークポイントによる例外かを判定。もし、そうならブレークポイントを解除し、その位置からシングルステップモードで実行再開
    • シングルステップ例外(例外コードEXCEPTION_SINGLE_STEP)
      • 該当のブレークポイントを再設定。

こんな感じだろうか???(LOAD_DLL_EVENT、EXIT_PROCESS_EVENTイベント発生時にブレークポイントをわざわざ解除する必要がないかもしれない・・・とかそういう細かい事は気にしないbearing)。

今回はここまでで、次回実際にクラス設計を行ってみる。

| | コメント (0) | トラックバック (0)

Win32デバッグ(4)・・・デバッグ情報(Borland/CodeGear編)

今回は具体的にデバッグ情報の中身を実際に覗いてみるgood。覗いてみると宣言するように、デバッグ情報の構造やファイルフォーマットなどの詳細まで書くと確実に倒れるbearingので、あくまでイメージを掴むために覗くだけにする。ははは・・。というより、基本的にファイルフォーマットはコンパイラなどの開発環境のメーカーによってその製品のバージョンアップ時に改訂されることがあるので、デバッグ情報にアクセスするためのインターフェースだけを公開し、フォーマットの詳細は公開しないのが現在の流れであるようなので、フォーマットの詳細は知りませんbearing

また、Microsoftの開発ツールによって生成されるデバッグ情報はMicrosoftのよって公開されているDbgHelpライブラリもしくはDIA(Debug Interface Access) SDKを使ってアクセスできるので、詳細はMSDNを参照ということで。DbgHelpライブラリはOSインストール時にインストールされるが、DIA SDKはデフォルトでインストールされていないので、使用する場合には、Visual Studioの統合開発環境をインストールするか、もしくは、再配布可能パッケージvcredist_x86.exeをインストールし、COMサーバーなのでmsdiaXX.dll(XXは80とか90)をregsvr32.exeなどでシステムに登録する必要がある。

ここではタイトルにもあるように自分が使っているBorland/CodeGearの開発ツールiよって生成されるデバッグ情報の中身を覗いてみるのであるが、DelphiのIDEに統合されている統合デバッガが利用しているデバッグ情報にアクセスするためのAPIが公開されていないのである。ガーンcoldsweats02

ということ終了・・って事になるなら、普通は初めからこんなタイトルの記事を書かない事からも分かるように、ここでは、TD32デバッグ情報と呼ばれるデバッグ情報を生成でき、また、アクセスするためのAPIも公開されているのでその中身を見てみる。歴史は詳しくわからないが、TD32デバッグ情報はTurbo Debuggerと呼ばれる社名もまだBorlandの頃の古いデバッガで利用していたデバッグ情報の事だと思う・・・

まず、デフォルトではTD32デバッグ情報は生成されないので、TD32デバッグ情報の生成の仕方から。

適当なプロジェクトを作成して、「プロジェクト」メニューから「オプション」メニューを選択、そして、「リンカ」設定を選択、

Win32debug_4_1_3

上の画像のように「EXEとDLLのオプション」の「TD32デバッグ情報を含める」にチェック。

Win32debug_4_2_2

以上であるが、「コンパイラ」設定の「デバッグ情報」「ローカルシンボル」あたりのオプションも生成されるTD32デバッグ情報の詳細レベルの影響するので・・

で、プロジェクトをコンパイルする。これによってTD32デバッグ情報を含んだ実行可能ファイルが生成される。

TD32デバッグ情報にアクセスするためのAPI(BorDebug.DLLとそのCヘッダ)はCodeGearの以下のサイトからダウンロードできる。

また、BorDebug.DLLの説明とDelphi用のヘッダファイルは以下のサイトからダウンロードできる。

英語の文書を読むのが苦にならない人は上記のサイトからダウンロードできるPDFファイルとCヘッダのコメント読んだ方が手っ取り早いので・・bearing

でいよいよ、実際にTD32デバッグ情報を覗いてみるのだが、そのためのツールTD32Scannerを作ってみた(製品に付属のTDUMP.EXEを使えば、中身をダンプできるのであるが、後でBorDebug.DLLを使ってプログラムからアクセスしたいので、その使い方を覚えるために作ってみたbearing)。実行画面は次の通り。

Win32debug_4_3_2

左側の「SubSection Headers」ウィンドウにはTD32デバッグ情報に含まれるサブセクションのヘッダ一覧が表示されている。最初にデバッグ情報の構造やファイルフォーマットまで詳細に書かないと述べたが、話を進めるために最低限の事は書こうと思う。、まずは、サブセクションから。サブセクションとはTD32デバッグ情報に含まれる論理的な情報のグループのことで、サブセクションに含まれる情報によって、次のタイプに分けられる。

  • Module
  • SrcModule
  • AlignSym
  • GlobalSym
  • GlobalPub
  • Names
  • GlobalTypes
  • Browse

順にこれらのうちのいくつかを見ていくが、その前に今回は、次の簡単なコンソールプログラムのTD32デバッグ情報を例にとり話を進めるので・・

1×1から9×9まで計算するプログラムで、Multiplyは2つの値を積を返す関数である。

まずは、Moduleサブセクション。Moduleサブセクションはモジュールに関する情報が格納されているサブセクションである。Delphiでいうとモジュールは.DCU(Delphi Compiled Unit)ファイルに相当する。上の画像より今回の例ではModuleIndexが1から7までの7つのModuleサブセクションがあることがわかる。「SubSection Headers」ウィンドウのグリッドからModuleIndexが7のModuleサブセクションをダブルクリックしてその詳細を見てみると、次のようになる。

Win32debug_4_5_2

上の画像のNameIndexより、モジュールTD32Target.DCUに関するModuleサブセクションであることが分かる。

また、Segmentsグリッドにはそのモジュールに含まれるコードが最終的な実行可能ファイルのどこに含まれているかの対応関係を表す情報が表示されている。

Windowsにおける実行可能ファイルのフォーマットであるPE(Portable Executale)フォーマットの場合、コンパイラによって生成される機械語命令は最終的にはセクションと呼ばれるブロックに格納されるが、先ほどのSegmentsグリッドのSegment列はそのセクションのインデックスを表す。

この事から、先ほどの画像より、例えば、TD32Target.DCUに含まれるコードがセクションインデックス1の(コード)セクションのオフセット0x00007070からオフセット0x00007213までの420バイトの領域に含まれている事が分かる(ややこしいが、TD32Scannerにおいて、Segmentと表記されているものは、PEファイルにおけるセクションのインデックスを表すので注意・・・・bearing)。

次に、ソースに関する情報が格納されているSrcModuleサブセクション。ここでは、TD32Target.DCUの元になるソースに関する情報を見るとして、「SubSection Headers」ウィンドウのModuleIndexが7のSrcModuleサブセクションをダブルクリックする。

Win32debug_4_6

SourceFilesグリッドより、TD32Target.DCUは1つのソースファイルTD32Target.dprから構成されていることが分かる。

次に、ローカルシンボルに関する情報が格納されているAlignSymサブセクション。SrcModuleの時と同じように、TD32Target.DCUに含まれるシンボルを見るとして、ModuleIndexが7のAlignSymサブセクションをダブルクリックする。

Win32debug_4_8

Symbolsグリッドには含まれるすべてのシンボルが列挙されている。SymbolKindはシンボルの種類を表すがその一覧はここでは省略するとして、ここではローカルプロシージャを表すシンボルであるLPROC32シンボルの詳細を見てみる。例えば、上の画像において、上から2つめのLPROCシンボルを選択してみる。

Win32debug_4_9_2

NameIndexより自分で定義したMultiply関数のシンボルである事が分かる。また、Segment、Offset、Lengthより、この関数の機械語命令のコードがセクションインデックス1のセクションのオフセット0x00007070から0x0000708Bまでの28バイトの領域に含まれていることが分かる。

とまぁ、残りは省略するが、デバッグ情報の内容とはこんな感じなのである。

作ったTD32ScannerはSkyDriveにアップロードしておいた。現在、GlobalTypesサブセクションにはしっかり対応していないのでsad。型に関する情報多すぎbearing

ふぅ。

| | コメント (0) | トラックバック (0)

Win32デバッグ(3)・・・デバッグ情報

前回までで、Win32のデバッグAPIなどを見てきて、GetThreadContextやWriteProcessMemory関数を使えば、スレッドのコンテキストやプロセスのアドレス空間のメモリを書き換えることができることなどと書いたが、デバッガやプロファイラなどの実用的なソフトウェアを作成するにはこれだけではまだ不十分なのである。

ということで、今回はそのような実用的なソフトウェアを作成するために必要となるであろうデバッグ情報について。

デバッグ情報とは、コンパイラやリンカによって生成される実行可能モジュールなどをデバッグするために必要な情報(そのまんまじゃん・・wobbly)、例えば、ソースファイル内で定義した関数なら、実行可能モジュールを実行すると、ローダーと呼ばれるOSのコンポーネントによって、実行可能モジュールのイメージがプロセスのアドレス空間にロードされ、実行が開始されるが、ロード後のプロセスのアドレス空間における関数のコードの開始と終了位置のアドレスを求めるために必要な情報などそのような類似の情報の総称の事である。

生成されるデバッグ情報の詳細レベルは使用しているコンパイラやリンカの種類やまたそれらのオプションスイッチによって変わるが、まぁ、ソースファイル内の行からロード後のアドレスを求めるための行レベルの情報など程度の差こそあれ基本的に似たような情報が格納されているのでここでは問題にしない。

また、デバッグ情報はその情報がシンボルとして格納されているのが一般的なのでシンボリックデバッグ情報とも呼ばれる。シンボルには、宣言した変数や関数などのソースファイル内に現われる具体的なシンボルの他にもソースコードをコンパイル・リンクしたコンパイラやリンカの情報を表すシンボルなどもあり、これも使用しているコンパイラやリンカによって異なるでここでは問題にしない。

Microsoftの統合開発環境Visual Studioを使ってる場合、基本的にそのような情報は拡張子PDBのプログラムデータベースファイル(Program Database File)と呼ばれるファイルに格納され、自分が使ってるBorland/CodeGearの統合開発環境だとorz・・・・

と今回はこれぐらいにするが、ちょっと抽象的な説明ばかりなので、次回以降に具体的にデバッグ情報というものの中身を簡単に覗いてみようかなと・・

最後に、前2回、えらく、長くなってしまったが、ココログにホームページを作成する機能があるっぽさそうなので、ホームページとして作成すれば良かったのかもdespair。まず、このブログの幅leftrightが狭いので広げたいのであるが・・・とりあえず、このネタはブログの記事として終わらせるとして、今度使ってみようgood

| | コメント (0) | トラックバック (0)

Win32デバッグ(2)・・・クラス設計

前回はWin32のデバッグAPIの基本的なことを書いたが、今回はより実践的な事をやろうと思う。

前回、待機関数WaitForDebugEventでDEBUG_EVENT構造体に発生したイベントに関する情報が返されると書いたが、最初にこの構造体を内容を簡単に見てみる。まずは、DEBUG_EVENT構造体の定義から(定義だけはC言語を使うので、あしからず・・)

前半の3つメンバと後半の共用体の部分から構成されている。まずは、前半の3つのメンバから。これらは前回も触れたが、dwDebugCodeEventには、デバッグイベントの種類、dwProcessId、dwThreadIdにはデバッグイベントが発生したプロセスとスレッドの識別子がそれざれ返される。残りの共用体の部分には、デバッグイベントの種類に応じた追加の情報が返されるのであるが、例えば、CREATE_PROCESS_DEBUG_EVENTイベント発生時には、共用体の部分にCREATE_PROCESS_DEBUG_INFO構造体として返される。

順にCREAET_PROCESS_DEBUG_INFO構造体の定義は

となっている。例えば、上記構造体において、hProcess、hThreadには作成されたプロセス、スレッドのハンドルが返されるのであるが、シングルスレッドアプリケーションをデバッグするのならまだしも、マルチスレッドアプリケーションさらにはCreateProcess関数でDEBUG_PROCESSオプションを指定して、子孫のプロセスまでデバッグする場合、これらの情報を適切に管理しておくと、デバッガAPIを使った何かしらの実践的なアプリケーション(例えば、デバッガ自体やプロファイラなど)を作成する時に楽なので、それらをさっさと設計してしまおうというのが今回の趣旨なのである。

と、前置き長すぎbearing

ということで、ここでは、クラスとして設計するのであるが、まずは、プロセスから。デバッグ対象のプロセスの作成、終了は述べたとおり、CREATE_PROCESS_DEBUG_EVENT、EXIT_PROCESS_DEBUG_EVENTイベントで通知され、また、プロセスはプロセス識別子で一意に識別できるので、例えば、プロセスを表すクラスTProcessは次のようになる。

ProcessIdプロパティはプロセスのプロセス識別子、Handleプロパティはプロセスのプロセスハンドルを表す、また、プロセスは1つ以上のスレッドから構成されるので、そのリストであるTThreadListクラスのThreadsプロパティ、同様に、プロセスは、1つ以上のモジュールから構成されるので、そのリストであるTModuleListクラスのModulesプロパティからなる。また、プロセスを強制終了するTerminiateメソッド、プロセスのアドレス空間のメモリを読み書きできるようヘルパーとしてReadXXX、WriteXXXメソッドも追加してある。ReadXXX、WriteXXXメソッドの実装は前回に出てきたAPIを使って、こんな感じ。

次に、複数のプロセスをデバッグできるようにTProcessクラスのリストを保持するTProcessListクラスを定義する。

Addメソッドはリストに指定のプロセス識別子のプロセスを追加する。戻り値は上記のTProcessクラスのインスタンスで、重複したプロセス識別子を指定すると例外が発生する。Removeメソッドは、指定のプロセス識別子のプロセスをリストから削除する。Itemsプロパティは指定したプロセス識別子のプロセスを表すTProcessクラスのインスタンスを返す(存在しなければnil)。

同様に、スレッドを表すTThreadクラス、モジュールを表すTModuleクラスとそれらのリストクラスであるTThreadList、TModuleListクラスを定義する。プロセス、スレッドの場合はそれぞれプロセス識別子、スレッド識別子で一意に識別できるが、モジュールの場合は、モジュールがロードされたアドレスを使って一意に識別する(実行イメージやDLLはローダーによって衝突しないようプロセスのアドレス空間に配置されるのでロードされたアドレスはプロセス内で一意である、また、CREATE_PROCESS_DEBUG_EVENT、EXIT_PROCESS_DEBUG_EVENT、LOAD_DLL_DEBUG_EVENT、UNLOAD_DLL_DEBUG_EVENTイベント発生時にDEBUG_INFO構造体にそのアドレスが返される)。

TProcessとそのリストTProcessListと同様の設計になっているが、TThreadクラスには、スレッドのコンテキストを取得・設定するGetContextメソッド、SetContextメソッドを、また、スレッドを再開・停止・強制終了するResume、Suspend、Terminateメソッド、TModuleクラスには、モジュールのPEファイルのセクションヘッダを読み込むReadSectionHeadersメソッドをヘルパとして追加してある。

と、以上のクラスを定義すれば、各デバッグイベントのイベントハンドラは例えば次のように書ける。

最後に今回、作成したプログラムのダウンロードはSkyDriveのここから。これだけじゃ、ドライバが無いのでコンパイルもできないがとりあえず、アップロードしといた。Debug.pasは、今回作成したTProcessなどのクラスを定義したユニット、Debugger.pasでは、デバッグループやイベントハンドラを含むTDebuggerクラスを定義してある。複数のプロセスをデバッグできるようスレッドとして動作するようにしてある。

以上。ふぅhappy02

| | コメント (0) | トラックバック (0)

Win32デバッグ

プログラミングを行ってる人はソフトウェアを作成する過程で特定のソースコード位置などで実行を停止して、その時の変数の値を確認したり、また、1行ずつコードを実行して、処理の実際の流れを確認したりと、デバッガと呼ばれるソフトウェアもしくは機能を利用する事になると思うが、今回はそのようなデバッガやパフォーマンスプロファイラなどを作成するためにWindowsで提供されているデバッグAPIについて。

Windowsで提供されているデバッグAPIはイベント駆動型のモデルを採用していて、デバッグ対象のプロセス内でデバッグイベントと呼ばれるイベントが発生するのを待機し、発生したら発生したイベントに応じて処理を行うという、Windows上でGUIアプリケーションを作成する時に見かけられるWindowsメッセージを処理するためのメッセージループに似たループを行うことになる。発生するデバッグイベントには次のようなものがある。

デバッグイベント
種類 概要
CREATE_PROCESS_DEBUG_EVENT 新しいデバッグ対象のプロセスが作成される、または、既存のプロセスにデバッグのためアタッチされた時に発生
EXIT_PROCESS_DEBUG_EVENT デバッグ対象のプロセスが終了される時に発生
CREATE_THREAD_DEBUG_EVENT デバッグ対象のプロセス内で新しいスレッドが作成される時に発生
EXIT_THREAD_DEBUG_EVENT デバッグ対象のプロセス内のスレッドが終了する時に発生
LOAD_DLL_DEBUG_EVENT デバッグ対象のプロセス内でDLLがロードされる時に発生
UNLOAD_DLL_DEBUG_EVENT デバッグ対象のプロセス内でDLLがアンロードされる時に発生
EXCEPTION_DEBUG_EVENT デバッグ対象のプロセス内で例外が生成された時に発生
OUTPUT_DEBUG_STRING_EVENT デバッグ対象のプロセス内でOutputDebugString関数が呼び出された時に発生
RIP_DEBUG_EVENT

上記イベントの厳密な発生条件などはMSDNのここを参照。

新規にプロセスを作成してデバッグを開始するには、CreateProcess関数で第6引数dwCreationFlagsにDEBUG_PROCESSオプションを指定する。DEBUG_PROCESSを指定するとCreateProcess関数で作成されたプロセスによって作成される子孫のプロセスもデバッグ対象になるが、CreateProcess関数で作成されるプロセスだけに限定する場合は、DEBUG_PROCESSオプションの代わりにDEBUG_ONLY_THIS_PROCESSオプションを指定する(紛らわしいがDEBUG_PROCESSまたはDEBUG_ONLY_THIS_PROCESSのどちららかを指定する。論理和とって両方指定するのではない.ので注意)。

また、既存の実行中のプロセスのデバッグを開始するには、DebugActiveProcess関数で引数に既存の実行中のプロセスのプロセス識別子を指定し、プロセスにアタッチする。

CreateProcessもしくはDebugActiveProcessでデバッグを開始した後は、先ほど述べたループに突入するのであるが、Delphiで書くと全体の流れは次のような感じなる。

WaitForDebugEventはデバッグイベントが発生するのを待機する待機関数で、第2引数にタイムアウトを指定する。第2引数にINFINITEを指定するとこの関数を呼び出した側へWaitForDebugEvent関数から制御が返らない。デバッグベントが発生すると第1引数で指定したDEBUG_EVENT構造体に発生したイベントの種類、イベントが発生させたプロセスとスレッドの識別子、イベントの種類に応じた追加の情報が返される。

デバッグ対象のプロセス内でデバッグイベントが発生するとデバッグ対象のプロセス内のすべてのスレッドの実行が停止されるが、ContinueDebugEventで実行を再開する。

ContinueDebugEventの第1、2引数にはデバッグイベントが発生して、デバッグ対象のプロセスが停止した原因となったプロセス、スレッドの識別子を指定するが、これらの値はDEBUG_EVENT構造体のdwProcessId、dwThreadIdにWaitForDebugEvent関数から返されているのでこれらの値をそのまま指定する(もちろん、デバッグ対象がシングルスレッドアプリケーションで、CreateProcess関数を使ってデバッグを開始するのならCreateProcess関数で返されるPROCESS_INFORMATION構造体の値を指定してもいいのだが、DEBUG_EVENT構造体の値を使った方が汎用的である)。

ContinueDebugEventの第3引数には以下の続行オプションのいずれかを指定するのであるが、これは、ややこしいbearing

  • DBG_CONTINUE
  • DBG_EXCEPTION_NOT_HANDLED

完全に説明すると、Windowsの構造化例外処理(Structured Exception Handling)の話から書かないといけないので、EXCEPTION_DEBUG_EVENTイベント発生時以外は、MSDNのドキュメントに書いてあるように上記2つのオプションに違いはないので、上記サンプルではDBG_CONTINUEオプションを、EXCEPTION_DEBUG_EVENTイベント発生時は、発生した例外の種類がシングルステップ例外(EXCEPTION_SINGLE_STEP)、ブレークポイント例外(EXCEPTION_BREAKPOINT)以外の時は、通常の例外処理を行わせるためにDBG_EXCEPTION_NOT_HANDLEDを指定、シングルステップ例外、ブレークポイント例外の場合は、DBG_CONTINUEを指定した。

ということで。ははは。

とMSDNのドキュメントを反復してるだけでbearing、MSDNのドキュメントを読んだ方が正確だし手っ取り早いのであるが、ここでは、実際にデバッグAPIを使った時の注意点をいくつか。

まず、CREATE_PROCESS_DEBUG_EVENT、LOAD_DLL_DEBUG_EVENTイベント発生時に返されるファイルハンドル(hFileメンバ)について。これらイベントの発生時、DEBUG_EVENT構造体を通して、ロードされたプロセスの実行イメージのファイルハンドル(CREAET_PROCESS_DEBUG_EVENTの場合)、もしくは、ロードされたDLLのファイルハンドル(LOAD_DLL_DEBUG_EVENT)が返されるが、MSDNのドキュメントにもあるように、使い終わったらこのハンドルは明示的にCloseHandle関数でクローズしないとハンドルがオープンされたままになるので注意。上記のプログラム例では、88行目あたり。

また、

特に、EXIT_PROCESS_DEBUG_EVENTイベント発生時に、発生直後すぐにブレイクしてデバッグループを抜ける例をよく見かけるが、これだと、CREATE_PROCESS_DEBUG_EVENTイベントなどで返されるプロセスハンドルやスレッドハンドルがオープンされたままになるので、通常は1回、ContinueDebugEvent関数を呼んだ後にループからブレイクする方が無難である(ContinueDebugEvent関数で自動的にハンドルがクローズされるので)。上記のプログラム例では、66行目あたり。

最後は、WaitForDebugEvent関数はDEBUG_PROCESSまたはDEBUG_ONLY_THIS_PROCESSオプションを指定したCreateProcess関数またはDebugActiveProcess関数を呼んでデバッグを開始したスレッドでしか呼ぶことができないとの事。

とここまでくれば、後はデバッグAPIの他の関数である

  • ReadProcessMemory
  • WriteProcessMemory

を使えば、プロセスハンドルからそのプロセスのアドレス空間を読み書きする事ができるし、

  • GetThreadContext
  • SetThreadContext

を使えば、スレッドのコンテキスト(x86プラットフォームのWindowsの場合は、EAXやEBXなど各レジスタなど)を読み書きできる。これらのAPIは何もDEBUG_PROCESSまたはDEBUG_ONLY_THIS_PROCESSオプションを指定したCreateProcess関数によって作成されたプロセスやDebugActiveProcessでアタッチしたプロセスでなくてもよいのだが、使用する時は基本的に対象のプロセス・スレッドをSuspendThread関数などで停止していないとおかしくなると思う(WaitForDebugEvent関数でデバッグイベントを受信した時はデバッグ対象プロセス内のすべてのスレッドが停止しているのでSuspendThread関数で停止する必要はないが・・・)。

以上長々と書いたが次回以降は具体的に使ってみようと。

というより、TrueType/OpenTypeフォントの時の成果物であるT2FAnalyzerと同じようにブログ書きながら最終的に実用的?なソフトウェアが出来上がればいいのであるが・・・ははは。

| | コメント (0) | トラックバック (0)