偵錯資訊指派追蹤¶
指派追蹤是一種在 LLVM 中透過最佳化來追蹤變數位置偵錯資訊的替代技術。它為區域變數(或其欄位)作為左側的指派提供準確的變數位置。在極少數且複雜的情況下,間接指派可能會在未被追蹤的情況下被最佳化掉,但除此之外,我們會盡力追蹤所有變數位置。
核心概念是依序追蹤更多關於原始碼指派的資訊,並保留足夠的資訊,以便能夠延遲決定是使用非記憶體位置(暫存器、常數)還是記憶體位置,直到中端最佳化執行完畢。這與使用 #dbg_declare
和 #dbg_value
相反,後者是在早期為大多數變數做出決定,這可能導致次佳的變數位置,這些位置可能不正確或不完整。
指派追蹤的次要目標是盡量減少 LLVM pass 作者的額外工作量,並盡量減少對 LLVM 的整體干擾。
狀態和用法¶
**狀態**:實驗性工作正在進行中。強烈建議不要啟用,除非用於開發和測試。
**在 Clang 中啟用**: -Xclang -fexperimental-assignment-tracking
這會導致 Clang 讓 LLVM 執行 pass declare-to-assign
。該 pass 會將傳統的偵錯記錄轉換為指派追蹤中繼資料,並將模組旗標 debug-info-assignment-tracking
設定為值 i1 true
。若要檢查是否為模組啟用了指派追蹤,請呼叫 isAssignmentTrackingEnabled(const Module &M)
(來自 llvm/IR/DebugInfo.h
)。
設計和實作¶
指派標記: #dbg_assign
¶
#dbg_value
是一種傳統的偵錯記錄,它標記出 IR 中變數採用特定值的位址。同樣地,指派追蹤會使用稱為 #dbg_assign
的記錄來標記出指派的位址。
為了知道在 IR 中的哪個位置適合使用變數的記憶體位置,每個指派標記都必須以某種方式參考執行指派的 store(如果有的話,或多個!)。這樣一來,在做出選擇時,就可以同時考慮 store 和標記的位址。參考 store 的另一個重要好處是,我們可以建立 store<->標記的雙向映射,用於在修改 store 時找到需要更新的標記。
未連結到任何指令的 #dbg_assign
標記表示執行賦值的存放指令已被最佳化移除,因此記憶體位置至少在程式的一部分中將無效。
以下是 #dbg_assign
簽章。 Value *
類型參數會先包裝在 ValueAsMetadata
中
#dbg_assign(Value *Value,
DIExpression *ValueExpression,
DILocalVariable *Variable,
DIAssignID *ID,
Value *Address,
DIExpression *AddressExpression)
前三個參數的外觀和行為類似於 #dbg_value
。 ID
是對存放指令的參考(請參閱下一節)。 Address
是存放指令的目的地位址,並由 AddressExpression
修改。 空白/未定義/毒物位址表示位址組件已被刪除(記憶體位址不再是有效位置)。 LLVM 目前在 DIExpression
中編碼變數片段資訊,因此作為實作怪癖,Variable
的 FragmentInfo
僅包含在 ValueExpression
中。
指令連結: DIAssignID
¶
DIAssignID
中繼資料是目前用於編碼存放指令與標記連結的機制。 中繼資料節點沒有運算元,並且所有實例都是 distinct
; 相等性檢查是透過比較位址來執行的。
#dbg_assign
記錄使用 DIAssignID
中繼資料節點實例作為運算元。 這樣它就可以參考任何具有相同 DIAssignID
附加元件的類存放指令。 例如,對於此 test.cpp,
int fun(int a) {
return a;
}
在沒有最佳化的情況下編譯
$ clang++ test.cpp -o test.ll -emit-llvm -S -g -O0 -Xclang -fexperimental-assignment-tracking
我們得到
define dso_local noundef i32 @_Z3funi(i32 noundef %a) #0 !dbg !8 {
entry:
%a.addr = alloca i32, align 4, !DIAssignID !13
#dbg_assign(i1 undef, !14, !DIExpression(), !13, i32* %a.addr, !DIExpression(), !15)
store i32 %a, i32* %a.addr, align 4, !DIAssignID !16
#dbg_assign(i32 %a, !14, !DIExpression(), !16, i32* %a.addr, !DIExpression(), !15)
%0 = load i32, i32* %a.addr, align 4, !dbg !17
ret i32 %0, !dbg !18
}
...
!13 = distinct !DIAssignID()
!14 = !DILocalVariable(name: "a", ...)
...
!16 = distinct !DIAssignID()
第一個 #dbg_assign
透過 !DIAssignID !13
參考 alloca
,第二個透過 !DIAssignID !16
參考 store
。
類存放指令¶
在沒有連結的 #dbg_assign
的情況下,對已知是變數後備儲存空間的位址進行存放被視為表示對該變數的賦值。
這為我們提供了一種安全的後備機制,以防 #dbg_assign
記錄已被刪除、存放指令上的 DIAssignID
附加元件已被刪除,或者最佳化器已將曾經間接的存放指令(未透過賦值追蹤追蹤)變為直接存放指令。
中介端:Pass 編寫器的注意事項¶
非偵錯指令更新¶
複製 指令:無需執行任何新操作。 複製會自動複製 DIAssignID
附加元件。 多個指令可能具有相同的 DIAssignID
指令。 在這種情況下,賦值被視為發生在程式中的多個位置。
移動 非偵錯指令:無需執行任何新操作。 連結到 #dbg_assign
的指令的初始 IR 位置由 #dbg_assign
的位置標記。
刪除非除錯指令:無需進行任何新操作。簡單的 DSE 不需要任何更改;刪除帶有 DIAssignID
附加資料的指令是安全的。使用未附加到任何指令的 DIAssignID
的 #dbg_assign
表示記憶體位置無效。
合併存放區:在許多情況下,不需要進行任何更改,因為如果呼叫 combineMetadata
,DIAssignID
附加資料會自動合併。無論如何,DIAssignID
附加資料都必須合併,以便新的存放區連結到合併存放區所連結的所有 #dbg_assign
記錄。這可以透過呼叫輔助函數 Instruction::mergeDIAssignID
來輕鬆實現。
內嵌存放區:在內嵌存放區時,我們會產生 #dbg_assign
記錄和 DIAssignID
附加資料,就好像存放區代表原始碼指派一樣,就像在前端一樣。這並不完美,因為存放區可能在內嵌之前被移動、修改或刪除,但它至少在非內嵌範圍內保持了有關變數的正確資訊。
分割存放區:SROA 和分割存放區的 pass 會將 #dbg_assign
記錄視為與 #dbg_declare
記錄類似。複製連結到存放區的 #dbg_assign
記錄,更新 ValueExpression
中的 FragmentInfo,並為分割的存放區(和複製的記錄)分別提供新的 DIAssignID
附加資料。換句話說,將分割的存放區視為單獨的指派。對於部分 DSE(例如縮短 memset),我們會執行相同的操作,但失效片段的 #dbg_assign
會獲得 Undef
Address
。
提升 allocas 和存放區/載入:#dbg_assign
記錄隱式描述了 CFG 聯集處記憶體位置的聯集值,但在提升(或部分提升)變數後,情況不一定如此。提升變數的 pass 負責在提升期間產生的 PHI 指令之後插入 #dbg_assign
記錄。mem2reg
已經必須為 #dbg_declare
執行此操作(使用 #dbg_value
)。如果存放區沒有連結的記錄,則假設該存放區代表對儲存在目標地址的變數的指派。
除錯記錄更新¶
移動除錯記錄:盡可能避免移動 #dbg_assign
記錄,因為它們代表原始碼層級的指派,其在程式中的位置不應受到最佳化 pass 的影響。
刪除除錯記錄:無需進行任何新操作。就像傳統的除錯記錄一樣,除非無法到達,否則刪除 #dbg_assign
記錄幾乎總是不正確的。
將 #dbg_assign
降級為 MIR¶
首先,僅支援 SelectionDAG ISel。#dbg_assign
記錄會被降級為 MIR DBG_INSTR_REF
指令。在此之前,我們需要決定對於每個變數,在哪些情況下適合使用記憶體位置,以及在哪些情況下必須使用非記憶體位置(或不使用位置)。為了做出這些決定,我們會執行標準的定點資料流程分析,在每個指令處做出選擇,並迭代地合併每個區塊的結果。
待辦事項清單¶
由於這是一項實驗性的進行中工作,因此我們還需要處理一些項目。
如測試 llvm/test/DebugInfo/assignment-tracking/X86/diamond-3.ll 中所述,分析應該將跳脫呼叫視為未標記的存放操作。
系統預期區域變數由區域 alloca 支援。但情況並非總是如此 - 有時會將指向儲存空間的指標傳遞給函式(例如 sret、byval)。我們需要能夠處理這些情況。範例請參閱 llvm/test/DebugInfo/Generic/assignment-tracking/track-assignments.ll 和 clang/test/CodeGen/assignment-tracking/assignment-tracking.cpp。
trackAssignments
尚未適用於其#dbg_declare
位置被DIExpression
修改的變數,例如當變數的位址本身儲存在alloca
中,且#dbg_declare
使用DIExpression(DW_OP_deref)
時。範例請參閱 llvm/test/DebugInfo/Generic/assignment-tracking/track-assignments.ll 和 clang/test/CodeGen/assignment-tracking/assignment-tracking.cpp 中的indirectReturn
。為了要解決第一個項目,我們需要能夠在不使用
DIAssignID
的情況下指定記憶體位置是否可用。這是因為儲存空間位址不是由指令計算得出(它是一個引數值),因此我們沒有地方可以放置中繼資料附件。為了要解決這個問題,我們可能需要另一個標記記錄來表示「變數的堆疊起始位址為 X 位址」 - 類似於#dbg_declare
,但它需要與#dbg_assign
記錄組合,以便僅在#dbg_assign
記錄同意的情況下,才會選擇堆疊起始位址作為變數的位置。考慮到上述情況(一個特殊的「堆疊起始位址為 X」記錄),以及我們只能追蹤具有固定偏移量和大小的賦值這一事實,我認為我們可能可以擺脫位址和位址運算式部分,因為它始終可以使用我們擁有的資訊來計算。