ここではコンパイラやリンカが出力するデバッグ情報について簡単にまとめています。
参考
デバッグ情報に含められている情報の代表的なものとして、
などがあります。
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%綺麗に分かれているわけではなくて、まあそんな分類をすると分かりやすいかな程度の分類です。例えばクラスのメンバフィールドなんかは型情報に含まれるのですが、見方によってはシンボルにも見えると思います。
このセクションには、可変長レコードによりシンボル情報が記されてします。各レコードは最初に自身のレコード長と各識別子をもち、これによって可変長での扱いを可能にしています。関数やブロックはその親スコープも保持します。
| フィールド名 | バイト数 | 目的 |
| length | 2 | レコード長 |
| S_GDATA32 | 2 | 32ビット、グローバル変数であることを示す識別子 |
| offset | 4 | 変数アドレス |
| segment | 2 | 変数アドレス(セグメント部) |
| type | 2 | 型インデックス |
| name | 可変長 | 変数名 |
| フィールド名 | バイト数 | 目的 |
| 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++の仮想関数テーブルのパス記述子 |
このセクションでは、型情報が記述されています。この保存の仕方は若干複雑で、型同士が葉構造を形成しています。例えば、*int型はint型への参照をお持ち、Class型は各メソッドやフィールドへの参照を持ちます。
| length | 2 | レコード長 |
| LF_DIMCONU | 2 | レコードの識別子 |
| rank | 2 | 配列の次元数 |
| index | 2 | 配列インデックスの型を示すレコードのインデックス |
| bound | rank * S | 各次元の上限を指定する定数。Sはインデックスの型によって決まる |
| 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++の仮想関数テーブル |
デバッグ情報には他にも、アドレスとソースステートメントの対応をつけるための情報などが含まれています。