使用 LLVM 進行原始碼層級除錯

簡介

本文檔是關於 LLVM 中除錯資訊的所有資訊的中心儲存庫。它描述了 LLVM 除錯資訊採用的實際格式,這對於那些有興趣建立前端或直接處理資訊的人很有用。此外,本文檔提供了 C/C++ 除錯資訊外觀的具體範例。

LLVM 除錯資訊背後的理念

LLVM 除錯資訊的理念是捕捉原始語言抽象語法樹的重要部分如何映射到 LLVM 程式碼。一些設計考量塑造了此處顯示的解決方案。重要的考量如下:

  • 除錯資訊對編譯器的其餘部分應影響甚微。不應因為除錯資訊而需要修改任何轉換、分析或程式碼產生器。

  • LLVM 最佳化應以定義明確且易於描述的方式與除錯資訊互動。

  • 由於 LLVM 旨在支援任意程式語言,因此 LLVM 到 LLVM 工具不需要了解任何關於原始碼層級語言語意的内容。

  • 原始碼層級語言通常彼此之間差異很大。LLVM 不應對原始語言的風格施加任何限制,並且除錯資訊應適用於任何語言。

  • 在程式碼產生器支援下,應可以使用 LLVM 編譯器將程式編譯為原生機器碼和標準除錯格式。這允許與傳統的機器碼層級除錯器(如 GDB 或 DBX)相容。

LLVM 實作採用的方法是使用一小組除錯紀錄來定義 LLVM 程式物件與原始碼層級物件之間的映射。原始碼層級程式的描述維護在 LLVM 元資料中,採用實作定義的格式(C/C++ 前端目前使用 DWARF 3 標準 的第 7 版工作草案)。

當程式正在除錯時,除錯器會與使用者互動,並將儲存的除錯資訊轉換為原始語言特定的資訊。因此,除錯器必須了解原始語言,因此與特定語言或語言家族相關聯。

除錯資訊消費者

除錯資訊的作用是提供通常在編譯過程中剝離的中繼資訊。此中繼資訊為 LLVM 使用者提供了產生的程式碼與原始程式碼之間的關係。

目前,除錯資訊有兩個後端消費者:DwarfDebug 和 CodeViewDebug。DwarfDebug 產生適用於 GDB、LLDB 和其他基於 DWARF 的除錯器的 DWARF。CodeViewDebug 產生 CodeView,Microsoft 除錯資訊格式,可用於 Microsoft 除錯器,例如 Visual Studio 和 WinDBG。LLVM 的除錯資訊格式主要源自 DWARF 並受其啟發,但可以轉換為其他目標除錯資訊格式,例如 STABS。

使用除錯資訊來饋送分析產生程式碼的效能分析工具,或用於從產生的程式碼重建原始碼的工具也是合理的。

除錯資訊與最佳化

LLVM 除錯資訊的極高優先順序是使其與最佳化和分析良好地互動。特別是,LLVM 除錯資訊提供以下保證:

  • 無論執行了哪些 LLVM 最佳化,LLVM 除錯資訊始終提供資訊以準確讀取程式的原始碼層級狀態如何更新除錯資訊:LLVM Pass 作者指南 說明了應如何在各種程式碼轉換中更新除錯資訊,以避免破壞此保證,以及如何盡可能保留有用的除錯資訊。請注意,某些最佳化可能會影響使用除錯器修改程式目前狀態的能力,例如設定程式變數或呼叫已刪除的函式。

  • 根據需要,可以升級 LLVM 最佳化以感知除錯資訊,從而允許它們在執行積極的最佳化時更新除錯資訊。這表示,透過努力,LLVM 最佳化器可以像最佳化非除錯程式碼一樣最佳化除錯程式碼。

  • LLVM 除錯資訊不會阻止最佳化發生(例如內聯、基本區塊重新排序/合併/清理、尾部複製等)。

  • LLVM 除錯資訊與程式的其餘部分一起自動最佳化,使用現有的設施。例如,重複的資訊會由連結器自動合併,而未使用的資訊會自動移除。

基本上,除錯資訊允許您使用「-O0 -g」編譯程式並獲得完整的除錯資訊,讓您可以從除錯器任意修改執行中的程式。使用「-O3 -g」編譯程式可為您提供始終可用且準確讀取的完整除錯資訊(例如,儘管進行了尾部呼叫消除和內聯,您仍可獲得準確的堆疊追蹤),但您可能會失去修改程式和呼叫已從程式中最佳化掉或完全內聯掉的函式的能力。

