LLDB の内部構造
LLDB のデバッグ情報処理は、主に lldb/src/Plugins で定義されている一連の拡張可能なインターフェイスに依存しています。これらは、サードパーティのコンパイラ開発者が、LLDB によって実行時にロードされる言語サポートを追加できるようにすることを意図したものですが、執筆時点(2025年11月)では公開 API がまだ確定していないため、プラグインは LLDB 自体の内部、または LLDB のスタンドアロンフォーク内に存在しています。
通常、言語サポートはこれらのプラグインのパイプラインとして記述されます: *ASTParser ->
TypeSystem -> ExpressionParser/Language。
以下は、LLDB のプラグイン API の既存の実装例です。
- Swift をサポートする Apple のフォーク
- Rust をサポートしていた CodeLLDB の以前のフォーク
- Rust サポートの再実装の進行中の作業
- Rust 式パーサープラグイン。
これは
TypeSystemAPI が作成される前に書かれたものです。式解析は自由形式の性質を持つため、基盤となる字句解析、構文解析、関数呼び出しなどは、今でも有益な知見を提供してくれるはずです。
Rust サポートと TypeSystemClang
デバッグ情報の概要で述べたように、LLDB には部分的な Rust サポートがあります。さらに明確にすると、Rust は C/C++ 向けに構築されたプラグインパイプラインを使用しています(ただし、Rust の enum 型向けのヘルパーもいくつか含まれています)。これは clang コンパイラの型表現に直接依存しています。そのため、LLDB の出力が期待するものと一致しない場合に、どの程度変更できるかに大きな制約が課されます。いくつかの回避策は役立ちますが、最終的には Rust の要求は、C および C++ のコンパイルとデバッグが正しく動作することを保証することに比べると二次的なものです。
LLDB は TypeSystemRust の追加を受け入れる姿勢がありますが、それは非常に大規模な取り組みです。このセクションは、現在 TypeSystemClang とどのように連携しているかを文書化するだけでなく、将来 TypeSystemRust を実装する際の軽い指針としても機能します。
TypeSystem が対象言語のコンパイラと直接連携することが意図されている点は注目に値しますが、それは必須要件ではありません。必要な補助型はすべて、プラグイン実装内で作成できます。
注: LLDB のドキュメントは、ソースコード内のコメントも含めてかなり少ないです。
TypeSystemClangの実装を読んで言語サポートの仕組みを理解しようとするのは、clangコンパイラの内部構造を理解する必要も加わるため、やや困難です。 上に挙げた 2 つのTypeSystemRust実装を見ることを推奨します。これらはコンパイラの型表現を利用せずに「ゼロから」書かれているためです。これらは、言語サポートを実装するために必要な最小限に比較的近いものです。
DWARF と PDB
LLDB は DWARF と PDB の両方のデバッグ情報を扱えるという点で独自です。ただし、これにはいくらかの複雑さが伴います。さらに事態を複雑にしているのは、PDB サポートが dia と native に分かれていることです。dia は Visual Studio とともに配布される msdia140.dll ライブラリに依存しており、native は PDB 形式に関する公開情報を使用してゼロから書かれています。
注:
diaは LLDB バージョン 21 まではデフォルトでした。nativeは LLDB 22 のリリース時点で新しいデフォルトです。diaベースのプラグインを非推奨にし、完全に削除する計画があります。そのため、以下ではnativeによる解析のみを扱います。進捗については、 この Discourse スレッドおよび関連する追跡 issueを参照してください。
nativeは、LLDB 22 で追加されたplugin.symbol-file.pdb.reader設定、または環境変数LLDB_USE_NATIVE_PDB_READER=0/1を使用して切り替えられます。
デバッグノードの解析
最初のステップは、生のデバッグノードを使用可能なものへ処理することです。これは主に DWARFASTParser クラスと PdbAstBuilder クラスで行われます。これらのクラスには、それぞれ SymbolFileDWARF と SymbolFileNativePDB から生成された、デシリアライズ済みのデバッグ情報が渡されます。SymbolFile の実装者は、基盤となるデバッグ情報をパーサーに渡す前に、ほとんど変換を行いません。PDB と DWARF のどちらについても、デバッグ情報は LLVM のデバッグ情報ハンドラーを使用して読み取られます。
パーサーは、LLDB の目的にとってより扱いやすい形式へノードを変換します。clang の場合、これらの形式は clang::QualType、clang::Decl、clang::DeclContext であり、C および C++ をコンパイルする際に clang が内部的に使用する型です。繰り返しになりますが、コンパイラの型表現を使用することは必須ではありませんが、プラグインシステムはそれが可能であることを前提に構築されています。
注: 上記の型は、
TypeSystemClangの具体的な実装詳細が関係しない場合、言語非依存の用語としてLangType、Decl、およびDeclContextと呼びます。
LangType は型を表します。これには、型の名前、サイズとアラインメント、分類(例: struct、プリミティブ、ポインター)、修飾子(例:
const、volatile)、テンプレート引数、関数の引数型および戻り値型などの情報が含まれます。こちらは、RustType がどのようなものになり得るかの例です。
Decl はあらゆる種類の宣言を表します。それは型、変数、struct の静的フィールド、static または const が初期化される値などである可能性があります。
DeclContext は多かれ少なかれスコープを表します。DeclContext には通常、Decl や他の DeclContext が含まれますが、その関係はそれほど単純ではありません。たとえば、関数は Decl であることも(関数シグネチャは型であるため)、かつ DeclContext であることもできます(関数には変数宣言、ネストされた関数宣言などが含まれるためです)。
変換プロセスはかなり冗長になることがありますが、通常は単純です。ここでの作業の多くは、LangType、Decl、DeclContext を埋めるために必要な正確な情報に依存します。
ノードが変換されると、そのノードへのポインターは型消去され(void*)、CompilerType、CompilerDecl、または CompilerDeclContext にラップされます。これらのラッパーは、それらを所有する TypeSystem に関連付けます。これらのオブジェクトのメソッドは TypeSystem に委譲され、TypeSystem は void* を適切な LangType*/Decl*/DeclContext* にキャストし直して内部を操作します。Rust の用語では、この関係はおおよそ次のようになります。
struct CompilerType {
inner_type: *mut c_void,
type_system: Arc<dyn TypeSystem>,
}
impl CompilerType {
pub fn get_byte_size(&self) -> usize {
self.type_system.get_byte_size(self.lang_type)
}
}
...
impl TypeSystem for TypeSystemLang {
pub fn get_byte_size(lang_type: *mut c_void) -> usize {
let lang_type = lang_type as *mut LangType;
// LangType の内部を操作して
// そのサイズを決定する
...
}
}
型システム
TypeSystem インターフェイスには、主に 3 つの目的があります。
- ある言語の型に対する「唯一の権威」として機能すること。これにより、その型システムを LLDB の型システムの「プール」に追加できます。実行可能ファイルがロードされると、対象言語が判定され、その言語を処理できると主張する
TypeSystemを見つけるためにプールが問い合わせられます。TypeSystemを使用して、背後にあるSymbolFileを取得したり、型を検索したり、デバッグ情報に存在しない可能性のある基本型(例: プリミティブ、Tの配列、Tへのポインター)を合成したりすることもできます。 LangType、Decl、DeclContextオブジェクトのライフタイムを管理すること- それらの型がどのように表示され、どのように操作できるかの「デフォルト」をカスタマイズすること。
最初の 2 つの機能はかなり単純なので、ここでは 3 つ目に焦点を当てます。
TypeSystem インターフェイスの多くの関数は、ビジュアライザースクリプトを扱ったことがあるなら見覚えがあるでしょう。これらの関数は、対応する名前を持つ SBType や SBValue の関数を支えています。たとえば、TypeSystem::GetFormat は、カスタムフォーマッターが適用されていない場合に、その型のデフォルトフォーマットを返します。
特に注目すべきなのは、GetIndexOfChildWithName と GetNumChildren です。これらの関数の TypeSystem 版は、SBValue 版のように値ではなく、型に対して動作します。TypeSystem 関数から返される値は、構造体のどの部分が LLDB の他の部分からそもそも操作可能であるかを決定します。フィールドが省かれると、そのフィールドは LLDB にとって実質的にもはや存在しなくなります。
さらに、これらはオブジェクトを扱わないため、調査または解釈するための基礎となるメモリがありません。基本的に、これはこれらの関数が対応する SyntheticProvider 関数と同じ目的を持たないことを意味します。Vec にいくつの要素があるか、またはそれらの要素がどのアドレスに存在するかを判定する方法はありません。和型の判別子の値を判定することもできません。
理想的には、TypeSystem はデバッグ情報に現れる型を、可能な限り変更を加えずに公開すべきです。LLDB の synthetic とフロントエンドは、型を見やすく整えることができます。ある情報が役に立たない場合は、そもそもそのデバッグ情報を出力しないように Rust コンパイラーを変更すべきです。
式の解析
TypeSystem は通常、式の解析を処理できる対応物を持つように書かれます。そのためには、TypeSystem インターフェイスでいくつか追加の関数を実装する必要があります。式解析コードの大部分は lldb/source/Plugins/ExpressionParser に置くべきです。
パーサーについて特筆すべきことはあまり多くありません。(おそらく簡略化された)Rust 構文を処理できる単純なインタープリターを実装する必要があります。これらは lldb::ValueObject に対して動作し、これは SBValue を支えるオブジェクトです。
Language
Language プラグインは、Python ビジュアライザースクリプトに相当する C++ の機能です。これらは同じ目的、つまり synthetic child の作成と pretty-printing のために、SBValue オブジェクトに対して動作します。LibCxx 型に対する CPlusPlusLanguage の実装は、ビジュアライザーをどのように書くべきかを学ぶための優れた資料です。
これらのプラグインは LLDB の private な内部(基礎となる TypeSystem を含む)にアクセスできるため、Language プラグインとして書かれた synthetic/summary provider は、Python の同等物よりも高品質な出力を提供できます。
デバッグノードの解析、型システム、式の解析はすべて互いに密接に関連していますが、Language プラグインはよりカプセル化されているため、既存の型システムがサポートする任意の言語向けに「スタンドアロン」で書くことができます。参入障壁が低いため、RustLanguage プラグインは LLDB における完全な言語サポートへの良い足がかりになるかもしれません。
ビジュアライザー
作業中