デバッガ:デバッグ情報
デバッグ情報
ここではコンパイラやリンカが出力するデバッグ情報について簡単にまとめています。
- windowsの場合
参考
デバッグ情報に含まれているもの
デバッグ情報に含められている情報の代表的なものとして、
- シンボル情報
- 名前
- アドレス(ebpで示される変数はそのオフセット)
- シンボルサイズ
- シンボルの種類(グローバル or スタティック、変数 or 関数 など)
- スコープ
- 型情報
- 名前
- 型の種類(クラス、構造体、メンバフィールド、基本型など)
- 型のサイズ
- リンクされている「次の型」(これについては後述)
- 子供型(関数に対する仮引数、クラスに対するメンバフィールドなど)
- オフセット(クラスメンバのオフセットなど)
- 関数の呼び出し規約
- ソースコードと行番号情報
などがあります。
CやC++では言語的なリフレクションが行えないために、内部インスペクションをする場合には必ず何らかの外部情報が必要になります。ありていに言えばこれが「デバッグ情報」と呼ばれているものになります。例えば関数や変数のアドレス、クラスメンバのオフセットなどが含まれます。これはpdbやdbgのように外部ファイルで提供されることもあれば、gccのようにイメージに統合してしまう場合もあります。どちらの場合もアドレスはリロケータブルな形で扱われ、デバッグ開始時に適切な形で読み込まれます。
このデバッグ情報には例えば、「基底クラスのオフセット情報」や「virtual継承した場合の基底クラスのオフセット情報」など、コンパイラの実装に極度に依存した情報が含まれる場合があります。このため、デバッグ情報というのは言語やコンパイラ、下手をするとコンパイラのバージョンによっても詳細情報が変わることがあります。実際、windowsのpdb形式は頻繁にアップデートが行われていますし、それを読み込むためのライブラリ=dbghelpの実装も随時変更されています。というか、今でも絶賛アップデート中です。また、言語的な違いで言えば、例えばC#などのリフレクションを含んだ言語では型情報は根本的に存在する必要ありません。デバッグ情報がなくともリフレクションによって情報を取得できてしまうからです。場合によっては他の不要になる場合もあるでしょう。これはほんとに場合によって異なります。
デバッグ情報にはこういった特徴があるために、詳細なフォーマット情報が提供されることはあまり無いようです。少なくともwindowsの世界ではそうです。その代わりデバッグ情報を扱うライブラリがそういった詳細を隠す形で、各フォーマットに従い適切な処理を行うことが多いです。実際、windows(dbghelp), linux(dwarf)ではこのような実装が行われています。dwarfではバックエンドと呼ばれるレイヤを用意し、各コンパイラや環境間の差異を吸収しています。またこういった事情があるために、デバッグ情報の統一的なフォーマットを作成することが難しくなっています。
シンボル情報へのアクセス
以下にはネイティブデバッガが作る典型的なデータ群を紹介します。
- 命令アドレス -> スコープ へのマップ
- スコープ -> それを静的に含む親スコープ へのマップ
- スコープ + 識別子 -> そのシンボルの型と位置 へのマップ
- コードまたはデータのアドレス -> 変数や関数 へのマップ
- 命令アドレス -> ソースステートメント へのマップ
- ソースステートメント -> 命令アドレス へのマップ
ある調査によると、典型的なデバッグセッションではデバッグ情報全体の約15%程度しか参照されないようです。pdbファイルのサイズを見れば分かりますが(Gaucheの場合、releaseビルドで約3MB)、結構肥大化することがあります。こういう場合の戦略としてはキャッシュが最適です。事前にすべての情報をかき集めるような真似は止めましょう。
典型的なデバッグ情報の内容
ここでは、各デバッグフォーマットの基礎とも言うべきCodeView(C7)形式の概略を示します。実際にはデバッガの理論と実装からのパクリですが、イメージを伝えるため良しとします^^
その前に
PEやELFなどの実行バイナリ形式では、含まれている情報の種類に応じてセクションを分けています。例えば、コードは'.text'セクションに、データは'.data'セクションに入れることで、.textセクションの書き換えだけを不可にすることができます。悪名高い(?)自己書き換えコードの実行もこれで防ぐことができます。しかしながら、.dataセクションにある実行コードが実行できないかといえば、全然そんなことは無くて、実行できるかどうかは多分環境による(?)と思います。
「シンボル情報」や「型情報」などの区切りも、必ずしも100%綺麗に分かれているわけではなくて、まあそんな分類をすると分かりやすいかな程度の分類です。例えばクラスのメンバフィールドなんかは型情報に含まれるのですが、見方によってはシンボルにも見えると思います。
$$SYMBOLS セクション
このセクションには、可変長レコードによりシンボル情報が記されてします。各レコードは最初に自身のレコード長と各識別子をもち、これによって可変長での扱いを可能にしています。関数やブロックはその親スコープも保持します。
- 32ビット、グローバル変数のエンコーディング
フィールド名 バイト数 目的 length 2 レコード長 S_GDATA32 2 32ビット、グローバル変数であることを示す識別子 offset 4 変数アドレス segment 2 変数アドレス(セグメント部) type 2 型インデックス name 可変長 変数名
- 32ビット、ローカル変数のエンコーディング
フィールド名 バイト数 目的 length 2 レコード長 S_BPREL32 2 ebpからのオフセットで示された変数 offset 4 ebpレジスタからの符号付オフセット type 2 型インデックス name 可変長 変数名
- 他にも以下のようなレコードがあります。
S_REGISTER レジスタ定数 S_CONST 定数 S_UDT ユーザー定義型(Struct, Class など) S_LDATA32 static変数 S_LPROC32 static関数 S_GPROC32 グローバル関数 S_THUNK32 サンクプロシージャ S_BLOCK32 静的なスコープ S_WITH32 PascalなどのWithステートメント S_END 関数やブロックの終端 S_LABEL32 ラベルステートメント S_VFTPATH32 C++の仮想関数テーブルのパス記述子
$$TYPES セクション
このセクションでは、型情報が記述されています。この保存の仕方は若干複雑で、型同士が葉構造を形成しています。例えば、*int型はint型への参照をお持ち、Class型は各メソッドやフィールドへの参照を持ちます。
- インデックスの上限と下限を持つ、配列型のエンコーディング
length 2 レコード長 LF_DIMCONU 2 レコードの識別子 rank 2 配列の次元数 index 2 配列インデックスの型を示すレコードのインデックス bound rank * S 各次元の上限を指定する定数。Sはインデックスの型によって決まる
- C++クラスのエンコーディング
length 2 レコード長 LF_CLASS 2 count 2 メンバの数 memberlist 2 メンバリストを示すもう一つのレコードへのインデックス property 2 クラスの属性を決定するビットマスク(演算子のオーバーロードがあるかなど) dlist 2 継承クラス群を示すレコードへのインデックス vshape 2 仮想関数テーブルを示すレコードへのインデックス classlength 2 クラスのサイズ name 可変長 クラス名
- 他にも以下のようなレコードがあります。
LF_POINTER ポインタ型 LF_ENUM 列挙型 LF_PROCEDURE プロシージャ型 LF_METHODLIST メンバ関数リスト LF_FIELDLIST C++クラスなどのフィールドリスト LF_BITFIELD ビットフィールドを示す型 LF_ARGLIST 仮引数リスト LF_VFUNCTAB C++の仮想関数テーブル
他の情報
デバッグ情報には他にも、アドレスとソースステートメントの対応をつけるための情報などが含まれています。