LLVM 測試套件提供了一個框架來測試最佳化器對除錯資訊的處理。它可以像這樣執行:

% cd llvm/projects/test-suite/MultiSource/Benchmarks  # or some other level
% make TEST=dbgopt

這將測試除錯資訊對最佳化 Pass 的影響。如果除錯資訊影響了最佳化 Pass,則會將其報告為失敗。有關 LLVM 測試基礎架構以及如何執行各種測試的更多資訊,請參閱LLVM 測試基礎架構指南

除錯資訊格式

LLVM 除錯資訊經過仔細設計,使最佳化器有可能在不必了解任何關於除錯資訊的情況下最佳化程式和除錯資訊。特別是,元資料的使用從一開始就避免了重複的除錯資訊,並且全域無效程式碼消除 Pass 會在決定刪除函式時自動刪除函式的除錯資訊。

為此,大多數除錯資訊(類型、變數、函式、原始檔等的描述符)由語言前端以 LLVM 元資料的形式插入。

除錯資訊旨在與目標除錯器和除錯資訊表示法(例如 DWARF/Stabs/etc)無關。它使用通用 Pass 來解碼表示變數、類型、函式、命名空間等的資訊:這允許使用任意原始語言語意和類型系統,只要有為目標除錯器編寫的模組來解釋資訊即可。

為了提供基本功能,LLVM 除錯器確實必須對正在除錯的原始碼層級語言做出一些假設,儘管它將這些假設保持在最低限度。LLVM 除錯器假設存在的唯一常見功能是原始檔程式物件。除錯器使用這些抽象物件來形成堆疊追蹤、顯示關於區域變數的資訊等。

本文件的此章節首先描述任何原始語言通用的表示法方面。C/C++ 前端特定的除錯資訊 描述了 C 和 C++ 前端使用的資料佈局慣例。

除錯資訊描述符是專門的元資料節點,是 Metadata 的第一類子類別。

有兩種模型用於定義程式不同狀態下原始變數的值,並透過最佳化和程式碼產生追蹤這些值:除錯紀錄,目前的預設值,以及內建函式呼叫,它是非預設值,但目前為了向後相容性而支援 - 儘管這兩個模型絕不能在一個 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>

除錯紀錄始終以比指令多一個層級的縮排列印,並且始終具有前綴 #dbg_ 和以括號括起來的逗號分隔引數列表,與 call 一樣。

#dbg_declare

#dbg_declare([Value|MDNode], DILocalVariable, DIExpression, DILocation)

此紀錄提供關於本機元素(例如,變數)的資訊。第一個引數是與變數位址對應的 SSA 值,通常是函式入口區塊中的靜態 alloca。第二個引數是區域變數,其中包含變數的描述。第三個引數是複雜的表達式。第四個引數是原始碼位置#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(...)

前端應在原始變數宣告點精確產生一個 #dbg_declare 紀錄。將變數從記憶體完全提升到 SSA 值的最佳化 Pass 會將此紀錄替換為可能多個 #dbg_value` 紀錄。刪除儲存的 Pass 實際上是部分提升,它們將插入 #dbg_value 紀錄的混合,以在可用時追蹤原始變數值。最佳化後,可能有多個 #dbg_declare 紀錄描述變數駐留在記憶體中的程式點。對同一具體原始變數的所有呼叫都必須在記憶體位置上達成一致。

#dbg_value

#dbg_value([Value|DIArgList|MDNode], DILocalVariable, DIExpression, DILocation)

當使用者原始變數設定為新值時,此紀錄提供資訊。第一個引數是新值。第二個引數是區域變數,其中包含變數的描述。第三個引數是複雜的表達式。第四個引數是原始碼位置

#dbg_value 紀錄直接描述原始變數的,而不是其位址。請注意,此內建函式的值運算元可能是間接的(即,指向原始變數的指標),前提是解釋複雜的表達式可以推導出直接值。

#dbg_assign

#dbg_assign( [Value|DIArgList|MDNode] Value,
             DILocalVariable Variable,
             DIExpression ValueExpression,
             DIAssignID ID,
             [Value|MDNode] Address,
             DIExpression AddressExpression,
             DILocation SourceLocation )

此紀錄標記 IR 中發生原始碼指派的位置。它編碼變數的值。它引用執行指派的儲存(如果有的話)和目的地位置。

前三個引數與 #dbg_value 的引數相同。第四個引數是 DIAssignID,用於引用儲存。第五個是儲存的目的地,第六個是修改它的複雜的表達式,第七個是原始碼位置

