LLVMを用いたソースレベルデバッグ¶
はじめに¶
このドキュメントは、LLVMのデバッグ情報に関するすべての中心的な情報リポジトリです。 LLVMデバッグ情報が取る実際のフォーマットについて説明しており、フロントエンドの作成や情報に直接対処することに関心のある方に役立ちます。 さらに、このドキュメントでは、C/C++のデバッグ情報の具体的な例を示します。
LLVMデバッグ情報の背後にある哲学¶
LLVMデバッグ情報の考え方は、ソース言語の抽象構文木の重要な部分がLLVMコードにどのようにマッピングされるかを捉えることです。 ここで示す解決策は、いくつかの設計上の側面によって形作られています。 重要なものは以下のとおりです。
デバッグ情報は、コンパイラの他の部分にほとんど影響を与えないようにする必要があります。 デバッグ情報のために、変換、解析、またはコードジェネレーターを変更する必要はありません。
LLVMの最適化は、デバッグ情報と明確に定義され、容易に記述できる方法で相互作用する必要があります。
LLVMは任意のプログラミング言語をサポートするように設計されているため、LLVM-to-LLVMツールはソースレベル言語のセマンティクスについて何も知る必要はありません。
ソースレベル言語は、しばしば互いに**大きく**異なります。 LLVMはソース言語の種類に制限を設けるべきではなく、デバッグ情報はどの言語でも動作する必要があります。
コードジェネレーターのサポートにより、LLVMコンパイラーを使用してプログラムをネイティブマシンコードと標準デバッグフォーマットにコンパイルすることが可能になるはずです。 これにより、GDBやDBXのような従来のマシンコードレベルのデバッガーとの互換性が実現します。
LLVM実装で使用されるアプローチは、少数のデバッグレコードを使用して、LLVMプログラムオブジェクトとソースレベルオブジェクト間のマッピングを定義することです。 ソースレベルプログラムの記述は、LLVMメタデータに実装定義のフォーマットで保持されます(C/C++フロントエンドは現在、DWARF 3標準のワーキングドラフト7を使用しています)。
プログラムのデバッグ中は、デバッガーはユーザーと対話し、格納されたデバッグ情報をソース言語固有の情報に変換します。 したがって、デバッガーはソース言語を認識している必要があり、特定の言語または言語ファミリに関連付けられています。
デバッグ情報の利用者¶
デバッグ情報の役割は、通常はコンパイルプロセス中に削除されるメタ情報を提供することです。 このメタ情報は、LLVMユーザーに、生成されたコードと元のプログラムソースコードとの関係を提供します。
現在、デバッグ情報のバックエンドコンシューマーは、DwarfDebugとCodeViewDebugの2つです。 DwarfDebugは、GDB、LLDB、およびその他のDWARFベースのデバッガーで使用できるDWARFを生成します。 CodeViewDebugは、Microsoftのデバッグ情報フォーマットであるCodeViewを生成し、Visual StudioやWinDBGなどのMicrosoftデバッガーで使用できます。 LLVMのデバッグ情報フォーマットは、主にDWARFから派生してインスパイアされていますが、STABSなどの他のターゲットデバッグ情報フォーマットに変換することも可能です。
また、デバッグ情報を使用して、生成されたコードの分析のためのプロファイリングツール、または生成されたコードから元のソースを再構築するためのツールにフィードすることも合理的です。
デバッグ情報と最適化¶
LLVMデバッグ情報の非常に高い優先順位は、最適化と分析とうまく連携させることです。 特に、LLVMデバッグ情報は、以下のことを保証します。
LLVMデバッグ情報は、**実行されたLLVM最適化に関係なく、プログラムのソースレベルの状態を正確に読み取るための情報を常に提供します**。 デバッグ情報の更新方法:LLVMパス作成者向けガイドでは、この保証を破らないように、さまざまな種類のコード変換でデバッグ情報を更新する方法、および可能な限り多くの有用なデバッグ情報を保持する方法について説明しています。 一部の最適化は、プログラム変数の設定や削除された関数の呼び出しなど、デバッガーでプログラムの現在の状態を変更する機能に影響を与える可能性があることに注意してください。 必要に応じて、LLVMの最適化をデバッグ情報を認識するようにアップグレードして、積極的な最適化を実行するときにデバッグ情報を更新できるようにすることができます。 つまり、努力すれば、LLVMオプティマイザはデバッグコードを非デバッグコードと同じくらい最適化できます。
必要に応じて、LLVM最適化はデバッグ情報を認識するようにアップグレードでき、積極的な最適化を実行するときにデバッグ情報を更新できます。つまり、努力すれば、LLVMオプティマイザはデバッグコードを非デバッグコードと同じくらい最適化できます。
LLVMデバッグ情報は、最適化(インライン化、基本ブロックの並べ替え/マージ/クリーンアップ、テール複製など)の発生を妨げません。
LLVMデバッグ情報は、既存の機能を使用して、プログラムの残りの部分と一緒に自動的に最適化されます。たとえば、重複情報はリンカーによって自動的にマージされ、未使用の情報は自動的に削除されます。
基本的に、デバッグ情報を使用すると、「-O0 -g
」でプログラムをコンパイルして完全なデバッグ情報を取得し、デバッガーから実行時にプログラムを任意に変更できます。「-O3 -g
」でプログラムをコンパイルすると、常に利用可能で正確な完全なデバッグ情報が読み取られます(たとえば、テールコールの削除とインライン化にもかかわらず、正確なスタックトレースが得られます)。ただし、プログラムを変更したり、プログラムから最適化された、または完全にインライン化された関数を呼び出す機能が失われる可能性があります。
LLVMテストスイートは、オプティマイザのデバッグ情報の処理をテストするためのフレームワークを提供します。次のように実行できます。
% cd llvm/projects/test-suite/MultiSource/Benchmarks # or some other level
% make TEST=dbgopt
これは、デバッグ情報が最適化パスに与える影響をテストします。デバッグ情報が最適化パスに影響を与える場合、障害として報告されます。LLVMテストインフラストラクチャとさまざまなテストの実行方法の詳細については、LLVMテストインフラストラクチャガイドを参照してください。
デバッグ情報フォーマット¶
LLVMデバッグ情報は、オプティマイザがデバッグ情報について何も知らなくても、プログラムとデバッグ情報を最適化できるように慎重に設計されています。特に、メタデータを使用すると、最初からデバッグ情報の重複が回避され、グローバルデッドコード削除パスは、関数を削除することを決定した場合、その関数のデバッグ情報を自動的に削除します。
これを行うために、ほとんどのデバッグ情報(型、変数、関数、ソースファイルなどの記述子)は、言語フロントエンドによってLLVMメタデータの形式で挿入されます。
デバッグ情報は、ターゲットデバッガーとデバッグ情報表現(DWARF/Stabsなど)に依存しないように設計されています。変数、型、関数、名前空間などを表す情報をデコードするための汎用パスを使用します。これにより、ターゲットデバッガー用に記述されたモジュールが情報を解釈できる限り、任意のソース言語セマンティクスと型システムを使用できます。
基本的な機能を提供するために、LLVMデバッガーはデバッグされているソースレベル言語についていくつかの仮定を行う必要がありますが、これらを最小限に抑えます。LLVMデバッガーが想定する唯一の共通機能は、ソースファイルとプログラムオブジェクトです。これらの抽象オブジェクトは、デバッガーによってスタックトレースの形成、ローカル変数に関する情報の表示など、に使用されます。
ドキュメントのこのセクションでは、最初にソース言語に共通の表現の側面について説明します。C/C++フロントエンド固有のデバッグ情報では、CおよびC++フロントエンドで使用されるデータレイアウト規則について説明します。
デバッグ情報の記述子は、特殊なメタデータノードであり、Metadata
の第一級サブクラスです。
プログラムの異なる状態におけるソース変数の値を定義し、最適化とコード生成を通してこれらの値を追跡するためのモデルは2つあります。1つは現在のデフォルトであるデバッグレコード、もう1つはデフォルトではないが、後方互換性のために現在サポートされている組込み関数呼び出しです。ただし、これら2つのモデルをIRモジュール内で混在させてはなりません。新しいモデルに変更した理由、その仕組み、および古いコードまたはIRを更新してデバッグレコードを使用する方法のガイダンスについては、RemoveDIsドキュメントを参照してください。
デバッグレコード¶
デバッグレコードは、プログラムの実行中にソース変数が持つ値を定義します。デバッグレコードは命令とインターリーブされて表示されますが、それ自体は命令ではなく、コンパイラによって生成されるコードには影響しません。
LLVMは、ソース変数を定義するためにいくつかのタイプのデバッグレコードを使用します。これらのレコードの共通構文は次のとおりです。
#dbg_<kind>([<arg>, ]* <DILocation>)
; Using the intrinsic model, the above is equivalent to:
call void llvm.dbg.<kind>([metadata <arg>, ]*), !dbg <DILocation>
デバッグレコードは、命令と比較して常に1レベル余分なインデントで出力され、常にプレフィックス#dbg_と、callと同様に、カンマ区切りの引数のリストが付きます。
#dbg_declare
¶
#dbg_declare([Value|MDNode], DILocalVariable, DIExpression, DILocation)
このレコードは、ローカル要素(例:変数)に関する情報を提供します。最初の引数は、変数アドレスに対応するSSA値であり、通常は関数エントリブロック内の静的アロカです。2番目の引数は、変数の説明を含むローカル変数です。3番目の引数は、複合式です。4番目の引数は、ソースの場所です。#dbg_declare
レコードは、ソース変数の_アドレス_を記述します。
%i.addr = alloca i32, align 4
#dbg_declare(ptr %i.addr, !1, !DIExpression(), !2)
; ...
!1 = !DILocalVariable(name: "i", ...) ; int i
!2 = !DILocation(...)
; ...
%buffer = alloca [256 x i8], align 8
; The address of i is buffer+64.
#dbg_declare(ptr %buffer, !3, !DIExpression(DW_OP_plus, 64), !4)
; ...
!3 = !DILocalVariable(name: "i", ...) ; int i
!4 = !DILocation(...)
フロントエンドは、ソース変数の宣言時点で、ちょうど1つの#dbg_declare
レコードを生成する必要があります。変数をメモリからSSA値に完全に昇格させる最適化パスは、このレコードを、場合によっては複数の#dbg_value
レコードに置き換えます。ストアを削除するパスは事実上部分的な昇格であり、ソース変数が利用可能な場合にその値を追跡するために、#dbg_value
レコードを組み合わせて挿入します。最適化後、変数がメモリ内に存在するプログラムポイントを記述する複数の#dbg_declare
レコードが存在する場合があります。同じ具体的なソース変数に対するすべての呼び出しは、メモリ位置で一致する必要があります。
#dbg_value
¶
#dbg_value([Value|DIArgList|MDNode], DILocalVariable, DIExpression, DILocation)
このレコードは、ユーザーソース変数が新しい値に設定されたときに情報を提供します。最初の引数は新しい値です。2番目の引数は、変数の説明を含むローカル変数です。3番目の引数は、複合式です。4番目の引数は、ソースの場所です。
#dbg_value
レコードは、ソース変数の_値_を、そのアドレスではなく、直接記述します。この組込み関数の値オペランドは間接的(つまり、ソース変数へのポインタ)である場合がありますが、複合式を解釈することで直接値が導き出されるという条件があります。
#dbg_assign
¶
#dbg_assign( [Value|DIArgList|MDNode] Value,
DILocalVariable Variable,
DIExpression ValueExpression,
DIAssignID ID,
[Value|MDNode] Address,
DIExpression AddressExpression,
DILocation SourceLocation )
このレコードは、ソースの代入が発生したIRの位置をマークします。変数の値をエンコードします。代入を実行するストア(存在する場合)と、代入先アドレスを参照します。
最初の3つの引数は、#dbg_value
の場合と同じです。4番目の引数は、ストアを参照するために使用されるDIAssignID
です。5番目はストアの宛先、6番目はそれを変更する複合式、7番目はソースの場所です。
詳細については、デバッグ情報の代入追跡を参照してください。
デバッガー組込み関数¶
組込みモードでは、LLVMはいくつかの組込み関数(名前のプレフィックスは「llvm.dbg
」)を使用して、最適化とコード生成を通してソースローカル変数を追跡します。これらの組込み関数はそれぞれ上記のデバッグレコードのいずれかに対応しますが、いくつかの構文上の違いがあります。デバッガー組込み関数の各引数はメタデータとしてラップする必要があるため、metadata
というプレフィックスを付ける必要があります。また、各レコードのDILocation
引数は、呼び出し命令のメタデータ添付ファイルである必要があるため、引数リストの後にプレフィックス!dbg
が付いて表示されます。
llvm.dbg.declare
¶
void @llvm.dbg.declare(metadata, metadata, metadata)
この組込み関数は、#dbg_declare
と同等です。
#dbg_declare(i32* %i.addr, !1, !DIExpression(), !2)
call void @llvm.dbg.declare(metadata i32* %i.addr, metadata !1,
metadata !DIExpression()), !dbg !2
llvm.dbg.value
¶
void @llvm.dbg.value(metadata, metadata, metadata)
この組込み関数は、#dbg_value
と同等です。
#dbg_value(i32 %i, !1, !DIExpression(), !2)
call void @llvm.dbg.value(metadata i32 %i, metadata !1,
metadata !DIExpression()), !dbg !2
llvm.dbg.assign
¶
void @llvm.dbg.assign(metadata, metadata, metadata, metadata, metadata, metadata)
この組込み関数は、#dbg_assign
と同等です。
#dbg_assign(i32 %i, !1, !DIExpression(), !2,
ptr %i.addr, !DIExpression(), !3)
call void @llvm.dbg.assign(
metadata i32 %i, metadata !1, metadata !DIExpression(), metadata !2,
metadata ptr %i.addr, metadata !DIExpression(), metadata !3), !dbg !3
オブジェクトのライフタイムとスコープ¶
多くの言語では、関数内のローカル変数のライフタイムまたはスコープは、関数のサブセットに限定される場合があります。たとえば、Cファミリーの言語では、変数は定義されているソースブロック内でのみ有効です(読み書き可能)。関数型言語では、値は定義された後にのみ読み取り可能です。これは非常に明白な概念ですが、LLVMでモデル化するのは簡単ではありません。LLVMにはこの意味でのスコープの概念がなく、言語のスコープ規則に縛られたくないためです。
これを処理するために、LLVMデバッグ形式はllvm命令に添付されたメタデータを使用して、行番号とスコープ情報をエンコードします。たとえば、次のCのフラグメントを考えてみましょう。
1. void foo() {
2. int X = 21;
3. int Y = 22;
4. {
5. int Z = 23;
6. Z = X;
7. }
8. X = Y;
9. }
LLVMにコンパイルすると、この関数は次のように表されます。
; Function Attrs: nounwind ssp uwtable
define void @foo() #0 !dbg !4 {
entry:
%X = alloca i32, align 4
%Y = alloca i32, align 4
%Z = alloca i32, align 4
#dbg_declare(ptr %X, !11, !DIExpression(), !13)
store i32 21, i32* %X, align 4, !dbg !13
#dbg_declare(ptr %Y, !14, !DIExpression(), !15)
store i32 22, i32* %Y, align 4, !dbg !15
#dbg_declare(ptr %Z, !16, !DIExpression(), !18)
store i32 23, i32* %Z, align 4, !dbg !18
%0 = load i32, i32* %X, align 4, !dbg !20
store i32 %0, i32* %Z, align 4, !dbg !21
%1 = load i32, i32* %Y, align 4, !dbg !22
store i32 %1, i32* %X, align 4, !dbg !23
ret void, !dbg !24
}
attributes #0 = { nounwind ssp uwtable "less-precise-fpmad"="false" "frame-pointer"="all" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nounwind readnone }
!llvm.dbg.cu = !{!0}
!llvm.module.flags = !{!7, !8, !9}
!llvm.ident = !{!10}
!0 = !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang version 3.7.0 (trunk 231150) (llvm/trunk 231154)", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, enums: !2, retainedTypes: !2, subprograms: !3, globals: !2, imports: !2)
!1 = !DIFile(filename: "/dev/stdin", directory: "/Users/dexonsmith/data/llvm/debug-info")
!2 = !{}
!3 = !{!4}
!4 = distinct !DISubprogram(name: "foo", scope: !1, file: !1, line: 1, type: !5, isLocal: false, isDefinition: true, scopeLine: 1, isOptimized: false, retainedNodes: !2)
!5 = !DISubroutineType(types: !6)
!6 = !{null}
!7 = !{i32 2, !"Dwarf Version", i32 2}
!8 = !{i32 2, !"Debug Info Version", i32 3}
!9 = !{i32 1, !"PIC Level", i32 2}
!10 = !{!"clang version 3.7.0 (trunk 231150) (llvm/trunk 231154)"}
!11 = !DILocalVariable(name: "X", scope: !4, file: !1, line: 2, type: !12)
!12 = !DIBasicType(name: "int", size: 32, align: 32, encoding: DW_ATE_signed)
!13 = !DILocation(line: 2, column: 9, scope: !4)
!14 = !DILocalVariable(name: "Y", scope: !4, file: !1, line: 3, type: !12)
!15 = !DILocation(line: 3, column: 9, scope: !4)
!16 = !DILocalVariable(name: "Z", scope: !18, file: !1, line: 5, type: !12)
!17 = distinct !DILexicalBlock(scope: !4, file: !1, line: 4, column: 5)
!18 = !DILocation(line: 5, column: 11, scope: !17)
!29 = !DILocation(line: 6, column: 11, scope: !17)
!20 = !DILocation(line: 6, column: 9, scope: !17)
!21 = !DILocation(line: 8, column: 9, scope: !4)
!22 = !DILocation(line: 8, column: 7, scope: !4)
!23 = !DILocation(line: 9, column: 3, scope: !4)
この例は、LLVMデバッグ情報に関するいくつかの重要な詳細を示しています。特に、命令に添付されている#dbg_declare
レコードと位置情報がどのように連携して適用され、デバッガーがステートメント、変数定義、および関数の実装に使用されるコード間の関係を分析できるかを示しています。
#dbg_declare(ptr %X, !11, !DIExpression(), !13)
; [debug line = 2:9] [debug variable = X]
最初のレコード#dbg_declare
は、変数X
のデバッグ情報をエンコードします。レコードの末尾にある場所!13
は、変数X
のスコープ情報を提供します。
!13 = !DILocation(line: 2, column: 9, scope: !4)
!4 = distinct !DISubprogram(name: "foo", scope: !1, file: !1, line: 1, type: !5,
isLocal: false, isDefinition: true, scopeLine: 1,
isOptimized: false, retainedNodes: !2)
ここで、!13
は位置情報を提供するメタデータです。この例では、スコープは!4
、サブルーチン記述子によってエンコードされます。このように、レコードの位置情報パラメータは、変数X
が関数foo
の関数レベルスコープの行番号2で宣言されていることを示します。
では、別の例を見てみましょう。
#dbg_declare(ptr %Z, !16, !DIExpression(), !18)
; [debug line = 5:11] [debug variable = Z]
3番目のレコード#dbg_declare
は、変数Z
のデバッグ情報をエンコードします。レコードの末尾にあるメタデータ!18
は、変数Z
のスコープ情報を提供します。
!17 = distinct !DILexicalBlock(scope: !4, file: !1, line: 4, column: 5)
!18 = !DILocation(line: 5, column: 11, scope: !17)
ここで、!18
は、Z
が行番号5、列番号11で、字句スコープ!17
内で宣言されていることを示します。字句スコープ自体は、上記で説明したサブルーチン!4
内に存在します。
各命令に添付されているスコープ情報は、スコープでカバーされている命令を見つけるための簡単な方法を提供します。
最適化されたコードにおけるオブジェクトのライフタイム¶
上記の例では、すべての変数代入は、スタック上の変数の位置へのメモリ格納に一意に対応しています。ただし、高度に最適化されたコードでは、LLVMはほとんどの変数をSSA値に昇格させます。SSA値は最終的に物理レジスタまたはメモリ位置に配置される可能性があります。コンパイルを通してSSA値を追跡するために、オブジェクトがSSA値に昇格されると、各代入に対して#dbg_value
レコードが作成され、変数の新しい場所が記録されます。#dbg_declare
レコードと比較して
#dbg_value は、指定された変数の(重複するフラグメントの)先行する #dbg_value の効果を終了させます。
IR 内の #dbg_value の位置は、命令ストリーム内のどこで変数の値が変化するかを定義します。
オペランドは定数にすることができ、変数に定数値が割り当てられていることを示します。
最適化パスが命令とブロックを変更または移動する場合、#dbg_value
レコードを更新するように注意する必要があります。開発者は、プログラムのデバッグ時に、変数の値に反映されたそのような変更を観察する可能性があります。最適化されたプログラムのいかなる実行においても、デバッガーによって開発者に提示される変数の値のセットは、最適化されていないプログラムの実行において、同じ入力が与えられた場合に決して存在しなかった状態を示すべきではありません。そうすることで、存在しない状態を報告することにより、開発者を誤解させ、最適化されたプログラムの理解を損ない、デバッガーに対する信頼を損なう危険性があります。
場合によっては、変数の場所を完全に保持することが不可能な場合があります。多くの場合、冗長な計算が最適化で除外される場合です。このような場合、オペランドpoison
を持つ#dbg_value
を使用して、以前の変数の場所を終了し、デバッガーがoptimized out
を開発者に提示できるようにする必要があります。これらの潜在的に古い変数値を開発者から差し控えることで、利用可能なデバッグ情報の量は減少しますが、残りの情報の信頼性は向上します。
潜在的な問題を説明するために、次の例を考えてみましょう。
define i32 @foo(i32 %bar, i1 %cond) {
entry:
#dbg_value(i32 0, !1, !DIExpression(), !4)
br i1 %cond, label %truebr, label %falsebr
truebr:
%tval = add i32 %bar, 1
#dbg_value(i32 %tval, !1, !DIExpression(), !4)
%g1 = call i32 @gazonk()
br label %exit
falsebr:
%fval = add i32 %bar, 2
#dbg_value(i32 %fval, !1, !DIExpression(), !4)
%g2 = call i32 @gazonk()
br label %exit
exit:
%merge = phi [ %tval, %truebr ], [ %fval, %falsebr ]
%g = phi [ %g1, %truebr ], [ %g2, %falsebr ]
#dbg_value(i32 %merge, !1, !DIExpression(), !4)
#dbg_value(i32 %g, !3, !DIExpression(), !4)
%plusten = add i32 %merge, 10
%toret = add i32 %plusten, %g
#dbg_value(i32 %toret, !1, !DIExpression(), !4)
ret i32 %toret
}
!1
と!3
に2つのソースレベル変数が含まれています。関数は、おそらく次のコードに最適化される可能性があります。
define i32 @foo(i32 %bar, i1 %cond) {
entry:
%g = call i32 @gazonk()
%addoper = select i1 %cond, i32 11, i32 12
%plusten = add i32 %bar, %addoper
%toret = add i32 %plusten, %g
ret i32 %toret
}
このコードの元々の変数位置を表すために、どのような#dbg_value
レコードを配置する必要がありますか?残念ながら、ソース関数における!1
の2番目、3番目、4番目の#dbg_valuesのオペランド(%tval、%fval、%merge)は最適化によって削除されています。これらを復元できないと仮定すると、#dbg_valuesをこのように配置することを検討できます。
define i32 @foo(i32 %bar, i1 %cond) {
entry:
#dbg_value(i32 0, !1, !DIExpression(), !4)
%g = call i32 @gazonk()
#dbg_value(i32 %g, !3, !DIExpression(), !4)
%addoper = select i1 %cond, i32 11, i32 12
%plusten = add i32 %bar, %addoper
%toret = add i32 %plusten, %g
#dbg_value(i32 %toret, !1, !DIExpression(), !4)
ret i32 %toret
}
しかし、これは!3
が@gazonk()
の戻り値を持ち、同時に!1
が定数値ゼロを持つという状況を引き起こします。これは、最適化されていないプログラムでは決して発生しない代入のペアです。これを回避するには、!3
の#dbg_valueの前にpoison #dbg_valueを挿入することで、!1
が定数値代入を持つ範囲を終了させる必要があります。
define i32 @foo(i32 %bar, i1 %cond) {
entry:
#dbg_value(i32 0, !1, !DIExpression(), !2)
%g = call i32 @gazonk()
#dbg_value(i32 poison, !1, !DIExpression(), !2)
#dbg_value(i32 %g, !3, !DIExpression(), !2)
%addoper = select i1 %cond, i32 11, i32 12
%plusten = add i32 %bar, %addoper
%toret = add i32 %plusten, %g
#dbg_value(i32 %toret, !1, !DIExpression(), !2)
ret i32 %toret
}
新しい位置を追加することなく、支配的な位置定義を終了させることを意味する#dbg_valueの設定が他にもいくつかあります。完全なリストは以下のとおりです。
任意の場所オペランドが
poison
(またはundef
)である。任意の場所オペランドが空のメタデータタプル(
!{}
)である(!DIArgList
では発生しない)。場所オペランドがなく(空の
DIArgList
)、DIExpression
が空である。
変数の位置をkillするこのクラスの#dbg_valueは、「kill #dbg_value」または「kill location」と呼ばれ、従来の理由から、既存のコードでは「undef #dbg_value」という用語が使用される場合があります。 #dbg_valueがkill locationであるかどうかをチェックまたは設定するために場所オペランドを直接検査するのではなく、可能な場合はDbgVariableIntrinsic
メソッドのisKillLocation
およびsetKillLocation
を使用する必要があります。
一般に、#dbg_valueのオペランドが最適化によって削除され、復元できない場合は、以前の変数の位置を終了させるためにkill #dbg_valueが必要です。デバッガが代入の順序変更を観察できる場合は、追加のkill #dbg_valueが必要になる場合があります。
CodeGen中に変数位置メタデータがどのように変換されるか¶
LLVMは、中間レベルおよびバックエンドのパス全体でデバッグ情報を保持し、最終的にソースレベルの情報と命令範囲間のマッピングを生成します。命令を行番号にマッピングすることは単純な関連付けであるため、行番号情報に関しては比較的簡単です。しかし、変数の位置については、話はより複雑です。各#dbg_value
レコードは、ソース変数への値のソースレベルの代入を表すため、デバッグレコードは事実上、LLVM IR内に小さな命令型プログラムを埋め込みます。CodeGenの終わりまでに、これは各変数から命令範囲にわたるそれらのマシン位置へのマッピングになります。IRからオブジェクトの出力まで、変数の位置の忠実度に影響を与える主な変換は次のとおりです。
命令選択
レジスタ割り当て
ブロックレイアウト
それぞれについて以下で説明します。さらに、命令スケジューリングはプログラムの順序を大きく変更する可能性があり、さまざまなパスで発生します。
CodeGen中に変換されない変数の位置もあります。#dbg_declare
によって指定されたスタックの場所は、関数の期間全体にわたって有効かつ不変であり、単純なMachineFunctionテーブルに記録されます。関数のプロローグとエピローグにおける場所の変更も無視されます。フレームのセットアップと破棄には、複数の命令が必要になる場合があり、出力バイナリに記述するには不釣り合いな量のデバッグ情報が必要になり、デバッガはとにかくステップオーバーする必要があります。
命令選択とMIRにおける変数の位置¶
命令選択はIR関数からMIR関数を生成し、intermediate
命令をマシン命令に変換するのと同じように、intermediate
変数の位置をマシンの変数の位置に変換する必要があります。IR内では、変数の位置は常にValueによって識別されますが、MIRでは、さまざまなタイプの変数の位置が存在する可能性があります。さらに、一部のIRの位置は利用できなくなります。たとえば、複数のIR命令の操作が1つのマシン命令(乗算と累算など)に結合されると、中間値が失われます。命令選択を通じて変数の位置を追跡するために、まずコード生成に依存しない位置(定数、スタックの位置、割り当てられた仮想レジスタ)と依存する位置に分けられます。依存する位置については、デバッグメタデータがSelectionDAGのSDNodeに添付されます。命令選択が行われ、MIR関数が作成された後、デバッグメタデータに関連付けられたSDNodeに仮想レジスタが割り当てられている場合、その仮想レジスタが変数の位置として使用されます。SDNodeがマシン命令に折りたたまれるか、そうでなければレジスタ以外に変換される場合、変数の位置は利用できなくなります.
利用できない場所は、最適化によって削除されたかのように扱われます。IRでは、場所はデバッグレコードによってundef
に割り当てられ、MIRでは同等の場所が使用されます。
MIRの位置が各変数に割り当てられた後、各#dbg_value
レコードに対応するマシンの疑似命令が挿入されます。このタイプの命令には2つの形式があります。
最初の形式であるDBG_VALUE
は、次のようになります。
DBG_VALUE %1, $noreg, !123, !DIExpression()
- そして、以下のオペランドを持ちます。
最初のオペランドは、元のデバッグレコードがメモリを参照していた場合、レジスタ、フレームインデックス、イミディエイト、またはベースアドレスレジスタとして変数の位置を記録できます。
$noreg
は、変数の位置が未定義であることを示し、undef
#dbg_valueオペランドと同等です。2番目のオペランドのタイプは、変数の位置がDBG_VALUEによって直接参照されるか、間接的に参照されるかを示します。
$noreg
レジスタは前者を示し、イミディエイトオペランド(0)は後者を示します。オペランド3は、元のデバッグレコードのVariableフィールドです。
オペランド4は、元のデバッグレコードのExpressionフィールドです。
2番目の形式であるDBG_VALUE_LIST
は、次のようになります。
DBG_VALUE_LIST !123, !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_arg, 1, DW_OP_plus), %1, %2
- そして、以下のオペランドを持ちます。
最初のオペランドは、元のデバッグレコードのVariableフィールドです。
2番目のオペランドは、元のデバッグレコードのExpressionフィールドです。
3番目以降の任意の数のオペランドは、変数の位置オペランドのシーケンスを記録します。これらのオペランドは、上記の
DBG_VALUE
命令の最初のオペランドと同じ値をとることができます。これらの変数の位置オペランドは、DIExpressionのDW_OP_LLVM_arg演算子によって示される位置に最終的なDWARF式に挿入されます。
DBG_VALUEが挿入される位置は、IRブロック内の対応する#dbg_value
レコードの位置に対応する必要があります。最適化と同様に、LLVMはソースプログラムで発生した変数代入の順序を保持することを目指しています。ただし、SelectionDAGは命令スケジューリングを実行するため、代入の順序が変更される可能性があります(以下で説明します)。関数パラメータの位置は、関数の先頭にない場合は関数の先頭に移動され、関数の開始時にすぐに使用できるようにします。
命令選択中の変数の位置を示すために、次の例を考えてみましょう。
define i32 @foo(i32* %addr) {
entry:
#dbg_value(i32 0, !3, !DIExpression(), !5)
br label %bb1, !dbg !5
bb1: ; preds = %bb1, %entry
%bar.0 = phi i32 [ 0, %entry ], [ %add, %bb1 ]
#dbg_value(i32 %bar.0, !3, !DIExpression(), !5)
%addr1 = getelementptr i32, i32 *%addr, i32 1, !dbg !5
#dbg_value(i32 *%addr1, !3, !DIExpression(), !5)
%loaded1 = load i32, i32* %addr1, !dbg !5
%addr2 = getelementptr i32, i32 *%addr, i32 %bar.0, !dbg !5
#dbg_value(i32 *%addr2, !3, !DIExpression(), !5)
%loaded2 = load i32, i32* %addr2, !dbg !5
%add = add i32 %bar.0, 1, !dbg !5
#dbg_value(i32 %add, !3, !DIExpression(), !5)
%added = add i32 %loaded1, %loaded2
%cond = icmp ult i32 %added, %bar.0, !dbg !5
br i1 %cond, label %bb1, label %bb2, !dbg !5
bb2: ; preds = %bb1
ret i32 0, !dbg !5
}
このIRをllc -o - -start-after=codegen-prepare -stop-after=expand-isel-pseudos -mtriple=x86_64--
でコンパイルすると、次のMIRが生成されます。
bb.0.entry:
successors: %bb.1(0x80000000)
liveins: $rdi
%2:gr64 = COPY $rdi
%3:gr32 = MOV32r0 implicit-def dead $eflags
DBG_VALUE 0, $noreg, !3, !DIExpression(), debug-location !5
bb.1.bb1:
successors: %bb.1(0x7c000000), %bb.2(0x04000000)
%0:gr32 = PHI %3, %bb.0, %1, %bb.1
DBG_VALUE %0, $noreg, !3, !DIExpression(), debug-location !5
DBG_VALUE %2, $noreg, !3, !DIExpression(DW_OP_plus_uconst, 4, DW_OP_stack_value), debug-location !5
%4:gr32 = MOV32rm %2, 1, $noreg, 4, $noreg, debug-location !5 :: (load 4 from %ir.addr1)
%5:gr64_nosp = MOVSX64rr32 %0, debug-location !5
DBG_VALUE $noreg, $noreg, !3, !DIExpression(), debug-location !5
%1:gr32 = INC32r %0, implicit-def dead $eflags, debug-location !5
DBG_VALUE %1, $noreg, !3, !DIExpression(), debug-location !5
%6:gr32 = ADD32rm %4, %2, 4, killed %5, 0, $noreg, implicit-def dead $eflags :: (load 4 from %ir.addr2)
%7:gr32 = SUB32rr %6, %0, implicit-def $eflags, debug-location !5
JB_1 %bb.1, implicit $eflags, debug-location !5
JMP_1 %bb.2, debug-location !5
bb.2.bb2:
%8:gr32 = MOV32r0 implicit-def dead $eflags
$eax = COPY %8, debug-location !5
RET 0, $eax, debug-location !5
まず、ソースIRのすべての#dbg_value
レコードに対応するDBG_VALUE命令があり、ソースレベルの代入が失われないようになっていることに注意してください。次に、変数の位置が記録されるさまざまな方法を考えてみましょう。
最初の#dbg_valueでは、イミディエイトオペランドを使用してゼロ値が記録されます。
PHI命令の#dbg_valueは、仮想レジスタ
%0
のDBG_VALUEになります。最初のGEPの効果は最初のロード命令に(4バイトのオフセットとして)折りたたまれますが、変数の位置はGEPの効果をDIExpressionに折りたたむことによって救済されます。
2番目のGEPも対応するロードに折りたたまれます。ただし、救済するには単純すぎて、
$noreg
DBG_VALUEとして出力され、変数が未定義の位置をとることを示します。最後の#dbg_valueは、その値が仮想レジスタ
%1
に配置されます。
命令スケジューリング¶
多くのパスが命令を再スケジューリングできます。特に、命令選択とpre-and-post RAマシンスケジューラです。命令スケジューリングはプログラムの性質を大きく変える可能性があります。(非常にまれな)最悪の場合、命令シーケンスが完全に逆になる可能性があります。このような状況では、LLVMは最適化に適用される原則、つまりデバッガが誤解を招く状態よりも状態を表示しない方が良いという原則に従います。したがって、命令が実行順に進むたびに、対応するDBG_VALUEは元の位置に保持され、命令が遅延された場合、変数には遅延の期間、未定義の位置が与えられます。説明のために、この疑似MIRを考えてみましょう。
%1:gr32 = MOV32rm %0, 1, $noreg, 4, $noreg, debug-location !5 :: (load 4 from %ir.addr1)
DBG_VALUE %1, $noreg, !1, !2
%4:gr32 = ADD32rr %3, %2, implicit-def dead $eflags
DBG_VALUE %4, $noreg, !3, !4
%7:gr32 = SUB32rr %6, %5, implicit-def dead $eflags
DBG_VALUE %7, $noreg, !5, !6
SUB32rrが前に移動されて、次のMIRが得られたとします。
%7:gr32 = SUB32rr %6, %5, implicit-def dead $eflags
%1:gr32 = MOV32rm %0, 1, $noreg, 4, $noreg, debug-location !5 :: (load 4 from %ir.addr1)
DBG_VALUE %1, $noreg, !1, !2
%4:gr32 = ADD32rr %3, %2, implicit-def dead $eflags
DBG_VALUE %4, $noreg, !3, !4
DBG_VALUE %7, $noreg, !5, !6
この状況では、LLVMは上記のようにMIRを残します。仮想レジスタ%7のDBG_VALUEをSUB32rrと一緒に上に移動すると、代入の順序が変更され、プログラムの新しい状態が導入されます。一方、上記のソリューションでは、!3
と!5
の値が同時に変更されるため、デバッガは変数値の組み合わせが1つ少なくなります。これは、元のプログラムを誤って表現するよりも推奨されます。
比較として、MOV32rmを沈めた場合、LLVMは以下を生成します。
DBG_VALUE $noreg, $noreg, !1, !2
%4:gr32 = ADD32rr %3, %2, implicit-def dead $eflags
DBG_VALUE %4, $noreg, !3, !4
%7:gr32 = SUB32rr %6, %5, implicit-def dead $eflags
DBG_VALUE %7, $noreg, !5, !6
%1:gr32 = MOV32rm %0, 1, $noreg, 4, $noreg, debug-location !5 :: (load 4 from %ir.addr1)
DBG_VALUE %1, $noreg, !1, !2
ここでは、!1
への最初の代入が消える状態を提示しないようにするために、ブロックの先頭にあるDBG_VALUEは、値がブロックの最後で利用可能になるまで、変数に未定義の位置を割り当てます。追加のDBG_VALUEが追加されます。MOV32rmが沈んだ命令に!1
の他のDBG_VALUEが発生した場合、%1
のDBG_VALUEは削除され、デバッガは変数でそれを観察しません。これは、元のプログラムの対応する部分で値が使用できないことを正確に反映しています。
レジスタ割り当て中の変数の位置¶
デバッグ命令がレジスタ割り当てに干渉するのを避けるため、LiveDebugVariablesパスはMIR関数から変数位置を抽出し、対応するDBG_VALUE命令を削除します。ブロック内では、いくつかの局所的なコピー伝播が実行されます。レジスタ割り当て後、VirtRegRewriterパスはDBG_VALUE命令を元の位置に再挿入し、仮想レジスタ参照を物理マシン位置に変換します。正しくない変数位置をエンコードしないように、このパスでは、ライブでない仮想レジスタのDBG_VALUEは未定義の位置に置き換えられます。LiveDebugVariablesは、仮想レジスタの書き換えにより、冗長なDBG_VALUEを挿入する場合があります。これらは、RemoveRedundantDebugValuesパスによって後で削除されます。
変数位置のLiveDebugValues展開¶
すべての最適化が実行され、エミッションの直前に、LiveDebugValuesパスは2つの目的を達成するために実行されます。
コピーとレジスタスピルを介して変数の位置を伝播すること、
すべてのブロックに対して、そのブロック内のすべての有効な変数位置を記録すること。
このパスを実行した後、DBG_VALUE命令の意味が変わります。変数の値が変更される可能性のあるソースレベルの割り当てに対応するのではなく、ブロック内の変数の位置をアサートし、ブロック外では効果がなくなります。コピーとスピルを介した変数位置の伝播は簡単です。すべての基本ブロック内の変数位置を決定するには、制御フローを考慮する必要があります。いくつかの問題を示す次のIRを考えてみましょう。
define dso_local i32 @foo(i1 %cond, i32 %input) !dbg !12 {
entry:
br i1 %cond, label %truebr, label %falsebr
bb1:
%value = phi i32 [ %value1, %truebr ], [ %value2, %falsebr ]
br label %exit, !dbg !26
truebr:
#dbg_value(i32 %input, !30, !DIExpression(), !24)
#dbg_value(i32 1, !23, !DIExpression(), !24)
%value1 = add i32 %input, 1
br label %bb1
falsebr:
#dbg_value(i32 %input, !30, !DIExpression(), !24)
#dbg_value(i32 2, !23, !DIExpression(), !24)
%value2 = add i32 %input, 2
br label %bb1
exit:
ret i32 %value, !dbg !30
}
ここでの問題は次のとおりです。
制御フローは、基本ブロックの順序とほぼ逆です。
!23
変数の値は%bb1
にマージされますが、PHIノードはありません。
上記のように、#dbg_value
レコードは、本質的にIRに埋め込まれた命令型プログラムを形成し、各レコードは変数位置を定義します。これは、IR値の制御フローマージを識別し、phiノードを挿入するために使用定義チェーンを使用するのと同じ方法で、mem2regによってSSA形式に変換できます。ただし、デバッグ変数位置はすべてのマシン命令に対して定義されているため、実際にはすべてのIR命令がすべての変数位置を使用するため、多数のデバッグレコードが生成されます。
上記の例を調べると、変数!30
には、関数の両方の条件付きパスで%input
が割り当てられますが、!23
には、いずれかのパスで異なる定数値が割り当てられます。%bb1
で制御フローがマージされる場合、!30
はその位置(%input
)を保持しますが、!23
は、PHIノードを挿入せずに実行時に%bb1で持つべき値を判断できないため、未定義になります。mem2regは、デバッグが有効になっているときにcodegenを変更しないようにPHIノードを挿入せず、非常に多数のレコードを追加しないように他の#dbg_valuesを挿入しません。
代わりに、LiveDebugValuesは、制御フローがマージされるときに変数位置を決定します。データフロー分析を使用して、ブロック間で位置を伝播します。制御フローがマージされるときに、変数がすべての先行ブロックで同じ位置にある場合、その位置は後続ブロックに伝播されます。先行ブロックの位置が一致しない場合、位置は未定義になります。
LiveDebugValuesが実行されると、すべてのブロックには、ブロック内のDBG_VALUE命令によって記述されたすべての有効な変数位置が含まれている必要があります。その後、サポートクラス(DbgEntityHistoryCalculatorなど)は、制御フローを考慮することなく、各命令からすべての有効な変数位置へのマップを構築するために、非常に少ない労力で済みます。上記の例から、そうでなければ変数!30
の位置がブロック%bb1
に「上」に流れるはずであると判断することは困難ですが、変数!23
の位置は%exit
ブロックに「下」に流れてはいけません。
C/C++フロントエンド固有のデバッグ情報¶
CおよびC++フロントエンドは、情報の内容に関してDWARFと事実上同一の形式でプログラムに関する情報を表します。これにより、コードジェネレータは標準のdwarf情報を生成することでネイティブデバッガを簡単にサポートでき、非dwarfターゲットが必要に応じて変換できる十分な情報が含まれています。
このセクションでは、CおよびC++プログラムを表すために使用される形式について説明します。他の言語は、これ(DWARFと同じ方法でプログラムを表すように調整されています)をモデルにすることも、DWARFモデルに適合しない場合は完全に異なる形式を提供することもできます。LLVMソース言語フロントエンドにデバッグ情報のサポートが追加されると、使用される情報はここに記載する必要があります。
以下のセクションでは、いくつかのC/C++コンストラクトの例と、それらのコンストラクトを最もよく説明するデバッグ情報を示します。正規の参考文献は、include/llvm/IR/DebugInfoMetadata.h
で定義されているDINode
クラスと、lib/IR/DIBuilder.cpp
のヘルパー関数の 実装です。
C/C++ソースファイル情報¶
llvm::Instruction
は、命令に添付されたメタデータへの簡単なアクセスを提供します。Instruction::getDebugLoc()
とDILocation::getLine()
を使用して、LLVM IRでエンコードされた行番号情報を抽出できます。
if (DILocation *Loc = I->getDebugLoc()) { // Here I is an LLVM instruction
unsigned Line = Loc->getLine();
StringRef File = Loc->getFilename();
StringRef Dir = Loc->getDirectory();
bool ImplicitCode = Loc->isImplicitCode();
}
フラグImplicitCodeがtrueの場合、命令はフロントエンドによって追加されましたが、ユーザーが記述したソースコードに対応していないことを意味します。例えば
if (MyBoolean) {
MyObject MO;
...
}
スコープの終わりにMyObjectのデストラクタが呼び出されますが、明示的には記述されていません。この情報は、コードカバレッジを作成する際に括弧にカウンターを付けることを避けるために役立ちます。
C/C++グローバル変数情報¶
次のように宣言された整数グローバル変数を考えると
_Alignas(8) int MyGlobal = 100;
C/C++フロントエンドは、次の記述子を生成します。
;;
;; Define the global itself.
;;
@MyGlobal = global i32 100, align 8, !dbg !0
;;
;; List of debug info of globals
;;
!llvm.dbg.cu = !{!1}
;; Some unrelated metadata.
!llvm.module.flags = !{!6, !7}
!llvm.ident = !{!8}
;; Define the global variable itself
!0 = distinct !DIGlobalVariable(name: "MyGlobal", scope: !1, file: !2, line: 1, type: !5, isLocal: false, isDefinition: true, align: 64)
;; Define the compile unit.
!1 = distinct !DICompileUnit(language: DW_LANG_C99, file: !2,
producer: "clang version 4.0.0",
isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug,
enums: !3, globals: !4)
;;
;; Define the file
;;
!2 = !DIFile(filename: "/dev/stdin",
directory: "/Users/dexonsmith/data/llvm/debug-info")
;; An empty array.
!3 = !{}
;; The Array of Global Variables
!4 = !{!0}
;;
;; Define the type
;;
!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
;; Dwarf version to output.
!6 = !{i32 2, !"Dwarf Version", i32 4}
;; Debug info schema version.
!7 = !{i32 2, !"Debug Info Version", i32 3}
;; Compiler identification
!8 = !{!"clang version 4.0.0"}
DIGlobalVariable記述のalign値は、C11 _Alignas()、C++ 11 alignas()キーワード、またはコンパイラ属性__attribute__((aligned()))によって強制された場合の変数のアライメントを指定します。それ以外の場合(このフィールドがない場合)、アライメントはデフォルトと見なされます。これは、DW_AT_alignment値のDWARF出力を生成するときに使用されます。
C/C++関数情報¶
次のように宣言された関数を考えると
int main(int argc, char *argv[]) {
return 0;
}
C/C++フロントエンドは、次の記述子を生成します。
;;
;; Define the anchor for subprograms.
;;
!4 = !DISubprogram(name: "main", scope: !1, file: !1, line: 1, type: !5,
isLocal: false, isDefinition: true, scopeLine: 1,
flags: DIFlagPrototyped, isOptimized: false,
retainedNodes: !2)
;;
;; Define the subprogram itself.
;;
define i32 @main(i32 %argc, i8** %argv) !dbg !4 {
...
}
C++固有のデバッグ情報¶
C++特殊メンバ関数情報¶
DWARF v5では、C++プログラムのデバッグ情報を強化するために定義された属性が導入されています。LLVMは、これらの適切なDWARF属性を生成(または省略)できます。C++では、特殊メンバ関数Ctors、Dtors、Copy/ Move Ctors、代入演算子は、C++ 11キーワードdeletedで宣言できます。これは、LLVMではspFlags値DISPFlagDeletedを使用して表されます。
コピーコンストラクタがdeletedとして宣言されたクラス宣言があるとします。
class foo {
public:
foo(const foo&) = deleted;
};
C++フロントエンドは以下を生成します。
!17 = !DISubprogram(name: "foo", scope: !11, file: !1, line: 5, type: !18, scopeLine: 5, flags: DIFlagPublic | DIFlagPrototyped, spFlags: DISPFlagDeleted)
そして、これは追加のDWARF属性を次のように生成します。
DW_TAG_subprogram [7] *
DW_AT_name [DW_FORM_strx1] (indexed (00000006) string = "foo")
DW_AT_decl_line [DW_FORM_data1] (5)
...
DW_AT_deleted [DW_FORM_flag_present] (true)
Fortran固有のデバッグ情報¶
Fortran関数情報¶
Fortranプログラムのクライアントデバッグをサポートするために定義されているDWARF属性がいくつかあります。LLVMは、ELEMENTAL、PURE、IMPURE、RECURSIVE、およびNON_RECURSIVEのプレフィックス仕様に適切なDWARF属性を生成(または省略)できます。これは、spFlags値:DISPFlagElemental、DISPFlagPure、およびDISPFlagRecursiveを使用することによって行われます。
elemental function elem_func(a)
Fortranフロントエンドは、次の記述子を生成します。
!11 = distinct !DISubprogram(name: "subroutine2", scope: !1, file: !1,
line: 5, type: !8, scopeLine: 6,
spFlags: DISPFlagDefinition | DISPFlagElemental, unit: !0,
retainedNodes: !2)
そして、これは追加のDWARF属性を次のように具体化します。
DW_TAG_subprogram [3]
DW_AT_low_pc [DW_FORM_addr] (0x0000000000000010 ".text")
DW_AT_high_pc [DW_FORM_data4] (0x00000001)
...
DW_AT_elemental [DW_FORM_flag_present] (true)
Fortran固有のコンストラクト、つまりFortran character(n)を表すDW_TAG_string_typeを表すために定義されているDWARFタグがいくつかあります。LLVMでは、これはDIStringTypeとして表されます。
character(len=*), intent(in) :: string
Fortranフロントエンドは、次の記述子を生成します。
!DILocalVariable(name: "string", arg: 1, scope: !10, file: !3, line: 4, type: !15)
!DIStringType(name: "character(*)!2", stringLength: !16, stringLengthExpression: !DIExpression(), size: 32)
Fortranの遅延長文字は、文字列の長さに加えて、文字の生のストレージの情報も含めることができます。この情報は、stringLocationExpressionフィールドにエンコードされています。この情報に基づいて、DW_AT_data_location属性がDW_TAG_string_typeデバッグ情報で出力されます。
!DIStringType(name:“character(*)!2″、stringLengthExpression:!DIExpression()、stringLocationExpression:!DIExpression(DW_OP_push_object_address、DW_OP_deref)、size:32)
そして、これはDWARFタグで次のように具体化されます。
DW_TAG_string_type
DW_AT_name ("character(*)!2")
DW_AT_string_length (0x00000064)
0x00000064: DW_TAG_variable
DW_AT_location (DW_OP_fbreg +16)
DW_AT_type (0x00000083 "integer*8")
DW_AT_data_location (DW_OP_push_object_address, DW_OP_deref)
...
DW_AT_artificial (true)
Fortranフロントエンドは、別のコンパイル単位で定義された関数を呼び出すために、*トランプ*関数を生成する必要がある場合があります。この場合、フロントエンドはトランプ関数に対して次の記述子を出力できます。
!DISubprogram(name: "sub1_.t0p", linkageName: "sub1_.t0p", scope: !4, file: !4, type: !5, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition, unit: !7, retainedNodes: !24, targetFuncName: "sub1_")
targetFuncNameフィールドは、トランプが呼び出す関数の名前です。この記述子は、次のDWARFタグになります。
DW_TAG_subprogram
...
DW_AT_linkage_name ("sub1_.t0p")
DW_AT_name ("sub1_.t0p")
DW_AT_trampoline ("sub1_")
デバッグ情報形式¶
Objective Cプロパティのデバッグ情報拡張¶
はじめに¶
Objective Cは、宣言されたプロパティを使用してアクセサメソッドを宣言および定義する簡単な方法を提供します。この言語は、プロパティを宣言し、コンパイラにアクセサメソッドを合成させる機能を提供します。
デバッガを使用すると、開発者はObjective Cインターフェースとそのインスタンス変数とクラス変数を検査できます。ただし、デバッガはObjective Cインターフェースで定義されているプロパティについては何も知りません。デバッガは、コンパイラによってDWARF形式で生成された情報を消費します。この形式は、Objective Cプロパティのエンコードをサポートしていません。この提案では、デバッガが開発者によるObjective Cプロパティの検査を可能にするために使用できる、Objective CプロパティをエンコードするためのDWARF拡張について説明します。
提案¶
Objective Cのプロパティは、クラスメンバとは別に存在します。プロパティは、「セッター」と「ゲッター」セレクタのみで定義でき、アクセスするたびに新たに計算できます。または、プロパティは、宣言されたivarへの直接アクセスとなることもあります。最後に、コンパイラによってivarを「自動的に合成」させることもできます。この場合、プロパティは、標準のC逆参照構文とプロパティの「ドット」構文を使用して、ユーザーコード内で直接参照できますが、このivarに対応する<code class="docutils literal notranslate"><span class="pre">@interface</span></code>宣言にはエントリがありません。
デバッグを容易にするために、これらのプロパティに対して、クラスの<code class="docutils literal notranslate"><span class="pre">DW_TAG_structure_type</span></code>定義に新しいDWARF TAGを追加し、指定されたプロパティの説明と、その説明を提供するDWARF属性のセットを保持します。プロパティタグには、プロパティの名前と宣言された型も含まれます。
関連するivarがある場合は、そのivarの<code class="docutils literal notranslate"><span class="pre">DW_TAG_member</span></code> DIEにも、そのプロパティのproperty TAGを参照するDWARFプロパティ属性が配置されます。コンパイラがivarを直接合成する場合、コンパイラは、そのivarの<code class="docutils literal notranslate"><span class="pre">DW_TAG_member</span></code>(<code class="docutils literal notranslate"><span class="pre">DW_AT_artificial</span></code>を1に設定)を生成することが期待されます。その名前は、コード内でこのivarに直接アクセスするために使用される名前になり、プロパティ属性は、それが裏付けとなっているプロパティを指します。
以下の例は、議論の例として役立ちます。
@interface I1 {
int n2;
}
@property int p1;
@property int p2;
@end
@implementation I1
@synthesize p1;
@synthesize p2 = n2;
@end
これは、次のDWARFを生成します(これは「疑似dwarfdump」出力です)。
0x00000100: TAG_structure_type [7] *
AT_APPLE_runtime_class( 0x10 )
AT_name( "I1" )
AT_decl_file( "Objc_Property.m" )
AT_decl_line( 3 )
0x00000110 TAG_APPLE_property
AT_name ( "p1" )
AT_type ( {0x00000150} ( int ) )
0x00000120: TAG_APPLE_property
AT_name ( "p2" )
AT_type ( {0x00000150} ( int ) )
0x00000130: TAG_member [8]
AT_name( "_p1" )
AT_APPLE_property ( {0x00000110} "p1" )
AT_type( {0x00000150} ( int ) )
AT_artificial ( 0x1 )
0x00000140: TAG_member [8]
AT_name( "n2" )
AT_APPLE_property ( {0x00000120} "p2" )
AT_type( {0x00000150} ( int ) )
0x00000150: AT_type( ( int ) )
現在の規則では、自動合成されたプロパティのivarの名前は、例に示すように、派生元のプロパティの名前にアンダースコアを付けたものになります。ただし、ivarの名前が直接提供されるため、実際にはこの規則を知る必要はありません。
また、ObjCでは、@interfaceと@implementationで異なるプロパティ宣言を行うのが一般的です。たとえば、インターフェースに読み取り専用プロパティを提供し、実装に読み取り/書き込みインターフェースを提供します。その場合、コンパイラは、現在の翻訳単位で有効になるプロパティ宣言を出力する必要があります。
開発者は、<code class="docutils literal notranslate"><span class="pre">DW_AT_APPLE_property_attribute</span></code>を使用してエンコードされた属性でプロパティを装飾できます。
@property (readonly, nonatomic) int pr;
TAG_APPLE_property [8]
AT_name( "pr" )
AT_type ( {0x00000147} (int) )
AT_APPLE_property_attribute (DW_APPLE_PROPERTY_readonly, DW_APPLE_PROPERTY_nonatomic)
セッターとゲッターのメソッド名は、<code class="docutils literal notranslate"><span class="pre">DW_AT_APPLE_property_setter</span></code>属性と<code class="docutils literal notranslate"><span class="pre">DW_AT_APPLE_property_getter</span></code>属性を使用してプロパティに添付されます。
@interface I1
@property (setter=myOwnP3Setter:) int p3;
-(void)myOwnP3Setter:(int)a;
@end
@implementation I1
@synthesize p3;
-(void)myOwnP3Setter:(int)a{ }
@end
このDWARFは次のようになります。
0x000003bd: TAG_structure_type [7] *
AT_APPLE_runtime_class( 0x10 )
AT_name( "I1" )
AT_decl_file( "Objc_Property.m" )
AT_decl_line( 3 )
0x000003cd TAG_APPLE_property
AT_name ( "p3" )
AT_APPLE_property_setter ( "myOwnP3Setter:" )
AT_type( {0x00000147} ( int ) )
0x000003f3: TAG_member [8]
AT_name( "_p3" )
AT_type ( {0x00000147} ( int ) )
AT_APPLE_property ( {0x000003cd} )
AT_artificial ( 0x1 )
<a class="toc-backref" href="#id44" role="doc-backlink">新しいDWARF属性</a><a class="headerlink" href="#new-dwarf-attributes" title="Permalink to this heading">¶</a>
属性 |
値 |
クラス |
---|---|---|
DW_AT_APPLE_property |
0x3fed |
リファレンス |
DW_AT_APPLE_property_getter |
0x3fe9 |
文字列 |
DW_AT_APPLE_property_setter |
0x3fea |
文字列 |
DW_AT_APPLE_property_attribute |
0x3feb |
定数 |
<a class="toc-backref" href="#id45" role="doc-backlink">新しいDWARF定数</a><a class="headerlink" href="#new-dwarf-constants" title="Permalink to this heading">¶</a>
名前 |
値 |
---|---|
DW_APPLE_PROPERTY_readonly |
0x01 |
DW_APPLE_PROPERTY_getter |
0x02 |
DW_APPLE_PROPERTY_assign |
0x04 |
DW_APPLE_PROPERTY_readwrite |
0x08 |
DW_APPLE_PROPERTY_retain |
0x10 |
DW_APPLE_PROPERTY_copy |
0x20 |
DW_APPLE_PROPERTY_nonatomic |
0x40 |
DW_APPLE_PROPERTY_setter |
0x80 |
DW_APPLE_PROPERTY_atomic |
0x100 |
DW_APPLE_PROPERTY_weak |
0x200 |
DW_APPLE_PROPERTY_strong |
0x400 |
DW_APPLE_PROPERTY_unsafe_unretained |
0x800 |
DW_APPLE_PROPERTY_nullability |
0x1000 |
DW_APPLE_PROPERTY_null_resettable |
0x2000 |
DW_APPLE_PROPERTY_class |
0x4000 |
<a class="toc-backref" href="#id46" role="doc-backlink">名前アクセラレータテーブル</a><a class="headerlink" href="#name-accelerator-tables" title="Permalink to this heading">¶</a>
<a class="toc-backref" href="#id47" role="doc-backlink">はじめに</a><a class="headerlink" href="#id9" title="Permalink to this heading">¶</a>
「<code class="docutils literal notranslate"><span class="pre">.debug_pubnames</span></code>」および「<code class="docutils literal notranslate"><span class="pre">.debug_pubtypes</span></code>」フォーマットは、デバッガが必要とするものではありません。セクション名にある「<code class="docutils literal notranslate"><span class="pre">pub</span></code>」は、テーブル内のエントリが公開されている名前のみであることを示します。つまり、静的関数または非表示関数は「<code class="docutils literal notranslate"><span class="pre">.debug_pubnames</span></code>」に表示されません。静的変数またはプライベートクラス変数は「<code class="docutils literal notranslate"><span class="pre">.debug_pubtypes</span></code>」にはありません。多くのコンパイラはこれらのテーブルに異なるものを追加するため、gcc、icc、またはclangの間でコンテンツに依存することはできません。
ユーザーから提供される一般的なクエリは、これらのテーブルの内容と一致しない傾向があります。たとえば、DWARF仕様では、「C++構造体、クラス、または共用体の関数メンバまたは静的データメンバの名前の場合、「<code class="docutils literal notranslate"><span class="pre">.debug_pubnames</span></code>」セクションに表示される名前は、参照されるデバッグ情報エントリの<code class="docutils literal notranslate"><span class="pre">DW_AT_name</span><span class="pre">属性</span></code>で指定される単純な名前ではなく、データまたは関数メンバの完全修飾名です」と述べています。そのため、これらのテーブルにある複雑なC++エントリの唯一の名前は、完全修飾名です。デバッガユーザーは、検索文字列を「<code class="docutils literal notranslate"><span class="pre">a::b::c(int,const</span><span class="pre"> Foo&</span>)<span class="pre"> const</span></code>」として入力するのではなく、「<code class="docutils literal notranslate"><span class="pre">c</span></code>」、「<code class="docutils literal notranslate"><span class="pre">b::c</span></code>」、または「<code class="docutils literal notranslate"><span class="pre">a::b::c</span></code>」として入力する傾向があります。そのため、名前テーブルに入力された名前をデマングルして適切に分割し、デバッガが使用する名前ルックアップテーブルとして効果的に機能させるために、追加の名前を手動でテーブルに入力する必要があります。
現在、すべてのデバッガは、「<code class="docutils literal notranslate"><span class="pre">.debug_pubnames</span></code>」テーブルを無視しています。これは、その一貫性のない役に立たない公開専用の名前コンテンツがオブジェクトファイルのスペースを無駄にしているためです。これらのテーブルは、ディスクに書き込まれる場合、いかなる方法でもソートされないため、すべてのデバッガが独自の解析とソートを行う必要があります。これらのテーブルには、テーブル自体に文字列値のインラインコピーも含まれているため、特に大規模なC++プログラムの場合、テーブルがディスク上で必要以上に大きくなります。
必要なすべての名前をこのテーブルに追加することで、セクションを修正できないでしょうか?いいえ、それはテーブルに含まれるように定義されているものではないため、古い不良テーブルと新しい良好テーブルの違いがわからないからです。せいぜい、必要なすべてのデータを含む独自の名前変更セクションを作成できます。
これらのテーブルは、LLDBのようなデバッガが必要とするものにも不十分です。LLDBは、式解析にclangを使用し、LLDBはPCHとして機能します。LLDBは、タイプ「<code class="docutils literal notranslate"><span class="pre">foo</span></code>」または名前空間「<code class="docutils literal notranslate"><span class="pre">bar</span></code>」を検索したり、名前空間「<code class="docutils literal notranslate"><span class="pre">baz</span></code>」の項目をリストしたりするようによく要求されます。名前空間は、pubnamesまたはpubtypesテーブルには含まれていません。clangは式を解析するときに多くの質問をするため、名前の検索は非常に高速である必要があります。非常に高速なルックアップに最適化された新しいアクセラレータテーブルがあると、このタイプのデバッグエクスペリエンスが大幅に向上します。
ディスクからメモリにマッピングし、そのまま使用できる名前ルックアップテーブルを生成したいと考えています。また、これらの異なるテーブルの正確な内容を制御して、必要なものだけを確実に含めることもできます。名前アクセラレータテーブルは、これらの問題を解決するために設計されました。これらの問題を解決するには、次のことが必要です。
ディスクからメモリにマッピングし、そのまま使用できるフォーマットを用意する
ルックアップは非常に高速である必要があります
拡張可能なテーブル形式にすることで、多くのプロデューサーがこれらのテーブルを作成できるようにする
一般的なルックアップに必要なすべての名前をすぐに含める
テーブルの内容に関する厳格なルール
テーブルサイズは重要であり、アクセラレータテーブル形式では、共通文字列テーブルの文字列を再利用できるため、名前の文字列が重複しません。また、最小限のヘッダー解析でテーブルをメモリにマッピングするだけで、テーブルをそのまま使用できるようにしたいと考えています。
名前ルックアップは高速で、デバッガが行う傾向のあるルックアップの種類に最適化されている必要があります。最適には、名前ルックアップを行う際に、マッピングされたテーブルのできるだけ少ない部分にアクセスし、探している名前エントリをすばやく見つけるか、一致するものがないことを発見できるようにします。デバッガの場合、ほとんどの場合に失敗するルックアップに最適化しました。
定義されている各テーブルには、アクセラレータテーブルの内容に関する厳格なルールがあり、クライアントがコンテンツに依存できるように文書化する必要があります。
<a class="toc-backref" href="#id48" role="doc-backlink">ハッシュテーブル</a><a class="headerlink" href="#hash-tables" title="Permalink to this heading">¶</a>
<a class="toc-backref" href="#id49" role="doc-backlink">標準ハッシュテーブル</a><a class="headerlink" href="#standard-hash-tables" title="Permalink to this heading">¶</a>
典型的なハッシュテーブルには、ヘッダー、バケットがあり、各バケットはバケットの内容を指します。
.------------.
| HEADER |
|------------|
| BUCKETS |
|------------|
| DATA |
`------------'
BUCKETSは、各ハッシュのDATAへのオフセットの配列です。
.------------.
| 0x00001000 | BUCKETS[0]
| 0x00002000 | BUCKETS[1]
| 0x00002200 | BUCKETS[2]
| 0x000034f0 | BUCKETS[3]
| | ...
| 0xXXXXXXXX | BUCKETS[n_buckets]
'------------'
上記の例では、<code class="docutils literal notranslate"><span class="pre">bucket[3]</span></code>には、テーブル0x000034f0へのオフセットがあり、バケットのエントリのチェーンを指しています。各バケットには、次のポインタ、完全な32ビットハッシュ値、文字列自体、および現在の文字列値のデータが含まれている必要があります。
.------------.
0x000034f0: | 0x00003500 | next pointer
| 0x12345678 | 32 bit hash
| "erase" | string value
| data[n] | HashData for this bucket
|------------|
0x00003500: | 0x00003550 | next pointer
| 0x29273623 | 32 bit hash
| "dump" | string value
| data[n] | HashData for this bucket
|------------|
0x00003550: | 0x00000000 | next pointer
| 0x82638293 | 32 bit hash
| "main" | string value
| data[n] | HashData for this bucket
`------------'
デバッガにとってこのレイアウトの問題点は、検索しているシンボルが存在しない負のルックアップケースに最適化する必要があることです。そのため、上記のテーブルで「<code class="docutils literal notranslate"><span class="pre">printf</span></code>」を検索する場合、「<code class="docutils literal notranslate"><span class="pre">printf</span></code>」の32ビットハッシュを作成し、<code class="docutils literal notranslate"><span class="pre">bucket[3]</span></code>と一致する可能性があります。オフセット0x000034f0に移動し、32ビットハッシュが一致するかどうかを確認する必要があります。そのためには、次のポインタを読み取り、ハッシュを読み取り、比較し、次のバケットにスキップする必要があります。毎回、メモリ内の多くのバイトをスキップし、新しいページにアクセスして、完全な32ビットハッシュの比較を行っています。これらのすべてのアクセスは、一致しなかったことを示しています。
名前ハッシュテーブル¶
上記の課題を解決するために、ハッシュテーブルの構造を少し変更しました。ヘッダー、バケット、すべてのユニークな32ビットハッシュ値の配列、それに続く各ハッシュ値に対応するハッシュ値データオフセットの配列、そして最後にすべてのハッシュ値のデータという構造です。
.-------------.
| HEADER |
|-------------|
| BUCKETS |
|-------------|
| HASHES |
|-------------|
| OFFSETS |
|-------------|
| DATA |
`-------------'
名前テーブルのBUCKETS
は、HASHES
配列へのインデックスです。すべての完全な32ビットハッシュ値をメモリ内で連続させることで、可能な限り少ないメモリに触れながら、効率的に一致を確認できます。ほとんどの場合、32ビットハッシュ値の確認だけでルックアップは完了します。一致する場合、通常は衝突なしで一致します。そのため、「n_buckets
」個のバケットと「n_hashes
」個のユニークな32ビットハッシュ値を持つテーブルの場合、BUCKETS
、HASHES
、およびOFFSETS
の内容を次のように明確化できます。
.-------------------------.
| HEADER.magic | uint32_t
| HEADER.version | uint16_t
| HEADER.hash_function | uint16_t
| HEADER.bucket_count | uint32_t
| HEADER.hashes_count | uint32_t
| HEADER.header_data_len | uint32_t
| HEADER_DATA | HeaderData
|-------------------------|
| BUCKETS | uint32_t[n_buckets] // 32 bit hash indexes
|-------------------------|
| HASHES | uint32_t[n_hashes] // 32 bit hash values
|-------------------------|
| OFFSETS | uint32_t[n_hashes] // 32 bit offsets to hash value data
|-------------------------|
| ALL HASH DATA |
`-------------------------'
上記の標準ハッシュの例とまったく同じデータを使用すると、次のようになります。
.------------.
| HEADER |
|------------|
| 0 | BUCKETS[0]
| 2 | BUCKETS[1]
| 5 | BUCKETS[2]
| 6 | BUCKETS[3]
| | ...
| ... | BUCKETS[n_buckets]
|------------|
| 0x........ | HASHES[0]
| 0x........ | HASHES[1]
| 0x........ | HASHES[2]
| 0x........ | HASHES[3]
| 0x........ | HASHES[4]
| 0x........ | HASHES[5]
| 0x12345678 | HASHES[6] hash for BUCKETS[3]
| 0x29273623 | HASHES[7] hash for BUCKETS[3]
| 0x82638293 | HASHES[8] hash for BUCKETS[3]
| 0x........ | HASHES[9]
| 0x........ | HASHES[10]
| 0x........ | HASHES[11]
| 0x........ | HASHES[12]
| 0x........ | HASHES[13]
| 0x........ | HASHES[n_hashes]
|------------|
| 0x........ | OFFSETS[0]
| 0x........ | OFFSETS[1]
| 0x........ | OFFSETS[2]
| 0x........ | OFFSETS[3]
| 0x........ | OFFSETS[4]
| 0x........ | OFFSETS[5]
| 0x000034f0 | OFFSETS[6] offset for BUCKETS[3]
| 0x00003500 | OFFSETS[7] offset for BUCKETS[3]
| 0x00003550 | OFFSETS[8] offset for BUCKETS[3]
| 0x........ | OFFSETS[9]
| 0x........ | OFFSETS[10]
| 0x........ | OFFSETS[11]
| 0x........ | OFFSETS[12]
| 0x........ | OFFSETS[13]
| 0x........ | OFFSETS[n_hashes]
|------------|
| |
| |
| |
| |
| |
|------------|
0x000034f0: | 0x00001203 | .debug_str ("erase")
| 0x00000004 | A 32 bit array count - number of HashData with name "erase"
| 0x........ | HashData[0]
| 0x........ | HashData[1]
| 0x........ | HashData[2]
| 0x........ | HashData[3]
| 0x00000000 | String offset into .debug_str (terminate data for hash)
|------------|
0x00003500: | 0x00001203 | String offset into .debug_str ("collision")
| 0x00000002 | A 32 bit array count - number of HashData with name "collision"
| 0x........ | HashData[0]
| 0x........ | HashData[1]
| 0x00001203 | String offset into .debug_str ("dump")
| 0x00000003 | A 32 bit array count - number of HashData with name "dump"
| 0x........ | HashData[0]
| 0x........ | HashData[1]
| 0x........ | HashData[2]
| 0x00000000 | String offset into .debug_str (terminate data for hash)
|------------|
0x00003550: | 0x00001203 | String offset into .debug_str ("main")
| 0x00000009 | A 32 bit array count - number of HashData with name "main"
| 0x........ | HashData[0]
| 0x........ | HashData[1]
| 0x........ | HashData[2]
| 0x........ | HashData[3]
| 0x........ | HashData[4]
| 0x........ | HashData[5]
| 0x........ | HashData[6]
| 0x........ | HashData[7]
| 0x........ | HashData[8]
| 0x00000000 | String offset into .debug_str (terminate data for hash)
`------------'
つまり、すべてのデータは同じままで、デバッガールックアップのためにより効率的に整理されています。上記の「printf
」ルックアップを繰り返すと、「printf
」をハッシュし、32ビットハッシュ値をn_buckets
で割った余りを取ることで、BUCKETS[3]
と一致することがわかります。BUCKETS[3]
には、HASHES
テーブルへのインデックスである「6」が含まれています。その後、ハッシュがBUCKETS[3]
にある限り、HASHES
配列内の連続する32ビットハッシュ値を比較します。これは、後続の各ハッシュ値をn_buckets
で割った余りが依然として3であることを確認することで行います。ルックアップが失敗した場合、BUCKETS[3]
のメモリにアクセスし、一致しないとわかるまで、連続するいくつかの32ビットハッシュを比較します。複数のメモリワードを順番に調べることはなく、アクセスされるプロセッサデータキャッシュラインの数を可能な限り少なく抑えます。
これらのルックアップテーブルに使用される文字列ハッシュは、ELFのGNU_HASH
セクションでも使用されているDaniel J. Bernsteinハッシュです。これは、ハッシュの衝突が非常に少ないプログラム内のあらゆる種類の名前に対して非常に優れたハッシュです。
空のバケットは、無効なハッシュインデックスUINT32_MAX
を使用して指定されます。
詳細¶
これらの名前ハッシュテーブルは、テーブルの特殊化がヘッダー(「HeaderData
」)に追加されるデータ、文字列値の格納方法(「KeyType
」)、および各ハッシュ値のデータの内容を定義できるように、汎用的に設計されています。
ヘッダーレイアウト¶
ヘッダーには、固定部分と特殊化部分があります。ヘッダーの正確な形式は次のとおりです。
struct Header
{
uint32_t magic; // 'HASH' magic value to allow endian detection
uint16_t version; // Version number
uint16_t hash_function; // The hash function enumeration that was used
uint32_t bucket_count; // The number of buckets in this hash table
uint32_t hashes_count; // The total number of unique hash values and hash data offsets in this table
uint32_t header_data_len; // The bytes to skip to get to the hash indexes (buckets) for correct alignment
// Specifically the length of the following HeaderData field - this does not
// include the size of the preceding fields
HeaderData header_data; // Implementation specific header data
};
ヘッダーは、ASCII整数としてエンコードされた「'HASH'
」である必要がある32ビットの「magic
」値で始まります。これにより、ハッシュテーブルの先頭を検出し、テーブルのバイト順を判断できるため、テーブルを正しく抽出できます。「magic
」値の後には、将来テーブルを改訂および変更できるようにする16ビットのversion
番号が続きます。現在のバージョン番号は1です。hash_function
は、このテーブルの作成に使用されたハッシュ関数を指定するuint16_t
列挙型です。ハッシュ関数列挙型の現在の値には、次のものが含まれます。
enum HashFunctionType
{
eHashFunctionDJB = 0u, // Daniel J Bernstein hash function
};
bucket_count
は、BUCKETS
配列にあるバケットの数を表す32ビット符号なし整数です。hashes_count
は、HASHES
配列にあるユニークな32ビットハッシュ値の数であり、OFFSETS
配列に含まれるオフセットの数と同じです。header_data_len
は、このテーブルの特殊化バージョンによって入力されるHeaderData
のサイズをバイト単位で指定します。
固定ルックアップ¶
ヘッダーの後には、バケット、ハッシュ、オフセット、およびハッシュ値データが続きます。
struct FixedTable
{
uint32_t buckets[Header.bucket_count]; // An array of hash indexes into the "hashes[]" array below
uint32_t hashes [Header.hashes_count]; // Every unique 32 bit hash for the entire table is in this table
uint32_t offsets[Header.hashes_count]; // An offset that corresponds to each item in the "hashes[]" array above
};
buckets
は、hashes
配列への32ビットインデックスの配列です。hashes
配列には、ハッシュテーブル内のすべての名前のすべての32ビットハッシュ値が含まれています。hashes
テーブルの各ハッシュには、ハッシュ値のデータを指すoffsets
配列にオフセットがあります。
このテーブル設定により、すべてのテーブルでルックアップメカニズムを同じに保ちながら、これらのテーブルを異なるデータを含むように簡単に再利用できます。また、このレイアウトにより、テーブルをディスクに保存して後でマップし、解析をほとんどまたはまったく行わずに非常に効率的な名前ルックアップを実行できます。
DWARFルックアップテーブルはさまざまな方法で実装でき、各名前に関する多くの情報を格納できます。DWARFテーブルを拡張可能にし、データを効率的に格納できるようにしたいと考えているため、各名前に格納するデータの種類を正確に定義するために、効率的なデータストレージを可能にするDWARF機能の一部を使用しました。
HeaderData
には、各HashDataチャンクの内容の定義が含まれています。各名前のすべてのデバッグ情報エントリ(DIE)へのオフセットを格納したい場合があります。拡張性を維持するために、各名前のデータに含まれる項目、つまりアトムのリストを作成します。最初に、各アトムのデータのタイプを示します。
enum AtomType
{
eAtomTypeNULL = 0u,
eAtomTypeDIEOffset = 1u, // DIE offset, check form for encoding
eAtomTypeCUOffset = 2u, // DIE offset of the compiler unit header that contains the item in question
eAtomTypeTag = 3u, // DW_TAG_xxx value, should be encoded as DW_FORM_data1 (if no tags exceed 255) or DW_FORM_data2
eAtomTypeNameFlags = 4u, // Flags from enum NameFlags
eAtomTypeTypeFlags = 5u, // Flags from enum TypeFlags
};
列挙値とその意味は次のとおりです。
eAtomTypeNULL - a termination atom that specifies the end of the atom list
eAtomTypeDIEOffset - an offset into the .debug_info section for the DWARF DIE for this name
eAtomTypeCUOffset - an offset into the .debug_info section for the CU that contains the DIE
eAtomTypeDIETag - The DW_TAG_XXX enumeration value so you don't have to parse the DWARF to see what it is
eAtomTypeNameFlags - Flags for functions and global variables (isFunction, isInlined, isExternal...)
eAtomTypeTypeFlags - Flags for types (isCXXClass, isObjCClass, ...)
次に、各アトムタイプがアトムタイプと各アトムタイプデータのデータのエンコード方法を定義できるようにします。
struct Atom
{
uint16_t type; // AtomType enum value
uint16_t form; // DWARF DW_FORM_XXX defines
};
上記のform
タイプはDWARF仕様からのもので、Atomタイプのデータの正確なエンコーディングを定義します。DW_FORM_
定義については、DWARF仕様を参照してください。
struct HeaderData
{
uint32_t die_offset_base;
uint32_t atom_count;
Atoms atoms[atom_count0];
};
HeaderData
は、DW_FORM_ref1
、DW_FORM_ref2
、DW_FORM_ref4
、DW_FORM_ref8
、またはDW_FORM_ref_udata
を使用してエンコードされたアトムに追加する必要がある基本DIEオフセットを定義します。また、各HashData
オブジェクトに含まれる内容も定義します。Atom.form
は、HashData
の各フィールドのサイズを示し、Atom.type
はこのデータの解釈方法を示します。
「.apple_names
」(すべての関数+グローバル)、「.apple_types
」(定義されているすべてのタイプの名前)、および「.apple_namespaces
」(すべての名前空間)の現在の実装では、現在Atom
配列を次のように設定しています。
HeaderData.atom_count = 1;
HeaderData.atoms[0].type = eAtomTypeDIEOffset;
HeaderData.atoms[0].form = DW_FORM_data4;
これは、コンテンツを32ビット値(DW_FORM_data4)としてエンコードされたDIEオフセット(eAtomTypeDIEOffset)として定義します。これにより、単一の名前が単一ファイルに複数の一致するDIEを持つことができ、たとえばインライン関数で発生する可能性があります。将来のテーブルには、DIEが関数、メソッド、ブロック、またはインラインであるかどうかを示すフラグなど、DIEに関する詳細情報を含めることができます。
DWARFテーブルのKeyTypeは、「.debug_str」テーブルへの32ビット文字列テーブルオフセットです。「.debug_str」は、すでにすべての文字列のコピーが含まれている可能性のあるDWARFの文字列テーブルです。これは、コンパイラの助けを借りて、すべてのDWARFセクション間で文字列を再利用し、ハッシュテーブルのサイズを小さく保つのに役立ちます。コンパイラがデバッグ情報内のすべての文字列をDW_FORM_strpとして生成することのもう1つの利点は、DWARFの解析をはるかに高速化できることです。
ルックアップが行われた後、ハッシュデータへのオフセットが得られます。ハッシュデータは32ビットハッシュの衝突に対処できる必要があるため、ハッシュデータのオフセットにあるデータチャンクはトリプルで構成されます。
uint32_t str_offset
uint32_t hash_data_count
HashData[hash_data_count]
「str_offset」がゼロの場合、バケットの内容は完了です。ハッシュデータチャンクの99.9%には、単一の項目が含まれています(32ビットハッシュの衝突なし)。
.------------.
| 0x00001023 | uint32_t KeyType (.debug_str[0x0001023] => "main")
| 0x00000004 | uint32_t HashData count
| 0x........ | uint32_t HashData[0] DIE offset
| 0x........ | uint32_t HashData[1] DIE offset
| 0x........ | uint32_t HashData[2] DIE offset
| 0x........ | uint32_t HashData[3] DIE offset
| 0x00000000 | uint32_t KeyType (end of hash chain)
`------------'
衝突がある場合、複数の有効な文字列オフセットがあります。
.------------.
| 0x00001023 | uint32_t KeyType (.debug_str[0x0001023] => "main")
| 0x00000004 | uint32_t HashData count
| 0x........ | uint32_t HashData[0] DIE offset
| 0x........ | uint32_t HashData[1] DIE offset
| 0x........ | uint32_t HashData[2] DIE offset
| 0x........ | uint32_t HashData[3] DIE offset
| 0x00002023 | uint32_t KeyType (.debug_str[0x0002023] => "print")
| 0x00000002 | uint32_t HashData count
| 0x........ | uint32_t HashData[0] DIE offset
| 0x........ | uint32_t HashData[1] DIE offset
| 0x00000000 | uint32_t KeyType (end of hash chain)
`------------'
実際のC++バイナリを用いた現在のテストでは、100,000の名前エントリにつき約1つの32ビットハッシュ衝突が発生することが示されています。
目次¶
前述のように、異なるテーブルに何が含まれるかを厳密に定義したいと考えています。DWARFの場合、".apple_names
"、".apple_types
"、".apple_namespaces
"の3つのテーブルがあります。
".apple_names
"セクションには、DW_TAG
がアドレス属性(DW_AT_low_pc
、DW_AT_high_pc
、DW_AT_ranges
、またはDW_AT_entry_pc
)を持つDW_TAG_label
、DW_TAG_inlined_subroutine
、またはDW_TAG_subprogram
である各DWARF DIEのエントリが含まれている必要があります。また、locationにDW_OP_addr
を持つDW_TAG_variable
DIE(グローバル変数と静的変数)も含まれています。関数やクラス内でスコープされているものも含め、すべてのグローバル変数と静的変数が含まれている必要があります。例えば、以下のコードを使用した場合
static int var = 0;
void f ()
{
static int var = 0;
}
両方の静的`var`変数はテーブルに含まれます。すべての関数は、完全名とベース名の両方を出力する必要があります。CまたはC++の場合、完全名は(利用可能な場合)マンングルされた名前で、通常は`DW_AT_MIPS_linkage_name`属性にあり、`DW_AT_name`には関数ベース名が含まれています。グローバル変数または静的変数に`DW_AT_MIPS_linkage_name`属性にマンングルされた名前がある場合、これは`DW_AT_name`属性にある単純名とともに出力される必要があります。
".apple_types
"セクションには、タグが以下のいずれかである各DWARF DIEのエントリが含まれている必要があります。
DW_TAG_array_type
DW_TAG_class_type
DW_TAG_enumeration_type
DW_TAG_pointer_type
DW_TAG_reference_type
DW_TAG_string_type
DW_TAG_structure_type
DW_TAG_subroutine_type
DW_TAG_typedef
DW_TAG_union_type
DW_TAG_ptr_to_member_type
DW_TAG_set_type
DW_TAG_subrange_type
DW_TAG_base_type
DW_TAG_const_type
DW_TAG_immutable_type
DW_TAG_file_type
DW_TAG_namelist
DW_TAG_packed_type
DW_TAG_volatile_type
DW_TAG_restrict_type
DW_TAG_atomic_type
DW_TAG_interface_type
DW_TAG_unspecified_type
DW_TAG_shared_type
DW_AT_name
属性を持つエントリのみが含まれ、エントリは前方宣言(ゼロ以外の値を持つDW_AT_declaration
属性)であってはなりません。例えば、以下のコードを使用した場合
int main ()
{
int *b = 0;
return *b;
}
いくつかの型DIEが得られます
0x00000067: TAG_base_type [5]
AT_encoding( DW_ATE_signed )
AT_name( "int" )
AT_byte_size( 0x04 )
0x0000006e: TAG_pointer_type [6]
AT_type( {0x00000067} ( int ) )
AT_byte_size( 0x08 )
DW_TAG_pointer_typeは、`DW_AT_name`を持たないため、含まれません。
".apple_namespaces
"セクションには、すべての`DW_TAG_namespace` DIEが含まれている必要があります。名前のない名前空間に遭遇した場合、これは匿名名前空間であり、名前は"(anonymous namespace)
"(引用符なし)として出力する必要があります。なぜでしょうか?これは、マンングルされた名前を逆マンングルする標準C++ライブラリにある`abi::cxa_demangle()`の出力と一致するためです。
言語拡張とファイル形式の変更¶
Objective-C拡張¶
".apple_objc
"セクションには、Objective-Cクラスのすべての`DW_TAG_subprogram` DIEが含まれている必要があります。ハッシュテーブルで使用される名前は、Objective-Cクラス自体の名前です。Objective-Cクラスにカテゴリがある場合、カテゴリのないクラス名と、カテゴリを持つクラス名の両方にエントリが作成されます。そのため、オフセット0x1234に"-[NSString(my_additions) stringWithSpecialString:]
"という名前のメソッドのDIEがある場合、DIE 0x1234を指す"NSString
"のエントリと、0x1234を指す"NSString(my_additions)
"のエントリを追加します。これにより、式を実行する際に、Objective-CクラスのすべてのObjective-Cメソッドをすばやく追跡できます。これは、Objective-Cの動的な性質上、誰でもクラスにメソッドを追加できるため必要です。Objective-CメソッドのDWARFも、メソッドが通常クラス定義に含まれておらず、1つ以上のコンパイル単位に分散しているC++クラスとは異なる方法で出力されます。カテゴリは、異なる共有ライブラリで定義することもできます。そのため、Objective-Cクラス名、またはクラス名+カテゴリ名に基づいて、すべてのメソッドとクラス関数をすばやく見つけることができる必要があります。このテーブルにはセレクタ名は含まれておらず、Objective-Cクラス名(またはクラス名+カテゴリ)をすべてのメソッドとクラス関数にマッピングするだけです。セレクタは、".debug_names
"セクションに関数ベース名として追加されます。
Objective-C関数の".apple_names
"セクションでは、完全名は括弧を含む関数名全体("-[NSString stringWithCString:]
")であり、ベース名はセレクタのみ("stringWithCString:
")です。
Mach-Oの変更¶
Appleハッシュテーブルのセクション名は、非mach-oファイル用です。mach-oファイルの場合、セクションは`__DWARF`セグメントに含まれ、名前は次のとおりです。
"
.apple_names
" -> "__apple_names
""
.apple_types
" -> "__apple_types
""
.apple_namespaces
" -> "__apple_namespac
" (16文字制限)"
.apple_objc
" -> "__apple_objc
"
CodeViewデバッグ情報フォーマット¶
LLVMは、Microsoftのデバッグ情報フォーマットであるCodeViewの出力をサポートしており、このセクションでは、そのサポートの設計と実装について説明します。
フォーマットの背景¶
CodeViewは、明らかにC++デバッグを指向したフォーマットであり、C++では、デバッグ情報の大部分は型情報である傾向があります。そのため、CodeViewの設計上の最大の制約は、型情報を他の「シンボル」情報から分離することで、翻訳単位間で型情報を効率的にマージできるようにすることです。型情報とシンボル情報は、一般にレコードのシーケンスとして格納され、各レコードは16ビットのレコードサイズと16ビットのレコード種類で始まります。
型情報は、通常、オブジェクトファイルの` .debug$T`セクションに格納されます。行情報、文字列テーブル、シンボル情報、インライン情報など、その他のすべてのデバッグ情報は、1つ以上の` .debug$S`セクションに格納されます。他のすべてのデバッグ情報が` .debug$T`セクションを参照するため、オブジェクトファイルごとに` .debug$T`セクションは1つしか存在できません。コンパイル中にPDB(MSVCオプション`/Zi`で有効)が使用された場合、` .debug$T`セクションには、PDBを指す` LF_TYPESERVER2`レコードのみが含まれます。PDBを使用する場合、シンボル情報はオブジェクトファイルの` .debug$S`セクションに残っているようです。
型レコードは、そのインデックスによって参照されます。インデックスは、特定のレコードの前にストリームに存在するレコードの数に` 0x1000`を加えたものです。基本的な整数型やそれらへの非修飾ポインタなど、多くの一般的な基本型は、` 0x1000`未満の型インデックスを使用して表されます。このような基本型はCodeViewコンシューマに組み込まれており、型レコードは必要ありません。
各型レコードには、自身の型インデックスよりも小さい型インデックスのみを含めることができます。これにより、型ストリーム参照のグラフが非巡回的になります。ソースレベルの型グラフには、ポインタ型を介したサイクルが含まれている場合がありますが(リンクリスト構造体を考えてみてください)、これらのサイクルは、ユーザー定義レコード型の前方宣言レコードを常に参照することで型ストリームから削除されます。` .debug$S`ストリームの「シンボル」レコードのみが、完全な、前方宣言ではない型レコードを参照できます。
CodeViewの操作¶
これらは、LLVMのCodeViewサポートの改善に取り組んでいる開発者向けの一般的なタスクの手順です。ほとんどは、` llvm-readobj`に埋め込まれているCodeViewダンパーの使用に関するものです。
MSVCの出力をテストする
$ cl -c -Z7 foo.cpp # Use /Z7 to keep types in the object file $ llvm-readobj --codeview foo.obj
ClangからLLVM IRデバッグ情報を取得する
$ clang -g -gcodeview --target=x86_64-windows-msvc foo.cpp -S -emit-llvm
LLVMテストケースのLLVM IRを生成するために使用します。
LLVM IRメタデータからCodeViewを生成してダンプする
$ llc foo.ll -filetype=obj -o foo.obj $ llvm-readobj --codeview foo.obj > foo.txt
litテストケースでこのパターンを使用し、llvm-readobjの出力をFileCheckします。
LLVMのCodeViewサポートの改善は、興味深い型レコードを見つけ、MSVCにそれらのレコードを出力させるC++テストケースを作成し、レコードをダンプし、それらを理解し、LLVMのバックエンドで同等のレコードを生成するプロセスです。