使用 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,這是微軟的除錯資訊格式,可用於 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

這將測試偵錯資訊對優化流程的影響。如果偵錯資訊影響了優化流程,則會回報為失敗。有關 LLVM 測試基礎設施以及如何執行各種測試的詳細資訊,請參閱 LLVM 測試基礎設施指南

偵錯資訊格式

LLVM 偵錯資訊經過精心設計,可以讓優化器在優化程式和偵錯資訊時,不必了解任何關於偵錯資訊的知識。特別是,使用中繼資料從一開始就避免了重複的偵錯資訊,並且如果全域死碼消除流程決定刪除函式,它會自動刪除該函式的偵錯資訊。

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

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

為了提供基本功能,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 值的最佳化過程,會將此記錄替換為可能多個 #dbg_value` 記錄。刪除存放區的過程實際上是部分提升,它們會插入混合的 #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 編碼,它是一個 子程式描述元。這樣,記錄的位置資訊參數表示變數 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 中的位置定義了變數值在指令流中的何處發生變化。

  • 運算元可以是常數,表示變數被賦予了一個常數值。

當優化遍歷改變或移動指令和區塊時,必須注意更新 #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_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!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_value。

CodeGen 期間變數位址元資料是如何轉換的

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

  1. 指令選擇

  2. 暫存器配置

  3. 區塊配置

以下將分別討論這些內容。此外,指令排程可以顯著改變程式的順序,並且發生在許多不同的過程階段。

某些變數位址在 CodeGen 期間不會被轉換。由 #dbg_declare 指定的堆疊位址在函式的整個生命週期中都是有效的且不變的,並記錄在一個簡單的 MachineFunction 表中。函式序言和結尾中的位址更改也會被忽略:框架設定和銷毀可能需要多個指令,需要在輸出二進制檔案中使用過多的除錯資訊來描述,並且無論如何偵錯器應該跳過這些指令。

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

指令選擇會從 IR 函式建立 MIR 函式,並且就像它將 intermediate 指令轉換為機器指令一樣,intermediate 變數位置也必須成為機器變數位置。在 IR 中,變數位置始終由值識別,但在 MIR 中,可以有不同類型的變數位置。此外,某些 IR 位置會變得不可用,例如,如果多個 IR 指令的操作組合成一個機器指令(例如乘法累加),則中間值會遺失。為了在指令選擇過程中追蹤變數位置,首先將它們分為不依賴程式碼生成的(常數、堆疊位置、已配置的虛擬暫存器)和依賴程式碼生成的。對於那些依賴程式碼生成的,偵錯中繼資料會附加到 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 是原始偵錯記錄的變數欄位。

  • 運算元 4 是原始偵錯記錄的運算式欄位。

第二種形式,DBG_VALUE_LIST,如下所示

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

  • 第二個運算元是原始偵錯記錄的運算式欄位。

  • 從第三個開始的任意數量運算元記錄了一系列變數位置運算元,它們可以採用與上述 DBG_VALUE 指令的第一個運算元相同的任何值。這些變數位置運算元被插入到最終的 DWARF 運算式中,位置由 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

首先請注意,原始 IR 中的每個 #dbg_value 記錄都有一個 DBG_VALUE 指令,確保不會遺漏任何原始碼層級的賦值。然後考慮記錄變數位置的不同方式

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

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

  • 第一個 GEP 的效果被折疊到第一個載入指令中(作為 4 位元組偏移量),但變數位置會透過將 GEP 的效果折疊到 DIExpression 中來保留。

  • 第二個 GEP 也被折疊到相應的載入中。但是,它不夠簡單,無法保留,因此會以 $noreg DBG_VALUE 的形式發出,表示變數採用未定義的位置。

  • 最後一個 #dbg_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 變為未定義,因為我們無法在運行時確定它在 %bb1 中的值,除非插入 PHI 節點。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、複製/移動建構函式、賦值運算子。這在 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 特定的結構,例如用於表示 Fortran character(n) 的 DW_TAG_string_type。在 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 屬性進行編碼的 DWARF 擴充,除錯器可以使用這些擴充讓開發人員檢查 Objective C 屬性。