有關更多資訊,請參閱除錯資訊指派追蹤

除錯器內建函式

在內建模式下,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 指令的元資料來編碼行號和作用域資訊。例如,考慮以下 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 編碼,!4子程式描述符。這樣,紀錄的位置資訊參數表示變數 X 在函式 foo 中的函式層級作用域的第 2 行宣告。

現在讓我們來看另一個範例。

#dbg_declare(ptr %Z, !16, !DIExpression(), !18)
; [debug line = 5:11] [debug variable = Z]

第三個紀錄 #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 在詞法作用域 !17 內的第 5 行和第 11 列宣告。詞法作用域本身位於上面描述的子程式 !4 內。

附加到每個指令的作用域資訊提供了一種直接的方法來查找作用域涵蓋的指令。

最佳化程式碼中的物件生命週期

在上面的範例中,每個變數指派都唯一對應於對堆疊上變數位置的記憶體儲存。但是,在經過大量最佳化的程式碼中,LLVM 會將大多數變數提升為 SSA 值,這些值最終可以放置在實體暫存器或記憶體位置中。為了在編譯過程中追蹤 SSA 值,當物件提升為 SSA 值時,會為每個指派建立一個 #dbg_value 紀錄,記錄變數的新位置。與 #dbg_declare 紀錄相比

  • #dbg_value 終止任何先前 #dbg_values 對於指定變數(的任何重疊片段)的效果。

  • #dbg_value 在 IR 中的位置定義了指令串流中變數值變更的位置。

  • 運算元可以是常數,表示變數被指派一個常數值。

在最佳化 Pass 變更或移動指令和區塊時,必須小心更新 #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 中的兩個原始碼層級變數。函式可能會被最佳化為以下程式碼

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 的第二個、第三個和第四個 #dbg_value 的運算元 (%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!1 具有常數值零的同時具有 @gazonk() 的傳回值 – 一對永遠不會在未最佳化程式中發生的指派。為了避免這種情況,我們必須在 !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 為空。

這類終止變數位置的 #dbg_value 稱為「kill #dbg_value」或「kill location」,並且由於歷史原因,術語「undef #dbg_value」可能會在現有程式碼中使用。DbgVariableIntrinsic 方法 isKillLocationsetKillLocation 應盡可能使用,而不是直接檢查位置運算元來檢查或設定 #dbg_value 是否為 kill location。

一般來說,如果任何 #dbg_value 的運算元被最佳化掉且無法恢復,則必須使用 kill #dbg_value 來終止較早的變數位置。當除錯器可以觀察到指派的重新排序時,可能需要額外的 kill #dbg_values。

變數位置元資料在 CodeGen 期間如何轉換

LLVM 在中階和後端 Pass 中保留除錯資訊,最終產生原始碼層級資訊與指令範圍之間的映射。對於行號資訊來說,這相對簡單,因為將指令映射到行號是一種簡單的關聯。但是,對於變數位置,情況更複雜。由於每個 #dbg_value 紀錄都表示將值指派給原始變數的原始碼層級指派,因此除錯紀錄有效地在 LLVM IR 中嵌入了一個小型命令式程式。到 CodeGen 結束時,這將變成從每個變數到其在指令範圍內的機器位置的映射。從 IR 到物件發射,影響變數位置保真度的主要轉換是

  1. 指令選擇

  2. 暫存器分配

  3. 區塊佈局

下面將分別討論每個轉換。此外,指令排程可能會顯著變更程式的順序,並且發生在許多不同的 Pass 中。

某些變數位置在 CodeGen 期間不會轉換。由 #dbg_declare 指定的堆疊位置在函式的整個持續時間內都有效且不變,並記錄在簡單的 MachineFunction 表中。函式序言和結語中的位置變更也被忽略:框架設定和銷毀可能需要多個指令,需要在輸出二進位檔中提供不成比例的除錯資訊來描述,並且無論如何都應由除錯器逐步跳過。

指令選擇和 MIR 中的變數位置

