デバッガ:デバッグ情報:windows
windowsでデバッグ情報を扱う場合には、このdbghelpというライブラリを使います。シンボル情報の読み込みや型情報の取得などはすべてこのライブラリが担っています。
このライブラリには多少扱いづらいところがあるため、一番最初にはまりやすい注意事項について書いてあります。
参考
dbghelp
注意事項1
実は、似たようなライブラリとしてimagehlpというのもあるのですが、これは古いバージョンのdbghelpだと考えていいでしょう。昔はimagehlpといえばデバッグ情報やイメージ情報を統合的に扱うライブラリだったのですが、ちょっと巨大すぎるということで一部の分割&切り出しが行われる予定でした。dbghelpがデバッグ情報部分のみを扱うライブラリになるはずだったのだと思います。名前にもそういう雰囲気がありますね。
現在のimagehlpとdbghelpを比較する限り、両者はバージョンが違う同じライブラリです。dllの中身もヘッダファイルの内容もほぼ同じです。ヘッダファイルの場合、APIの内容が同じでもインクルードガード部分が違うので、両者を同じプロジェクトで使うと確実にエラーを引き起こします。これは深刻な問題ですので、windowsではimagehlpは使わずに必ずdbghelpを使うようにしましょう。
注意事項2
dbghelpは今でも頻繁なアップデートが繰り返されています。理由は違うページにも書いてありますが、本質的にデバッグ情報はコンパイラやリンカと共にあるものだからです。このため古いOSにデフォルトで付属されているものを使ってしまうと、満足な結果が得られない場合があります。新しい関数が使えないのはもちろん、場合によっては正しくエクスポートされている古い関数ですら失敗することがあります。例えばwindows2000では、ほとんどのシンボル関連関数が引数によらず失敗します。
そのためdbghelp.dllについては、必ず新しいバージョンのdllを使う必要があります。ダウンロードは
などから行うことができます。ところが理由は不明なんですが、バージョンを調べるための関数ImagehlpApiVersionの値は当てになりません。dbghelp.dllが正しいバージョンかどうかは、比較的新しい関数をエクスポートしているかどうかで判断するとよいでしょう。
具体的には、このライブラリを使う場合は、
- アプリにdbghelp.dllの最新版を付属する。
- dbghelp.dllから使うすべての関数を動的にインポートし、新しいバージョンの関数に対応しているか調べる。
- 対応していない場合は、エラーを返す。
ということを守る必要があります。少し(?)面倒なんですが、これをしない場合、「WinXPでもWin2000でも初期化には成功するが、なぜかWin2000だと動かない」といった非常に分かりにくいバグを生む原因になります。気をつけましょう。
典型的な使い方
- SymGetOption/SymSetOptionでロードする情報や扱う情報の種類を指定します。
- SymInitializeで全体の初期化を行います。
- SymLoadModule(64) などの関数で、シンボル情報が必要なモジュールを読み込みます。これはSymInitializeで特殊なオプションを指定することにより、同様のことが行えます。
- シンボル情報が欲しい場合は、SymEnumSymbolsやSymFromAddrなどのAPIを用いて取得します。前者の場合はそのスコープ(モジュールなど)のシンボルをまとめて列挙、後者の場合はアドレスからシンボルを取得することができます。
- 型情報が欲しい場合は、SymGetTypeInfo関数を用いて型のさまざまな情報を取得できます。
- アドレスから対応するステートメントを取得する場合は、SymGetLineFromAddr(64)を用いてソースファイルのフルパスや行番号を取得できます。
- 不必要なモジュールはSymUnloadModule(64)でアンロードします。
- 最後はSymCleanupで終了します。
ローカル変数情報が欲しい場合には、SymEnumSymbolsを行う前にSymSetContextで関数アドレスを指定します。するとローカル変数の一覧を列挙することができます。
関数名の最後に"(64)"とついているものは、64ビット対応の関数が存在するという意味です。32ビットコンピュータでも、たまに64ビットバージョンを使った方がいい場合もあるので、可能な限り常にこちらを使うようにしましょう。ちなみに、ついていないものは最初から32/64ビットのどちらにも対応しているAPIです。
具体的な扱い方についてはmsdnやpdbdumpのソースを参考にしてください。
型情報について
型情報は、SymGetTypeInfoAPIで取得できます。ただし、型情報を構成する葉構造は少し分かりにくいと思うので軽く紹介しておきます。
詳しくはこちらHow to use DbgHelp (SymGetTypeInfo) to access type information
ざっくりといえば、以下のような感じで型が葉構造を形成しています。「次の型」というのがなんともいえないのですが、型を示す上で必要な情報がリスト状、もしくは木構造となっていて、デバッガはこの情報から型を文字列に変えたりします。例えば、int*型はint型へのリンクをもっています。また、関数型は仮引数を子要素としてもっています。かなり恣意的な構造なので、気をつけましょう。
図表の見方
|-------------------------------------| | Tag(型の大まかな種類) | |-------------------------------------| | Index(型のインデックス) | |-------------------------------------| | (Next)Type (型リスト上の「次の型」) | | Name (型名) | |-------------------------------------|
基本型
int i; |-----------------| |------------------| | Data | | BaseType | |-----------------| |------------------| | Index = 15 | (Next)Type | Index = 16 | |-----------------| -----------> |------------------| | (Next)Type = 16 | | BaseType = btInt | | Name = "i" | | Length = 4 | |-----------------| |------------------|
ポインタ型
int *p; |-----------------| |-----------------| |------------------| | Data | | PointerType | | BaseType | |-----------------| (Next)Type |-----------------| |------------------| | Index = 15 | -----------> | Index = 16 | (Next)Type | Index = 17 | |-----------------| |-----------------| -----------> |------------------| | (Next)Type = 16 | | (Next)Type = 17 | | BaseType = btInt | | Name = "p" | | Length = 4 | | Length = 4 | |-----------------| |-----------------| |------------------|
配列型
int array[3][4]; |-----------------| |-----------------| |------------------| |------------------| | Data | | ArrayType | | BaseType | | BaseType | |-----------------| (Next)Type |-----------------| |------------------| |------------------| | Index = 15 | -----------> | Index = 16 | (Next)Type | Index = 17 | | Index = 18 | |-----------------| |-----------------| -----------> |------------------| (Next)Type |------------------| | (Next)Type = 16 | | (Next)Type = 17 | | (Next)Type = 18 | -----------> | BaseType = btInt | | Name = "array" | | Length = 48 | | Length = 16 | | Length = 4 | |-----------------| | Count = 3 | | Count = 4 | |------------------| |-----------------| |------------------|
関数型
int CText::DoText(long Num); |------------------------| |--------------------| |----------------------| | Function | | FunctionType | | UDT(UserDefinedType) | |------------------------| (Next)Type |--------------------| |----------------------| | Index = 15 | -----------> | Index = 16 | ClassParent | Index = 18 | |------------------------| |--------------------| -----------> |----------------------| | (Next)Type = 16 | | (Next)Type = 17 | | Name = "CTest" | | Name = "CTest::DoTest" | | ClassParent = 18 |------- |----------------------| |------------------------| | Count = 2 | | (これは本当はメソッドリストなどを持ちます) |--------------------| | ↑ | (Next)Type | | | parent/child | |------------------| | |-------| BaseType | ↓ |------------------| |-------------------| | Index = 17 | | FunctionArgType | |------------------| |-------------------| | BaseType = btInt | | Index = 19 | | Length = 4 | |-------------------| |------------------| | (Next)Type = 20 | (これが戻り値となります) |-------------------| (仮引数が複数あれば、この要素は複数存在します) ↑ | | (Next)Type ↓ |-------------------| | BaseType | |-------------------| | Index = 17 | |-------------------| | BaseType = btLong | | Length = 4 | |-------------------|
pdbdump
サンプルとして、pdbに含まれているシンボル情報と型情報をそのまま出力するツールを作りました。何かの参考になれば幸いです。
使い方 pdbdump [イメージファイル名(.exe .dll など)] とすると、そのイメージがあるディレクトリに 1, [inputname].symbols.xml 2, [inputname].types.xml という二つのファイルが作られます。 1,のファイルがシンボル情報を、2,のファイルが型情報を含んでいます。 出力がxmlなのは単なる趣味です。 オプションを全く用意していないため日常的に使うツールとしては不十分ですが、 サンプルとしては十分でしょう。
- pdbdumpのソースなど
- サンプルの出力ファイル
- sample.cpp
- sample.exe.symbols.xml
- sample.exe.types.xml
- (コンパイル時には、ランタイムなどの余計な情報はすべて削ぎ落としてあります)
- libgauche.dll(VC8, リリースモード) の出力ファイル。巨大なため圧縮してあります。
代表的なAPI
- SymInitialize
BOOL SymInitialize(HANDLE hProcess, PSTR UserSearchPath, BOOL fInvadeProcess)
- dbghelp(imagehlp)の初期化を行います。
- hProcess: シンボルを維持するプロセスのハンドルを指定します。
- UserSearchPath: 1 つのパス、またはセミコロン(;)で区切られた複数のパスを指定する、null で終わる文字列へのポインタを指定します。このパスを使って、シンボルファイルを検索します。
- fInvadeProcess: このパラメータが TRUE の場合は、ImageHlp はこのプロセス用のロード済みモジュールを列挙し、各モジュールに対して効率的に SymLoadModule 関数を呼び出します。
- SymCleanup
BOOL SymCleanup(HANDLE hProcess)
- dbghelp(imagehlp)の終了処理を行います。
- hProcess: 最初に SymInitialize 関数へ渡された、プロセスのハンドルを指定します。
- SymLoadModule
DWORD SymLoadModule(HANDLE hProcess, HANDLE hFile, PSTR ImageName, PSTR ModuleName, DWORD BaseOfDll, DWORD SizeOfDll)
- シンボルテーブルをロードします。モジュールのベースアドレスを返します。
- hProcess: 最初に SymInitialize 関数へ渡された、プロセスのハンドルを指定します。
- hFile: 実行可能なイメージを保持するファイルのハンドルを指定します。ほとんどの場合、この引数を使うのはデバッガであり、デバッガがデバッグイベントにより取得したファイルハンドルを渡すことになります。値として NULL を指定すると、hFile を使わないことを意味します。
- ImageName: 実行可能イメージの名前を指定する、null で終わる文字列へのポインタを指定します。
- ModuleName: モジュールのショートカット名を指定する、null で終わる文字列へのポインタを指定します。
- BaseOfDll: モジュールのロードアドレスを指定します。このパラメータの値が 0 の場合、ImageHlp はシンボルファイルからロードアドレスを取得します。シンボルファイル内に記録されているロードアドレスは、必ずしも実際のロードアドレスとは限りません。デバッガや他のアプリケーションが実際のロードアドレスを把握している場合、この関数を呼び出すときに真のロードアドレスを使うべきです。 SizeOfDll: モジュールのサイズを指定します。このパラメータの値が 0 の場合、ImageHlp はシンボルファイルからサイズを取得します。シンボルファイル内に記録されているサイズは、必ずしも実際のサイズとは限りません。デバッガや他のアプリケーションが実際のサイズを把握している場合、この関数を呼び出すときに真のサイズを使うべきです。
- SymUnloadModule
BOOL SymUnloadModule(HANDLE hProcess, DWORD BaseOfDll)
- シンボルテーブルをアンロードします。
- hProcess: 最初に SymInitialize 関数へ渡された、プロセスのハンドルを指定します。
- BaseOfDll: アンロードしたいモジュールのベースアドレスを指定します。
- SymGetTypeInfo
BOOL SymGetTypeInfo(HANDLE hProcess, DWORD64 ModBase, ULONG TypeId, IMAGEHLP_SYMBOL_TYPE_INFO GetType, PVOID pInfo)
- 特定のインデックスを持った型の情報を取得します。
- hProcess: 最初に SymInitialize 関数へ渡された、プロセスのハンドルを指定します。
- ModBase: モジュールのロードアドレスを指定します。
- TypeId: 型のインデックスです。これはシンボル情報に含まれています。
- GetType: どの種類の情報を取得するかを示します。以下のような値(実際にはもっと多い)が指定できます。
- TI_GET_SYMTAG: 型の種類(ポインタや配列、クラス)などを取得します。
- TI_GET_SYMNAME: 型の種類を取得します。
- TI_GET_LENGTH: 型のサイズを取得します。
- TI_GET_TYPEID: 前述した「次の型」のIDを取得します。
- TI_GET_BASETYPE: 基本型である場合、その詳細な型の種類(intやcharなど)を取得します。
- TI_GET_ARRAYINDEXTYPEID: 配列型である場合、そのインデックスの型のIDを取得します。
- TI_GET_COUNT: 配列型である場合、その要素の数を取得します。
- TI_FINDCHILDREN: 木構造で記述された型の子供がいるばあい、それを取得します。 TI_GET_OFFSET,
- TI_GET_CHILDRENCOUNT: 子供がいる場合、その数を取得します。 TI_GET_SYMINDEX,
- TI_GET_UDTKIND: ユーザー定義型の種類(Class,Struct,Union)を取得します。
- TI_GET_CALLING_CONVENTION: 関数型なら、その関数の呼び出し規約を取得します。
- TI_GET_IS_REFERENCE: 参照型であるかどうかを取得します。
- pInfo: GetTypeの値に応じた適切なデータを指定します。これに情報が入ります。
- SymGetLineFromAddr
BOOL SymGetLineFromAddr(HANDLE hProcess, DWORD dwAddr, DWORD *pdwDisplacement, IMAGEHLP_LINE *Line)
- アドレスからソースファイル名や行番号を取得します。
- hProcess: 最初に SymInitialize 関数へ渡された、プロセスのハンドルを指定します。
- dwAddr: 行を検索したいアドレスを指定します。このアドレスは行の境界でなくてもかまいません。アドレスが行の先頭と行の終わりの間(行の中間)に相当する場合、その行が見つかります。
- pdwDisplacement: 行の先頭からの変位(オフセット)、または 0 を指定します。
- Line: 構造体へのポインタを指定します。