Rust コンパイラにおけるデバッグサポート
このドキュメントでは、Rust コンパイラ(rustc)におけるデバッグツールサポートの状況について説明します。 GDB、LLDB、WinDbg/CDB の概要に加えて、 Rust コードをデバッグするための Rust コンパイラ周辺のインフラストラクチャについても概説します。 Rust コンパイラ自体をデバッグする方法を学びたい場合は、 Debugging the Compiler を参照してください。
この資料は動画 Tom Tromey discusses debugging support in rustc からまとめたものです。
前提知識
デバッガー
Wikipedia によると、
debugger or debugging tool とは、他のプログラム(「ターゲット」プログラム)をテストおよびデバッグするために使用されるコンピュータープログラムです。
ある言語向けにデバッガーをゼロから作成するには、特に さまざまなプラットフォームでデバッガーをサポートする必要がある場合、多くの作業が必要です。 しかし、GDB と LLDB は、ある言語のデバッグをサポートするように拡張できます。 これが Rust が選択した道です。 このドキュメントの主な目的は、Rust コンパイラにおける前述のデバッガーサポートを文書化することです。
DWARF
DWARF 標準の Web サイトによると、
DWARF は、多くのコンパイラやデバッガーがソースレベル デバッグをサポートするために使用するデバッグファイル形式です。C、C++、Fortran など、 多数の手続き型言語の要件に対応しており、他の言語へ拡張できるように設計されています。 DWARF はアーキテクチャ非依存であり、任意のプロセッサーやオペレーティングシステムに適用できます。 Unix、Linux、その他のオペレーティングシステムで広く使用されているほか、 スタンドアロン環境でも使用されています。
DWARF リーダーは、DWARF 形式を取り込み、デバッガー互換の出力を作成するプログラムです。
このプログラムはコンパイラ自体の内部に存在する場合があります。
DWARF は、Debugging Information Entry(DIE)と呼ばれるデータ構造を使用します。これは、関数、
変数などを表すために、情報を「タグ」として格納します。たとえば、DW_TAG_variable、DW_TAG_pointer_type、DW_TAG_subprogram などです。
独自のタグや属性を考案することもできます。
CodeView/PDB
PDB(Program Database)は、Microsoft が作成した、デバッグ情報を含むファイル形式です。 PDB は、WinDbg/CDB などのデバッガーやその他のツールによって取り込まれ、デバッグ情報の表示に使用されます。 PDB には、特定のバイナリに関するデバッグ情報を記述する複数のストリームが含まれます。 たとえば、型、シンボル、指定されたバイナリのコンパイルに使用されたソースファイルなどです。 CodeView は、PDB ストリーム内に現れる symbol records と type records の構造を定義する別の 形式です。
サポートされているデバッガー
GDB
Rust 式パーサー
デバッグ出力を表示できるようにするには、式パーサーが必要です。 この(GDB の)式パーサーは Bison で書かれており、 Rust 式のサブセットのみを解析できます。 GDB パーサーはゼロから書かれており、rustc のパーサーを含む 他のどのパーサーとも関係がありません。
GDB には Rust 風の値と型の出力があります。 出力内で Rust 構文のように見える形で、値や型を表示できます。 また、GDB で ptype として型を表示すると、 それも Rust のソースコードのように見えます。 manual for GDB/Rust のドキュメントを確認してください。
パーサー拡張
式パーサーには、Rust ではできない機能を容易にするための拡張がいくつか含まれています。 いくつかの制限事項は manual for GDB/Rust に記載されています。 これらの拡張をサポートするために、GDB の DWARF リーダーには特別なコードがいくらかあります。
必要となる DWARF リーダーサポートの例をいくつか以下に示します。
-
列挙型: 列挙型のサポートに必要です。 Rust コンパイラは列挙型に関する情報を DWARF に書き込み、 GDB は DWARF を読み取って、タグフィールドがどこにあるのか、 あるいはタグフィールドが存在するのか、 あるいはタグスロットが非ゼロ最適化などと共有されているのかを理解します。
-
トレイトオブジェクトの分解: DWARF 拡張であり、DWARF 内のトレイトオブジェクトの記述が、 対応する vtable のスタブ記述も指し、そのスタブ記述がさらに このトレイトオブジェクトの対象である具体型を指します。 つまり、そのトレイトオブジェクトに対して
print *objectを実行でき、 GDB はトレイトオブジェクト内のペイロードの正しい型を見つける方法を理解します。
TODO: 重複が生じないように、以下をこのガイドページではなく GDB-Rust ドキュメントで言及すべきかどうかを確認する。 これは以下のコメントに関するものです。
gdb の Rust 拡張と制限事項は gdb マニュアルに記載されています: https://sourceware.org/gdb/onlinedocs/gdb/Rust.html – ただし、ここでは gdb の convenience variables と registers が gdb の $ 規約に従うこと、および Rust パーサーが gdb の @ 拡張を実装していることには触れていません。
@tromey、この部分は重複などが生じないように、この ドキュメントではなく GDB-Rust ドキュメントで言及すべきだと思いますか?
LLDB
Rust 式パーサー
この式パーサーは C++ で書かれています。 これは Recursive Descent parser の一種です。 GDB よりも少し少ない範囲の Rust 言語を実装しています。 LLDB には Rust 風の値と型の出力があります。
開発者向けメモ
- LLDB にはプラグインアーキテクチャがありますが、言語サポートには機能しません。
- GDB は一般に Linux 上でよりうまく動作します。
WinDbg/CDB
Microsoft は、Windows Debugger(WinDbg)や
Console Debugger(CDB)などの Windows Debugging Tools を提供しており、どちらも Rust で書かれたプログラムのデバッグをサポートしています。
これらのデバッガーは、利用可能な場合、バイナリのデバッグ情報を PDB から解析し、
デバッガー内で提供するための可視化を構築します。
Natvis
WinDbg と CDB はどちらも、Natvis フレームワークを使用して、
デバッガー内で任意の型に対するカスタム可視化を定義および表示することをサポートしています。
Rust コンパイラは、標準ライブラリ内の型のサブセットに対するカスタム可視化を定義する一連の Natvis
ファイルを定義しています。
たとえば、std、core、alloc などです。
これらの Natvis ファイルは、
*-pc-windows-msvc ターゲットトリプルによって生成される PDBs に埋め込まれ、
デバッグ時にこれらのカスタム可視化を自動的に有効化します。
このデフォルトは、strip rustc フラグを debuginfo または symbols のいずれかに設定することで上書きできます。
Rust は、標準ライブラリ外のクレートについて Natvis ファイルを埋め込むことを、
#[debugger_visualizer] 属性を使用してサポートしています。
デバッガー可視化機能を埋め込む方法の詳細については、
debugger_visualizer attribute に関するセクションを参照してください。
DWARF と rustc
DWARF は、デバッガーが読み取るデバッグ情報をコンパイラが生成する標準的な方法です。 これは macOS と Linux における the デバッグ形式です。 複数言語に対応した拡張可能な形式であり、 Rust の目的にはおおむね十分です。 したがって、現在の実装では DWARF の概念を再利用しています。 DWARF の一部の概念が Rust と意味論的に一致していない場合でも、これは当てはまります。なぜなら、 一般に、両者の間には何らかのマッピングが可能だからです。
Rust コンパイラが出力し、デバッガーが理解する DWARF 拡張がいくつかありますが、 それらは DWARF 標準には 含まれていません。
-
Rust コンパイラは仮想テーブル用の DWARF を出力し、この
vtableオブジェクトは実際の型を指すDW_AT_containing_typeを持ちます。 これにより、デバッガーはトレイトオブジェクトポインターを分解して、ペイロードを正しく見つけることができます。 以下は、gdb リポジトリのテストケースに含まれる、そのような DIE の例です。<1><1a9>: Abbrev Number: 3 (DW_TAG_structure_type) <1aa> DW_AT_containing_type: <0x1b4> <1ae> DW_AT_name : (indirect string, offset: 0x23d): vtable <1b2> DW_AT_byte_size : 0 <1b3> DW_AT_alignment : 8 -
もう 1 つの拡張は、Rust コンパイラがタグなし判別共用体を出力できることです。 この項目については DWARF feature request を参照してください。
DWARF の現在の制限
- トレイト - DWARF でトレイトを表現する方法について、通常よりも大きな DWARF への変更が必要です。
- DWARF には構造体とタプルを区別する方法がありません。
Rust コンパイラは
__0を持つフィールドを出力し、デバッガーはこの制限を回避するためにそのような名前のシーケンスを探します。 たとえば、この場合、デバッガーはx.0ではなくx.__0を介してフィールドを参照します。 これはデバッガー内の Rust パーサーによって解決されるため、現在はx.0を使用できます。
DWARF は、デバッガーがプラットフォーム ABI に関する一部の情報を知っていることに依存しています。 Rust は常にそうしているわけではありません。
開発者向けメモ
このセクションは、開発の特定の側面に関する講演からのものです。
不足しているもの
macOS 上の LLDB デバッグサーバーのコード署名
Wikipedia によると、System Integrity Protection は次のとおりです。
System Integrity Protection(SIP、rootless と呼ばれることもある)は、OS X El Capitan で導入された Apple の macOS オペレーティングシステムのセキュリティ機能です。これは、カーネルによって適用される 多数のメカニズムで構成されています。中心的な機能は、特定の「entitlement」を持たないプロセスによる システム所有のファイルやディレクトリへの変更を防ぐことであり、root ユーザーまたは root 権限(sudo)を持つユーザーとして 実行されている場合でも保護されます。
これは、プロセスが ptrace システムコールを使用することを防ぎます。
プロセスが ptrace を使用したい場合は、コード署名されている必要があります。
それに署名する証明書は、そのマシン上で信頼されている必要があります。
Apple developer documentation for System Integrity Protection を参照してください。
この署名を行うために、Apple に登録して鍵を取得する必要があるかもしれません。 Tom は、Mozilla が署名を許可されている鍵の最大数に達しているため、これを実行できないのではないかと調査しました。 Tom は、Mozilla がさらに鍵を取得できるかどうかを知りません。
あるいは、Tom は、Apple 経由で鍵を取得するために Rust の法人が必要かもしれないと示唆しています。 この問題は技術的な性質のものではありません。 そのような鍵があれば、GDB にも署名して、それを配布できます。
DWARF とトレイト
Rust のトレイトは DWARF にはまったく出力されません。
この影響として、メソッド x.method() の呼び出しはそのままでは機能しません。
その理由は、そのメソッドが型ではなくトレイトによって実装されているためです。
その情報が存在しないため、トレイトメソッドを見つけることができません。
DWARF にはインターフェイス型という概念があります(おそらく Java のために追加されたものです)。 Tom の考えは、このインターフェイス型をトレイトとして使用することでした。
DWARF は具象名のみを扱い、参照型は扱いません。
したがって、ある型に対するトレイトの特定の実装は、これらのインターフェイス(DW_tag_interface 型)の 1 つになります。
また、それが実装されている型は、この型が実装するすべてのインターフェイスを記述することになります。
これには DWARF 拡張が必要です。
GitHub の Issue: https://github.com/rust-lang/rust/issues/33014
デバッグ情報変更の典型的なプロセス(LLVM)
LLVM にはデバッグ情報(DI)ビルダーがあります。 これは、Rust が呼び出す主なものです。 これが、最初に LLVM を変更する必要がある理由です。なぜなら、最初に出力されるのは LLVM であり、DWARF が直接出力されるわけではないからです。 これは、構築して LLVM に引き渡す一種のメタデータです。 Rustc/LLVM の引き渡しでは、 型の表現を構築するために、いくつかの LLVM DI ビルダーメソッドが呼び出されます。
このプロセスの手順は次のとおりです。
-
LLVM を変更する必要があります。
LLVM はインターフェイス型をまったく出力しないため、まず LLVM でこれを実装する必要があります。
これが良いアイデアであることについて、LLVM メンテナーの承認を得ます。
-
DWARF 拡張を変更します。
-
デバッガーを更新します。
DWARF リーダー、式評価器を更新します。
-
Rust コンパイラを更新します。
この新しい情報を出力するように変更します。
手続きマクロのステップ実行
非常に根本的な問題は、実際に手続きマクロをどのようにデバッグするのか、ということです。 マクロ展開に対して、どの位置を出力するのでしょうか。 次のようないくつかのケースを考えてみます -
- マクロの呼び出し位置を出力できます。
- マクロの定義位置を出力できます。
- マクロの内容の位置を出力できます。
RFC: https://github.com/rust-lang/rfcs/pull/2117
焦点は、マクロが何をするかを決定できるようにすることです。 これは、マクロがコンパイラに行マーカーをどこに置くべきかを伝えられるようにする 何らかの属性を持たせることで実現できます。 これは、ブレークポイントを設定する場所と、ステップ実行時に何が起こるかに影響します。
デバッグ情報内のソースファイルチェックサム
DWARF と CodeView(PDB)はいずれも、関連付けられたバイナリに寄与した各ソースファイルの暗号学的ハッシュを 埋め込むことをサポートしています。
暗号学的ハッシュは、ソースファイルが実行可能ファイルと一致することをデバッガーが検証するために使用できます。 ソースファイルが一致しない場合、デバッガーはユーザーに警告を提供できます。
また、ハッシュは、特定のソースファイルが実行可能ファイルのコンパイルに使用されて以降変更されていないことを証明するためにも使用できます。 MD5 と SHA1 はどちらも脆弱性が実証されているため、 この用途には SHA256 の使用が推奨されます。
Rust コンパイラは、各ソースファイルのハッシュを
SourceMap 内の対応する SourceFile に保存します。
外部クレートへの入力ファイルのハッシュは、rlib メタデータに保存されます。
デフォルトのハッシュアルゴリズムは、ターゲット仕様で設定されます。 これにより、すべてのターゲットがすべてのハッシュアルゴリズムをサポートしているわけではないため、ターゲットが 利用可能な最適なハッシュを指定できます。
ターゲットのハッシュアルゴリズムは、-Z source-file-checksum=
コマンドラインオプションで上書きすることもできます。
DWARF 5
DWARF バージョン 5 は、使用中のソースファイルバージョンを検証するために MD5 ハッシュを埋め込むことをサポートしています。 DWARF 5 - セクション 6.2.4.1 opcode DW_LNCT_MD5
LLVM
LLVM IR は、DIFile ノード内の MD5 および SHA1(LLVM 11 以降では SHA256 も)ソースファイルチェックサムをサポートしています。
Microsoft Visual C++ Compiler /ZH オプション
MSVC コンパイラは、/ZH
コンパイラオプションを使用して、PDB 内に MD5、SHA1、または SHA256 ハッシュを埋め込むことをサポートしています。
Clang
Clang は常に MD5 チェックサムを埋め込みますが、これはドキュメントには記載されていないようです。
今後の作業
名前マングリングの変更
libiberty(gcc ソースツリー)内の新しいデマングラー。- LLVM または LLDB 内の新しいデマングラー。
TODO: デマングラーソースの場所を確認する。 #1157
式に Rust コンパイラを再利用する
これは重要な考え方です。というのも、デバッガーは概して型推論を実装しようとしないからです。 実際のソースコードよりも、デバッガーに入力するときのほうが、はるかに明示的である必要があります。 そのため、ソースコードから式をコピーしてデバッガーに貼り付け、そのまま同じ答えが得られると期待することはできませんが、そうできると便利でしょう。 これはコンパイラを使うことで助けられます。
それは確かに実現可能ですが、大きなプロジェクトです。 デバッガーへのブリッジは必ず必要になります。メモリにアクセスできるのはデバッガーだけだからです。 GDB (gcc) と LLDB (clang) のどちらにもこの機能があります。 LLDB は Clang を使ってコードを JIT コンパイルし、GDB も GCC を使って同じことができます。
両方のデバッガーの式評価は、Rust の上位集合であると同時に下位集合でもあるものを実装しています。 それらは式言語だけを実装していますが、 GDB の便宜変数のようないくつかの拡張も追加しています。 したがって、この道を進むのであれば、 このブリッジを作る必要があるだけでなく、 コンパイラが一部の拡張を理解できるようにするための何らかのモードを追加しなければならないかもしれません。