指令選擇從 IR 函式建立 MIR 函式,就像它將 intermediate 指令轉換為機器指令一樣,intermediate 變數位置也必須變成機器變數位置。在 IR 中,變數位置始終由 Value 識別,但在 MIR 中,可能存在不同類型的變數位置。此外,某些 IR 位置變得不可用,例如,如果多個 IR 指令的操作合併為一個機器指令(例如乘法累加),則會遺失中間 Value。為了在指令選擇中追蹤變數位置,首先將它們分為不依賴程式碼產生的位置(常數、堆疊位置、已分配的虛擬暫存器)和依賴程式碼產生的位置。對於依賴程式碼產生的位置,除錯元資料會附加到 SelectionDAG 中的 SDNode。在發生指令選擇並建立 MIR 函式後,如果與除錯元資料關聯的 SDNode 分配了虛擬暫存器,則該虛擬暫存器將用作變數位置。如果 SDNode 折疊到機器指令中或以其他方式轉換為非暫存器,則變數位置將變得不可用。

不可用的位置被視為已被最佳化掉:在 IR 中,位置將由除錯紀錄指派 undef,而在 MIR 中,使用等效的位置。

在將 MIR 位置指派給每個變數後,會插入對應於每個 #dbg_value 紀錄的機器偽指令。此類指令有兩種形式。

第一種形式 DBG_VALUE 如下所示

DBG_VALUE %1, $noreg, !123, !DIExpression()
並具有以下運算元
  • 第一個運算元可以將變數位置記錄為暫存器、框架索引、立即值,或者如果原始除錯紀錄引用了記憶體,則記錄為基底位址暫存器。$noreg 表示變數位置未定義,等效於 undef #dbg_value 運算元。

  • 第二個運算元的類型表示 DBG_VALUE 是直接引用變數位置,還是間接引用。 $noreg 暫存器表示前者,立即運算元 (0) 表示後者。

  • 運算元 3 是原始除錯紀錄的 Variable 欄位。

  • 運算元 4 是原始除錯紀錄的 Expression 欄位。

第二種形式 DBG_VALUE_LIST 如下所示

DBG_VALUE_LIST !123, !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_arg, 1, DW_OP_plus), %1, %2
並具有以下運算元
  • 第一個運算元是原始除錯紀錄的 Variable 欄位。

  • 第二個運算元是原始除錯紀錄的 Expression 欄位。

  • 從第 3 個運算元開始的任何數量的運算元都記錄了變數位置運算元的序列,這些運算元可以採用與上述 DBG_VALUE 指令的第一個運算元相同的值。這些變數位置運算元插入到最終 DWARF Expression 中,位置由 DIExpression 中 DW_OP_LLVM_arg 運算子指示。

插入 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
}

如果使用 llc -o - -start-after=codegen-prepare -stop-after=expand-isel-pseudos -mtriple=x86_64-- 編譯此 IR,則會產生以下 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

首先觀察到每個 #dbg_value 紀錄在原始碼 IR 中都有一個 DBG_VALUE 指令,確保沒有遺失任何原始碼層級指派。然後考慮記錄變數位置的不同方式

  • 對於第一個 #dbg_value,使用立即運算元來記錄零值。

  • PHI 指令的 #dbg_value 會導致虛擬暫存器 %0 的 DBG_VALUE。

  • 第一個 GEP 的效果已折疊到第一個載入指令中(作為 4 位元組偏移),但變數位置透過將 GEP 的效果折疊到 DIExpression 中而得到挽救。

  • 第二個 GEP 也折疊到對應的載入中。但是,它不夠簡單以至於無法挽救,並且作為 $noreg DBG_VALUE 發出,表示變數採用未定義的位置。

  • 最後一個 #dbg_value 的 Value 放置在虛擬暫存器 %1 中。

指令排程

許多趟編譯程序可以重新排程指令,特別是指令選擇以及前與後暫存器配置(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 將同時更改值。相較於錯誤地呈現原始程式,這更為可取。

相比之下,如果將 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 趟編譯程序運行以實現兩個目標

  • 通過複製和暫存器溢出傳播變數的位置,

  • 對於每個區塊,記錄該區塊中每個有效的變數位置。

在此趟編譯程序之後,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 中的命令式程式,每個記錄定義一個變數位置。這可以通過 mem2reg 轉換為 SSA 形式,就像它使用 use-def 鏈來識別控制流程合併並為 IR 值插入 phi 節點一樣。但是,由於偵錯變數位置是為每個機器指令定義的,實際上每個 IR 指令都使用每個變數位置,這將導致生成大量的偵錯記錄。

檢查上面的範例,變數 !30 在函數的兩個條件路徑上都被賦值為 %input,而 !23 在任一路徑上都被賦值為不同的常數值。在控制流程在 %bb1 中合併的位置,我們希望 !30 保留其位置 (%input),但 !23 變為未定義,因為我們無法在沒有插入 PHI 節點的情況下在 %bb1 中確定它應該具有什麼值。mem2reg 不會插入 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++ 中,可以使用 C++11 關鍵字 deleted 宣告特殊成員函數 Ctors、Dtors、Copy/Move Ctors、賦值運算子。這在 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 函數資訊

定義了一些 DWARF 屬性來支援 Fortran 程式的客戶端偵錯。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)

