PDB 情報ストリーム(別名 PDB ストリーム)

ストリームヘッダー

PDB ストリームのオフセット 0 には、以下のレイアウトのヘッダーがあります。

struct PdbStreamHeader {
  ulittle32_t Version;
  ulittle32_t Signature;
  ulittle32_t Age;
  Guid UniqueId;
};
  • **バージョン** - 次の enum からの値

enum class PdbStreamVersion : uint32_t {
  VC2 = 19941610,
  VC4 = 19950623,
  VC41 = 19950814,
  VC50 = 19960307,
  VC98 = 19970604,
  VC70Dep = 19990604,
  VC70 = 20000404,
  VC80 = 20030901,
  VC110 = 20091201,
  VC140 = 20140508,
};

このフィールドの意味は明らかのように見えますが、実際には、最新のツールチェーンを使用しても VC70 以外の値は観測されておらず、他の値が存在する理由が不明です。 VC70 以外の値の場合、PDB ストリームのレイアウト、そしておそらく他のストリームのレイアウトにも変更が加えられると想定されています。

  • **シグネチャ** - PDB ファイルが書き込まれた時点で time() を呼び出して生成された 32 ビットのタイムスタンプ。1 秒単位のタイムスタンプを使用することによる固有性の問題があるため、このフィールドは本来の目的を果たしておらず、通常は下記で説明する Guid フィールドが優先されます。

  • **Age** - PDB ファイルが書き込まれた回数。これは Guid とともに、PDB と対応する実行ファイルの対応付けに使用できます。

  • **Guid** - 空間と時間を超えて一意であることが保証された 128 ビットの識別子。一般的に、これは Win32 API UuidCreate を呼び出した結果と考えることができますが、LLVM は非 Windows プラットフォームでも動作する必要があるため、それに依存することはできません。

名前付きストリームマップ

ヘッダーの後に続くのは、キータイプが文字列で、値タイプが整数であるシリアライズされたハッシュテーブルです。 X -> Y のマッピングが存在することは、名前 X のストリームが、基礎となる MSF ファイルでストリームインデックス Y を持つことを意味します。すべてのストリームが名前付きであるとは限りません(たとえば、TPI ストリーム は固定インデックスを持ち、そのため名前でインデックスを検索する必要はありません)。実際には、名前付きストリームは通常少量であり、PDB ファイル形式のストリームテーブルに列挙されています。この系として、ストリームに名前がある場合(そして名前付きストリームマップにある場合)、名前付きストリームマップを参照することが、ストリームの MSF ストリームインデックスを検出する唯一の方法になる可能性が高いです。いくつかの重要なストリーム(/names と呼ばれるグローバル文字列テーブルなど)は、この方法でのみ見つけることができるため、ツールが正しく機能しないため、これを正しく生成および使用することが重要です。

重要

一部のストリームは固定インデックスで配置されます(例:TPI ストリームのインデックスは 2 です)。しかし、他のストリームは固定名で配置されます(例:文字列テーブルは /names と呼ばれます)。名前付きストリームマップを参照することでしか配置場所が特定できません。

名前付きストリームマップのディスク上のレイアウトは、2 つのコンポーネントで構成されます。最初のコンポーネントは、32 ビットの長さで始まる文字列データのバッファーです。2 番目のコンポーネントは、キーと値の両方の型が uint32_t であるシリアライズされたハッシュテーブルです。キーは、ストリームの名前を指定するヌル終端文字列の文字列データバッファー内のオフセットであり、値は、その名前のストリームの MSF ストリームインデックスです。キーは整数ですが、適切なバケットを見つけるために使用されるハッシュ関数は、文字列データバッファー内の対応するオフセットにある文字列をハッシュすることに注意してください。

シリアライズされたハッシュテーブルのディスク上のレイアウトは、PDB シリアライズされたハッシュテーブル形式で説明されています。

名前付きストリームマップ全体には長さプレフィックスが付いていないため、その後に続くデータにアクセスする唯一の方法は、それを完全に逆シリアル化することです。

PDB 機能コード

名前付きストリームマップの後に続き、PDB ストリームの残りのバイトをすべて消費すると、次の列挙からの値のリストになります。

enum class PdbRaw_FeatureSig : uint32_t {
  VC110 = 20091201,
  VC140 = 20140508,
  NoTypeMerge = 0x4D544F4E,
  MinimalDebugInfo = 0x494E494D,
};

これらの値の意味は、次の表にまとめられています。

フラグ

意味

VC110

  • 他の機能フラグはありません。

  • PDB にIPI ストリームが含まれています。

VC140

  • 他の機能フラグが存在する場合があります。

  • PDB にIPI ストリームが含まれています。

NoTypeMerge

  • TPI ストリームに重複する型が存在する可能性がありますが、その理由が不明です。

MinimalDebugInfo

  • プログラムは /DEBUG:FASTLINK でリンクされました。

  • TPI/IPI ストリームはなく、すべての型情報は元のオブジェクトファイルに含まれています。

PDB と実行ファイルの対応付け

リンカーは PDB と最終実行ファイルの両方の書き込みを担当し、その結果、PDB と実行ファイルの対応付けに必要な情報を書き込むことができる唯一のエンティティです。

これを実現するために、リンカーは PDB の guid を生成し(増分リンクの場合は既存の guid を再利用し)、Age フィールドを増分します。

実行ファイルは PE/COFF ファイルであり、PE/COFF ファイルの一部として、多数の「ディレクトリ」が存在します。ここでは、「デバッグディレクトリ」に関心があります。「デバッグディレクトリ」の正確な形式は、IMAGE_DEBUG_DIRECTORY 構造体で説明されています。この特定のケースでは、リンカーは IMAGE_DEBUG_TYPE_CODEVIEW タイプのデバッグディレクトリを出力します。このレコードの形式は llvm/DebugInfo/CodeView/CVDebugRecord.h で定義されていますが、ここでは、同じ GuidAge フィールドが含まれていることだけを述べておきます。実行時に、デバッガーまたはツールは、正しいタイプのデバッグディレクトリが存在するかどうかを COFF 実行可能イメージをスキャンし、Guid と Age が一致するかどうかを確認できます。