提案

Objective C 屬性獨立於類別成員存在。屬性可以僅由「設定器」和「獲取器」選擇器定義,並在每次存取時重新計算。或者,屬性可以只是對某些已宣告 ivar 的直接存取。最後,編譯器可以為其「自動合成」一個 ivar,在這種情況下,可以使用標準 C 解引用語法以及屬性「點」語法在使用者程式碼中直接引用該屬性,但在 @interface 宣告中沒有對應於此 ivar 的項目。

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

如果有相關的 ivar,則在該 ivar 的 DW_TAG_member DIE 中也會放置一個 DWARF 屬性,該屬性反向引用該屬性的 TAG。在編譯器直接合成 ivar 的情況下,預計編譯器會為該 ivar 產生一個 DW_TAG_member(將 DW_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)

設定器和獲取器方法名稱使用 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 屬性給定的簡單名稱,而是資料或函數成員的完整合格名稱。」 因此,這些表格中複雜 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,它指向該儲存區的條目鏈。每個儲存區都必須包含一個 next 指標、完整的 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 位元雜湊值是否相符。為此,我們需要讀取 next 指標,然後讀取雜湊值、進行比較,並跳到下一個儲存區。每次我們都會跳過記憶體中的許多位元組,並觸及新的頁面,只是為了比較完整的 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,它們包含在每個名稱的數據中。首先是每個原子中的數據類型

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 定義了基礎 DIE 位移,該位移應加到任何使用 DW_FORM_ref1DW_FORM_ref2DW_FORM_ref4DW_FORM_ref8DW_FORM_ref_udata 編碼的 atom。 它還定義了每個 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 是指向「.debug_str」表的 32 位元字串表位移。「.debug_str」是 DWARF 的字串表,它可能已經包含所有字串的副本。 這有助於確保在編譯器的幫助下,我們在所有 DWARF 區段之間重複使用字串,並減少雜湊表的大小。 讓編譯器在偵錯資訊中將所有字串生成為 DW_FORM_strp 的另一個好處是,可以大幅加快 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,我們有 3 個表格:「.apple_names」、「.apple_types」和「.apple_namespaces」。

.apple_names」區段應包含每個 DWARF DIE 的條目,其 DW_TAG 為具有地址屬性 DW_AT_low_pcDW_AT_high_pcDW_AT_rangesDW_AT_entry_pcDW_TAG_labelDW_TAG_inlined_subroutineDW_TAG_subprogram。 它還包含位置中具有 DW_OP_addrDW_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。如果我們遇到沒有名稱的命名空間,則這是一個匿名命名空間,並且名稱應輸出為「(匿名命名空間)」(不帶引號)。為什麼?這與標準 C++ 程式庫中用於解碼修飾後名稱的 abi::cxa_demangle() 的輸出相符。

語言擴展和文件格式變更

Objective-C 擴展

.apple_objc」區段應該包含 Objective-C 類別的所有 DW_TAG_subprogram DIE。雜湊表中使用的名稱是 Objective-C 類別本身的名稱。如果 Objective-C 類別有種類,則會為沒有種類的類別名稱和具有種類的類別名稱建立一個項目。因此,如果我們在偏移量 0x1234 處有一個名稱為「-[NSString(my_additions) stringWithSpecialString:]」方法的 DIE,我們會為「NSString」添加一個指向 DIE 0x1234 的項目,並為「NSString(my_additions)」添加一個指向 0x1234 的項目。這使我們能夠在進行表達式時快速追蹤 Objective-C 類別的所有 Objective-C 方法。這是必要的,因為 Objective-C 的動態特性允許任何人向類別添加方法。Objective-C 方法的 DWARF 發射方式也不同於 C++ 類別,在 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 支援發射 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 中繼資料生成和傾印 CodeView

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

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

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