定義了一些 DWARF 標籤來表示 Fortran 特定的結構,即 DW_TAG_string_type 用於表示 Fortran character(n)。在 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 屬性的編碼。此提案描述了 DWARF 擴展以編碼 Objective C 屬性,偵錯器可以使用這些擴展來讓開發人員檢查 Objective C 屬性。

提案

Objective C 屬性與類別成員分開存在。屬性可以僅由「setter」和「getter」選擇器定義,並且在每次存取時重新計算。或者屬性可以只是對某些宣告的 ivar 的直接存取。最後,它可以讓編譯器為其「自動合成」ivar,在這種情況下,屬性可以在使用者程式碼中使用標準 C 取值語法以及通過屬性「點」語法直接引用,但在 @interface 宣告中沒有對應於此 ivar 的條目。

為了方便偵錯,對於類別的 DW_TAG_structure_type 定義中的這些屬性,我們將添加一個新的 DWARF TAG,以保存給定屬性的描述,以及一組提供所述描述的 DWARF 屬性。屬性標籤還將包含屬性的名稱和宣告的類型。

如果存在相關的 ivar,則還將在該 ivar 的 DW_TAG_member DIE 中放置一個 DWARF 屬性,該屬性指回該屬性的屬性 TAG。在編譯器直接合成 ivar 的情況下,預期編譯器會為該 ivar 生成 DW_TAG_memberDW_AT_artificial 設定為 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 中使用不同的屬性宣告是很常見的做法 - 例如,在介面中提供唯讀屬性,在實作中提供讀寫介面。在這種情況下,編譯器應發射當前翻譯單元中生效的任何屬性宣告。

開發人員可以使用 DW_AT_APPLE_property_attribute 編碼的屬性裝飾屬性。

@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)

setter 和 getter 方法名稱使用 DW_AT_APPLE_property_setterDW_AT_APPLE_property_getter 屬性附加到屬性。

@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 )

新的 DWARF 標籤

標籤

DW_TAG_APPLE_property

0x4200

新的 DWARF 屬性

屬性

類別

DW_AT_APPLE_property

0x3fed

參考

DW_AT_APPLE_property_getter

0x3fe9

字串

DW_AT_APPLE_property_setter

0x3fea

字串

DW_AT_APPLE_property_attribute

0x3feb

常數

新的 DWARF 常數

名稱

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

名稱加速器表

簡介

.debug_pubnames」和「.debug_pubtypes」格式不是偵錯器需要的格式。「pub」在區段名稱中表示表中的條目僅是公開可見的名稱。這表示「.debug_pubnames」中不會顯示靜態或隱藏函數。「.debug_pubtypes」中沒有靜態變數或私有類別變數。許多編譯器向這些表中添加了不同的內容,因此我們不能依賴 gcc、icc 或 clang 之間的內容。

使用者給出的典型查詢傾向於與這些表的內容不符。例如,DWARF 規範指出,「對於 C++ 結構、類別或聯合的函數成員或靜態資料成員的名稱,在「.debug_pubnames」區段中呈現的名稱不是 DW_AT_name attribute 的引用偵錯資訊條目的簡單名稱,而是資料或函數成員的完整限定名稱。」因此,對於複雜的 C++ 條目,這些表中唯一的名稱是完整限定名稱。偵錯器使用者傾向於不將其搜尋字串輸入為「a::b::c(int,const Foo&) const」,而是輸入為「c」、「b::c」或「a::b::c」。因此,必須對名稱表中輸入的名稱進行反裝飾,以便適當地將其分割,並且必須手動將其他名稱輸入到表中,以使其作為偵錯器使用的名稱查找表有效。

由於其不一致且無用的僅公開名稱內容使其成為物件檔案中空間的浪費,因此目前所有偵錯器都忽略「.debug_pubnames」表。這些表在寫入磁碟時,不會以任何方式排序,從而使每個偵錯器都執行自己的剖析和排序。這些表還包括表中字串值的內嵌副本,這使得表在磁碟上比它們需要的要大得多,特別是對於大型 C++ 程式。

我們不能通過將我們需要的所有名稱添加到此表中來修復這些區段嗎?不能,因為這不是定義表要包含的內容,我們也不知道舊的壞表和新的好表之間的區別。充其量,我們可以製作自己的重新命名的區段,其中包含我們需要的所有資料。

