如何更新除錯資訊:LLVM Pass 作者指南¶
簡介¶
某些程式碼轉換可能會不經意地導致除錯資訊遺失,或更糟的是,使除錯資訊錯誤地表示程式的狀態。
本文件說明如何在各種程式碼轉換中正確更新除錯資訊,並針對如何為任意轉換建立目標明確的除錯資訊測試提供建議。
有關 LLVM 除錯資訊背後的哲學,請參閱 使用 LLVM 進行原始碼層級除錯。
更新除錯位置的規則¶
何時保留指令位置¶
如果指令保留在其基本區塊中,或者其基本區塊被摺疊到一個無條件分支的前導區塊中,則轉換應保留指令的除錯位置。要使用的 API 是 IRBuilder
或 Instruction::setDebugLoc
。
此規則的目的是確保常見的區塊本地最佳化保留在與其觸及的指令相對應的原始碼位置設定中斷點的能力。如果失去這種能力,除錯、崩潰日誌和 SamplePGO 的準確性將會受到嚴重影響。
應遵循此規則的轉換範例包括
指令排程。區塊本地指令重新排序不應捨棄原始碼位置,即使這可能會導致跳躍式的單步執行行為。
簡單的跳躍線程化。例如,如果區塊
B1
無條件跳躍到B2
,並且 是其唯一的前導區塊,則可以將B2
中的指令提升到B1
中。B2
的原始碼位置應保留。窺孔最佳化,例如替換或展開指令,如
(add X X) => (shl X 1)
。shl
指令的位置應與add
指令的位置相同。尾部複製。例如,如果區塊
B1
和B2
都無條件分支到B3
,並且B3
可以摺疊到其前導區塊中,則應保留B3
的原始碼位置。
此規則不適用於以下轉換範例,包括
LICM。例如,如果指令從迴圈主體移動到前置標頭,則適用 捨棄位置 的規則。
除了上述規則外,如果目標區塊已經包含具有相同除錯位置的指令,則在基本區塊之間移動指令的轉換也應保留該指令的除錯位置。
應遵循此規則的轉換範例包括
在基本區塊之間移動指令。例如,如果
BB1
中的指令I1
移動到BB2
中的I2
之前,如果I1
的原始碼位置與I2
的原始碼位置相同,則可以保留I1
的原始碼位置。
何時合併指令位置¶
如果轉換將多個指令替換為一個或多個新指令,並且 新指令產生多個原始指令的輸出,則轉換應合併指令位置。要使用的 API 是 Instruction::applyMergedLocation
。對於每個新指令 I,其新位置應為所有輸出由 I 產生的指令位置的合併。通常,這包括任何被新指令 RAUW 的指令,但不包括任何僅產生 RAUW 指令使用的中間值的指令。
此規則的目的是確保 a) 單個合併指令具有附加準確範圍的位置,以及 b) 防止誤導性的單步執行(或中斷點)行為。通常,合併指令是可能陷入陷阱的記憶體存取:附加準確的範圍極大地有助於崩潰分類,方法是識別發生錯誤記憶體存取的(可能內聯的)函數。此規則也旨在協助 SamplePGO,方法是禁止將包含合併指令的區塊樣本錯誤歸因於包含要合併的指令之一的區塊的情況。
應遵循此規則的轉換範例包括
從條件分支的所有後繼者提升相同的指令,或將這些指令從所有路徑沉降到後支配區塊。例如,合併在 CFG 菱形的兩側發生的相同載入/儲存(請參閱
MergedLoadStoreMotion
pass)。對於每組被提升/沉降的相同指令,應將其所有位置的合併應用於合併後的指令。合併相同的迴圈不變儲存(請參閱 LICM 工具
llvm::promoteLoopAccessesToScalars
)。純量指令被組合到向量指令中,例如
(add A1, B1), (add A2, B2) => (add (A1, A2), (B1, B2))
。由於新的向量add
同時計算兩個原始add
指令的結果,因此它應使用兩個位置的合併。同樣地,如果先前的最佳化已經產生向量(A1, A2)
和(B2, B1)
,那麼我們可能會建立一個(shufflevector (1, 0), (B2, B1))
指令來產生向量add
的(B1, B2)
;在這種情況下,我們建立了兩個指令來替換原始的adds
,因此兩個新指令都應使用合併的位置。
此規則不適用於以下轉換範例,包括
區塊本地窺孔,用於刪除冗餘指令,例如
(sext (zext i8 %x to i16) to i32) => (zext i8 %x to i32)
。內部的zext
已修改但仍保留在其區塊中,因此應適用 保留位置 的規則。將多個指令組合在一起的窺孔最佳化,例如
(add (mul A B) C) => llvm.fma.f32(A, B, C)
。請注意,mul
的結果不再出現在程式中,而add
的結果現在由fma
產生,因此應使用add
的位置。將 if-then-else CFG 菱形轉換為
select
。保留推測指令的除錯位置可能會讓人覺得條件為真,但實際上並非如此(反之亦然),這會導致混淆的單步執行體驗。此處應適用 捨棄位置 的規則。提升/沉降會使位置在先前無法到達時變成可到達。考慮從具有三個 case 的 switch 的前兩個 case 中提升兩個具有相同位置的相同指令。合併它們的位置將使前兩個 case 的位置在採用第三個 case 時可到達。適用 捨棄位置 的規則。
何時捨棄指令位置¶
如果 保留 和 合併 除錯位置的規則不適用,則轉換應捨棄除錯位置。要使用的 API 是 Instruction::dropLocation()
。
此規則的目的是防止在指令與原始碼位置沒有明確、明確關係的情況下,出現不穩定或誤導性的單步執行行為。
為了處理沒有位置的指令,DWARF 產生器預設為允許標籤後最後設定的位置向前級聯,或者在沒有先前位置可用的情況下,設定具有可行範圍資訊的第 0 行位置。
有關捨棄位置規則何時適用的範例,請參閱有關 合併位置 的章節中的討論。
更新除錯值的規則¶
刪除 IR 層級指令¶
當 Instruction
被刪除時,其除錯用途會變更為 undef
。這是除錯資訊的遺失:一個或多個原始碼變數的值變得不可用,從 #dbg_value(undef, ...)
開始。當沒有辦法重建遺失指令的值時,這是最好的結果。但是,通常可以做得更好
如果可以 RAUW 垂死指令,請執行此操作。
Value::replaceAllUsesWith
API 透明地更新垂死指令的除錯用途,以指向替換值。如果無法 RAUW 垂死指令,請對其呼叫
llvm::salvageDebugInfo
。這會盡最大努力透過將垂死指令的效果描述為DIExpression
來重寫其除錯用途。如果垂死指令的其中一個運算元將變得微不足道地無用,請使用
llvm::replaceAllDbgUsesWith
來重寫該運算元的除錯用途。考慮以下範例函數
define i16 @foo(i16 %a) {
%b = sext i16 %a to i32
%c = and i32 %b, 15
#dbg_value(i32 %c, ...)
%d = trunc i32 %c to i16
ret i16 %d
}
現在,這是不必要的截斷指令 %d
被簡化指令替換後發生的情況
define i16 @foo(i16 %a) {
#dbg_value(i32 undef, ...)
%simplified = and i16 %a, 15
ret i16 %simplified
}
請注意,在刪除 %d
後,其運算元 %c
的所有用途都變得微不足道地無用。曾經指向 %c
的除錯用途現在為 undef
,並且除錯資訊不必要地遺失。
為了解決這個問題,請執行
llvm::replaceAllDbgUsesWith(%c, theSimplifiedAndInstruction, ...)
這會產生更好的除錯資訊,因為保留了 %c
的除錯用途
define i16 @foo(i16 %a) {
%simplified = and i16 %a, 15
#dbg_value(i16 %simplified, ...)
ret i16 %simplified
}
您可能已經注意到 %simplified
比 %c
窄:這不是問題,因為 llvm::replaceAllDbgUsesWith
會負責將必要的轉換操作插入到更新的除錯用途的 DIExpression 中。
刪除 MIR 層級 MachineInstr¶
待辦事項
更新 DIAssignID
附件的規則¶
DIAssignID
metadata 附件由 Assignment Tracking 使用,目前這是一種實驗性除錯模式。
有關如何更新它們以及有關 Assignment Tracking 的更多資訊,請參閱 除錯資訊 Assignment Tracking。
如何自動將測試轉換為除錯資訊測試¶
IR 層級轉換的變異測試¶
在許多情況下,轉換的 IR 測試案例可以自動變異,以測試該轉換內的除錯資訊處理。這是一種測試正確除錯資訊處理的簡單方法。
debugify
工具 pass¶
debugify
測試工具只是一對 pass:debugify
和 check-debugify
。
第一個 pass 將合成除錯資訊應用於模組的每個指令,第二個 pass 檢查在最佳化發生後此 DI 是否仍然可用,並在此過程中報告任何錯誤/警告。
指令被分配依序遞增的行位置,並在所有可能的地方立即被除錯值記錄使用。
例如,這是之前的模組
define void @f(i32* %x) {
entry:
%x.addr = alloca i32*, align 8
store i32* %x, i32** %x.addr, align 8
%0 = load i32*, i32** %x.addr, align 8
store i32 10, i32* %0, align 4
ret void
}
以及執行 opt -debugify
之後
define void @f(i32* %x) !dbg !6 {
entry:
%x.addr = alloca i32*, align 8, !dbg !12
#dbg_value(i32** %x.addr, !9, !DIExpression(), !12)
store i32* %x, i32** %x.addr, align 8, !dbg !13
%0 = load i32*, i32** %x.addr, align 8, !dbg !14
#dbg_value(i32* %0, !11, !DIExpression(), !14)
store i32 10, i32* %0, align 4, !dbg !15
ret void, !dbg !16
}
!llvm.dbg.cu = !{!0}
!llvm.debugify = !{!3, !4}
!llvm.module.flags = !{!5}
!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "debugify", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, enums: !2)
!1 = !DIFile(filename: "debugify-sample.ll", directory: "/")
!2 = !{}
!3 = !{i32 5}
!4 = !{i32 2}
!5 = !{i32 2, !"Debug Info Version", i32 3}
!6 = distinct !DISubprogram(name: "f", linkageName: "f", scope: null, file: !1, line: 1, type: !7, isLocal: false, isDefinition: true, scopeLine: 1, isOptimized: true, unit: !0, retainedNodes: !8)
!7 = !DISubroutineType(types: !2)
!8 = !{!9, !11}
!9 = !DILocalVariable(name: "1", scope: !6, file: !1, line: 1, type: !10)
!10 = !DIBasicType(name: "ty64", size: 64, encoding: DW_ATE_unsigned)
!11 = !DILocalVariable(name: "2", scope: !6, file: !1, line: 3, type: !10)
!12 = !DILocation(line: 1, column: 1, scope: !6)
!13 = !DILocation(line: 2, column: 1, scope: !6)
!14 = !DILocation(line: 3, column: 1, scope: !6)
!15 = !DILocation(line: 4, column: 1, scope: !6)
!16 = !DILocation(line: 5, column: 1, scope: !6)
使用 debugify
¶
使用 debugify
的一個簡單方法如下
$ opt -debugify -pass-to-test -check-debugify sample.ll
這會將合成 DI 注入到 sample.ll
中,執行 pass-to-test
,然後檢查是否有遺失的 DI。-check-debugify
步驟當然可以省略,而改用更可自訂的 FileCheck 指令。
還有其他可用的執行 debugify 的方法
# Same as the above example.
$ opt -enable-debugify -pass-to-test sample.ll
# Suppresses verbose debugify output.
$ opt -enable-debugify -debugify-quiet -pass-to-test sample.ll
# Prepend -debugify before and append -check-debugify -strip after
# each pass on the pipeline (similar to -verify-each).
$ opt -debugify-each -O2 sample.ll
為了使 check-debugify
能夠運作,DI 必須來自 debugify
。因此,具有現有 DI 的模組將被跳過。
debugify
可用於測試後端,例如
$ opt -debugify < sample.ll | llc -o -
還有一個 MIR 層級的 debugify pass,可以在每個後端 pass 之前執行,請參閱:MIR 層級轉換的變異測試。
回歸測試中的 debugify
¶
debugify
pass 的輸出必須足夠穩定,才能在回歸測試中使用。不允許對此 pass 進行變更以破壞現有的測試。
注意
回歸測試必須穩健。避免在檢查行中硬編碼行號/變數號碼。在無法避免這種情況下(例如,如果測試不夠精確),最好將測試移至其自己的檔案。
測試最佳化中原始除錯資訊的保留¶
除了自動產生除錯資訊外,debugify
工具 pass 提供的檢查也可用於測試預先存在的除錯資訊 metadata 的保留。它可以按如下方式執行
# Run the pass by checking original Debug Info preservation.
$ opt -verify-debuginfo-preserve -pass-to-test sample.ll
# Check the preservation of original Debug Info after each pass.
$ opt -verify-each-debuginfo-preserve -O2 sample.ll
限制觀察到的函數數量以加速分析
# Test up to 100 functions (per compile unit) per pass.
$ opt -verify-each-debuginfo-preserve -O2 -debugify-func-limit=100 sample.ll
請注意,在大型專案上執行 -verify-each-debuginfo-preserve
可能非常耗時。因此,我們建議使用 -debugify-func-limit
和適當的限制數量,以防止極長的建置時間。
此外,有一種方法可以將已發現的問題匯出到 JSON 檔案中,如下所示
$ opt -verify-debuginfo-preserve -verify-di-preserve-export=sample.json -pass-to-test sample.ll
然後使用 llvm/utils/llvm-original-di-preservation.py
腳本產生一個 HTML 頁面,以更易於閱讀的形式報告問題,如下所示
$ llvm-original-di-preservation.py sample.json sample.html
可以從前端層級調用原始除錯資訊保留的測試,如下所示
# Test each pass.
$ clang -Xclang -fverify-debuginfo-preserve -g -O2 sample.c
# Test each pass and export the issues report into the JSON file.
$ clang -Xclang -fverify-debuginfo-preserve -Xclang -fverify-debuginfo-preserve-export=sample.json -g -O2 sample.c
請注意,對於原始碼位置和除錯記錄檢查,有一些已知的誤報,因此這將作為未來的工作來解決。
MIR 層級轉換的變異測試¶
在 IR 層級轉換的變異測試 中描述的 debugify
工具的變體也可用於 MIR 層級轉換:與 IR 層級 pass 非常相似,mir-debugify
將依序遞增的行位置插入到 Module
中的每個 MachineInstr
。MIR 層級的 mir-check-debugify
與 IR 層級的 check-debugify
pass 類似。
例如,這是之前的程式碼片段
name: test
body: |
bb.1 (%ir-block.0):
%0:_(s32) = IMPLICIT_DEF
%1:_(s32) = IMPLICIT_DEF
%2:_(s32) = G_CONSTANT i32 2
%3:_(s32) = G_ADD %0, %2
%4:_(s32) = G_SUB %3, %1
以及執行 llc -run-pass=mir-debugify
之後
name: test
body: |
bb.0 (%ir-block.0):
%0:_(s32) = IMPLICIT_DEF debug-location !12
DBG_VALUE %0(s32), $noreg, !9, !DIExpression(), debug-location !12
%1:_(s32) = IMPLICIT_DEF debug-location !13
DBG_VALUE %1(s32), $noreg, !11, !DIExpression(), debug-location !13
%2:_(s32) = G_CONSTANT i32 2, debug-location !14
DBG_VALUE %2(s32), $noreg, !9, !DIExpression(), debug-location !14
%3:_(s32) = G_ADD %0, %2, debug-location !DILocation(line: 4, column: 1, scope: !6)
DBG_VALUE %3(s32), $noreg, !9, !DIExpression(), debug-location !DILocation(line: 4, column: 1, scope: !6)
%4:_(s32) = G_SUB %3, %1, debug-location !DILocation(line: 5, column: 1, scope: !6)
DBG_VALUE %4(s32), $noreg, !9, !DIExpression(), debug-location !DILocation(line: 5, column: 1, scope: !6)
預設情況下,mir-debugify
會在所有合法的位置插入 DBG_VALUE
指令。特別是,每個(非 PHI)定義暫存器的機器指令都必須後跟對該定義的 DBG_VALUE
使用。如果指令未定義暫存器,但可以後跟除錯 inst,則 MIRDebugify 會插入引用常數的 DBG_VALUE
。DBG_VALUE
的插入可以透過設定 -debugify-level=locations
來停用。
若要執行 MIRDebugify 一次,只需將 mir-debugify
插入到您的 llc
調用中,例如
# Before some other pass.
$ llc -run-pass=mir-debugify,other-pass ...
# After some other pass.
$ llc -run-pass=other-pass,mir-debugify ...
若要在管道中的每個 pass 之前執行 MIRDebugify,請使用 -debugify-and-strip-all-safe
。這可以與 -start-before
和 -start-after
結合使用。例如
$ llc -debugify-and-strip-all-safe -run-pass=... <other llc args>
$ llc -debugify-and-strip-all-safe -O1 <other llc args>
如果您想在管道中的每個 pass 之後檢查它,請使用 -debugify-check-and-strip-all-safe
。這也可以與 -start-before
和 -start-after
結合使用。例如
$ llc -debugify-check-and-strip-all-safe -run-pass=... <other llc args>
$ llc -debugify-check-and-strip-all-safe -O1 <other llc args>
若要檢查測試中的所有除錯資訊,請使用 mir-check-debugify
,例如
$ llc -run-pass=mir-debugify,other-pass,mir-check-debugify
若要從測試中剝離所有除錯資訊,請使用 mir-strip-debug
,例如
$ llc -run-pass=mir-debugify,other-pass,mir-strip-debug
結合 mir-debugify
、mir-check-debugify
和/或 mir-strip-debug
可能有助於識別在存在除錯資訊的情況下會中斷的後端轉換。例如,若要執行 AArch64 後端測試,其中所有正常 pass 都「夾在」MIRDebugify 和 MIRStripDebugify 變異 pass 之間,請執行
$ llvm-lit test/CodeGen/AArch64 -Dllc="llc -debugify-and-strip-all-safe"
使用 LostDebugLocObserver¶
待辦事項