對於像 LLDB 這樣的偵錯器來說,這些表也不夠用。LLDB 使用 clang 進行表達式剖析,其中 LLDB 充當 PCH。然後,LLDB 經常被要求查找類型「foo」或命名空間「bar」,或列出命名空間「baz」中的項目。pubnames 或 pubtypes 表中不包含命名空間。由於 clang 在剖析表達式時會提出很多問題,因此我們需要非常快速地查找名稱,因為這種情況經常發生。擁有針對非常快速查找進行最佳化的新加速器表將極大地有益於這種偵錯體驗。

我們希望生成可以從磁碟映射到記憶體中並按原樣使用的名稱查找表,而無需進行很少或不進行任何預先剖析。我們還可以控制這些不同表的確切內容,以便它們完全包含我們需要的內容。名稱加速器表旨在解決這些問題。為了解決這些問題,我們需要

  • 具有可以從磁碟映射到記憶體中並按原樣使用的格式

  • 查找應該非常快速

  • 可擴展的表格式,以便這些表可以由許多產生者製作

  • 開箱即用即可包含典型查找所需的所有名稱

  • 表內容的嚴格規則

表大小很重要,加速器表格式應允許重複使用來自通用字串表的字串,以便名稱的字串不會重複。我們還希望確保表已準備好按原樣使用,只需將表映射到記憶體中並進行最少的標頭剖析即可。

名稱查找需要快速,並針對偵錯器傾向於執行的查找類型進行最佳化。理想情況下,我們希望在執行名稱查找時盡可能少地接觸映射表的部分,並且能夠快速找到我們正在尋找的名稱條目,或發現沒有匹配項。在偵錯器的情況下,我們針對大多數時候失敗的查找進行了最佳化。

定義的每個表都應該對加速器表中包含的內容以及記錄的內容有嚴格的規則,以便客戶端可以依賴這些內容。

雜湊表

標準雜湊表

典型的雜湊表具有標頭、儲存桶,並且每個儲存桶都指向儲存桶內容

.------------.
|  HEADER    |
|------------|
|  BUCKETS   |
|------------|
|  DATA      |
`------------'

BUCKETS 是一個偏移陣列,指向每個雜湊的 DATA

.------------.
| 0x00001000 | BUCKETS[0]
| 0x00002000 | BUCKETS[1]
| 0x00002200 | BUCKETS[2]
| 0x000034f0 | BUCKETS[3]
|            | ...
| 0xXXXXXXXX | BUCKETS[n_buckets]
'------------'

因此,對於上面範例中的 bucket[3],我們有一個表偏移 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
            `------------'

偵錯器的這種佈局問題在於,我們需要針對我們正在搜尋的符號不存在的負面查找情況進行最佳化。因此,如果我們要在上表中查找「printf」,我們將為「printf」製作一個 32 位元雜湊,它可能與 bucket[3] 匹配。我們需要轉到偏移 0x000034f0 並開始查看我們的 32 位元雜湊是否匹配。為此,我們需要讀取下一個指標,然後讀取雜湊,比較它,然後跳到下一個儲存桶。每次我們都在記憶體中跳過許多位元組並接觸新頁面,只是為了對完整的 32 位元雜湊進行比較。所有這些存取然後告訴我們我們沒有匹配項。

名稱雜湊表

為了解決上述問題,我們以稍微不同的方式構造了雜湊表:標頭、儲存桶、所有唯一 32 位元雜湊值的陣列,後跟每個雜湊值的雜湊值資料偏移陣列,然後是所有雜湊值的資料

.-------------.
|  HEADER     |
|-------------|
|  BUCKETS    |
|-------------|
|  HASHES     |
|-------------|
|  OFFSETS    |
|-------------|
|  DATA       |
`-------------'

名稱表中的 BUCKETSHASHES 陣列的索引。通過使所有完整的 32 位元雜湊值在記憶體中連續,我們允許自己有效地檢查匹配,同時盡可能少地接觸記憶體。大多數時候,檢查 32 位元雜湊值就是查找的全部內容。如果它確實匹配,它通常是沒有衝突的匹配。因此,對於具有「n_buckets」個儲存桶和「n_hashes」個唯一 32 位元雜湊值的表,我們可以將 BUCKETSHASHESOFFSETS 的內容釐清為

.-------------------------.
|  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] 包含「6」,它是 HASHES 表的索引。然後,只要雜湊在 BUCKETS[3] 中,我們就會比較 HASHES 陣列中的任何連續 32 位元雜湊值。我們通過驗證每個後續雜湊值模數 n_buckets 仍然是 3 來做到這一點。在查找失敗的情況下,我們將存取 BUCKETS[3] 的記憶體,然後比較幾個連續的 32 位元雜湊,然後我們才知道我們沒有匹配項。我們不會最終遍歷記憶體的多個字組,並且我們確實使存取的處理器資料快取行數保持盡可能少。

用於這些查找表的字串雜湊是 Daniel J. Bernstein 雜湊,它也用於 ELF GNU_HASH 區段。對於程式中的各種名稱來說,這是一個非常好的雜湊,雜湊衝突非常少。

空儲存桶通過使用無效的雜湊索引 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
};

標頭以 32 位元「magic」值開始,該值必須是編碼為 ASCII 整數的 'HASH'。這允許檢測雜湊表的開始,並且還允許確定表的位元組順序,以便可以正確提取表。「magic」值之後是一個 16 位元 version 數字,允許在未來修訂和修改表。目前的版本號是 1。hash_function 是一個 uint16_t 列舉,指定用於產生此表的雜湊函數。雜湊函數列舉的目前值包括

enum HashFunctionType
{
  eHashFunctionDJB = 0u, // Daniel J Bernstein hash function
};

bucket_count 是一個 32 位元無符號整數,表示 BUCKETS 陣列中有多少個儲存桶。hashes_countHASHES 陣列中唯一 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 是一個 32 位元索引陣列,指向 hashes 陣列。hashes 陣列包含雜湊表中所有名稱的所有 32 位元雜湊值。hashes 表中的每個雜湊在 offsets 陣列中都有一個偏移量,該偏移量指向雜湊值的資料。

此表設置使得重新利用這些表來包含不同的資料非常容易,同時保持所有表的查找機制相同。此佈局還使得將表儲存到磁碟並稍後映射進來成為可能,並執行非常有效的名稱查找,而無需很少或不進行任何剖析。

DWARF 查找表可以用多種方式實作,並且可以為每個名稱儲存大量資訊。我們希望 DWARF 表格是可擴展的,並且能夠有效率地儲存資料,因此我們使用了一些 DWARF 功能,這些功能可以有效率地儲存資料,以便精確定義我們為每個名稱儲存的資料種類。

HeaderData 包含每個 HashData 區塊內容的定義。我們可能希望為每個名稱儲存指向所有除錯資訊條目 (DIE) 的偏移量。為了保持可擴展性,我們建立了一個項目或 Atom 的列表,這些項目包含在每個名稱的資料中。首先是每個 atom 中資料的類型

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, ...)

然後我們允許每個 atom 類型定義 atom 類型以及每個 atom 類型資料的編碼方式

struct Atom
{
  uint16_t type;  // AtomType enum value
  uint16_t form;  // DWARF DW_FORM_XXX defines
};

上面的 form 類型來自 DWARF 規範,並定義了 Atom 類型資料的精確編碼方式。請參閱 DWARF 規範以了解 DW_FORM_ 定義。

struct HeaderData
{
  uint32_t die_offset_base;
  uint32_t atom_count;
  Atoms    atoms[atom_count0];
};

HeaderData 定義了應新增至使用 DW_FORM_ref1DW_FORM_ref2DW_FORM_ref4DW_FORM_ref8DW_FORM_ref_udata 編碼的任何 atom 的基礎 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;

這將內容定義為 DIE 偏移量 (eAtomTypeDIEOffset),其編碼為 32 位元值 (DW_FORM_data4)。這允許單一名稱在單一檔案中擁有多個相符的 DIE,例如內聯函數可能會出現這種情況。未來的表格可以包含更多關於 DIE 的資訊,例如指示 DIE 是否為函數、方法、區塊或內聯的旗標。

DWARF 表格的 KeyType 是一個 32 位元字串表格偏移量,指向 “.debug_str” 表格。“.debug_str” 是 DWARF 的字串表格,可能已經包含所有字串的副本。這有助於確保在編譯器的協助下,我們可以在所有 DWARF 區段之間重複使用字串,並縮減雜湊表格的大小。編譯器在除錯資訊中將所有字串產生為 DW_FORM_strp 的另一個好處是,可以讓 DWARF 剖析速度更快。

在進行查找後,我們會獲得雜湊資料的偏移量。雜湊資料需要能夠處理 32 位元雜湊衝突,因此雜湊資料中偏移量處的資料區塊由三元組組成

uint32_t str_offset
uint32_t hash_data_count
HashData[hash_data_count]

如果 “str_offset” 為零,則表示 bucket 內容已完成。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,我們有 3 個表格:“.apple_names”、“.apple_types” 和 “.apple_namespaces”。

.apple_names” 區段應包含每個 DWARF DIE 的條目,其 DW_TAGDW_TAG_labelDW_TAG_inlined_subroutineDW_TAG_subprogram,且具有位址屬性:DW_AT_low_pcDW_AT_high_pcDW_AT_rangesDW_AT_entry_pc。它也包含具有位置 DW_OP_addrDW_TAG_variable DIE (全域和靜態變數)。應包含所有全域和靜態變數,包括在函數和類別範圍內的變數。例如,使用以下程式碼

static int var = 0;

void f ()
{
  static int var = 0;
}

兩個靜態 var 變數都將包含在表格中。所有函數都應同時發出其完整名稱和基本名稱。對於 C 或 C++,完整名稱是 mangled name(如果可用),通常在 DW_AT_MIPS_linkage_name 屬性中,而 DW_AT_name 包含函數基本名稱。如果全域或靜態變數在 DW_AT_MIPS_linkage_name 屬性中具有 mangled 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++ 程式庫中解構 mangled name 的 abi::cxa_demangle() 的輸出相符。

語言擴展和檔案格式變更

Objective-C 擴展

.apple_objc” 區段應包含 Objective-C 類別的所有 DW_TAG_subprogram DIE。雜湊表格中使用的名稱是 Objective-C 類別本身的名稱。如果 Objective-C 類別具有類別,則會為不含類別的類別名稱和包含類別的類別名稱建立條目。因此,如果我們在偏移量 0x1234 處有一個 DIE,其名稱為方法 “-[NSString(my_additions) stringWithSpecialString:]”,我們將為指向 DIE 0x1234 的 “NSString” 新增一個條目,並為指向 0x1234 的 “NSString(my_additions)” 新增一個條目。這讓我們可以在執行表達式時快速追蹤 Objective-C 類別的所有 Objective-C 方法。這是必要的,因為 Objective-C 的動態特性,任何人都可以向類別新增方法。Objective-C 方法的 DWARF 發射方式也與 C++ 類別不同,後者的方法通常不包含在類別定義中,而是分散在一個或多個編譯單元中。類別也可以在不同的共享程式庫中定義。因此,我們需要能夠根據 Objective-C 類別名稱快速找到所有方法和類別函數,或快速找到類別 + 類別名稱的所有方法和類別函數。此表格不包含任何 selector 名稱,它僅將 Objective-C 類別名稱(或類別名稱 + 類別)對應到所有方法和類別函數。Selector 作為函數基本名稱新增到 “.debug_names” 區段中。

在 Objective-C 函數的 “.apple_names” 區段中,完整名稱是包含括號的完整函數名稱 (“-[NSString stringWithCString:]”),而基本名稱僅為 selector (“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 支援發射 CodeView,即 Microsoft 除錯資訊格式,本節描述該支援的設計和實作。

格式背景

CodeView 作為一種格式,顯然是以 C++ 除錯為導向的,而在 C++ 中,大多數除錯資訊往往是類型資訊。因此,CodeView 最重要的設計約束是將類型資訊與其他 “符號” 資訊分離,以便可以跨翻譯單元有效率地合併類型資訊。類型資訊和符號資訊通常都儲存為記錄序列,其中每個記錄都以 16 位元記錄大小和 16 位元記錄種類開頭。

類型資訊通常儲存在物件檔案的 .debug$T 區段中。所有其他除錯資訊,例如行資訊、字串表格、符號資訊和內聯資訊,都儲存在一個或多個 .debug$S 區段中。每個物件檔案可能只有一個 .debug$T 區段,因為所有其他除錯資訊都參考它。如果在編譯期間使用了 PDB(由 /Zi MSVC 選項啟用),則 .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 metadata 產生和傾印 CodeView

    $ llc foo.ll -filetype=obj -o foo.obj
    $ llvm-readobj --codeview foo.obj > foo.txt
    

    在 lit 測試案例中使用此模式,並使用 FileCheck 檢查 llvm-readobj 的輸出

改進 LLVM 的 CodeView 支援是一個尋找有趣的類型記錄、建構使 MSVC 發射這些記錄的 C++ 測試案例、傾印記錄、理解它們,然後在 LLVM 後端產生等效記錄的過程。