偵錯資訊遷移:從內建函式到記錄¶
我們正計劃從 LLVM 中移除偵錯資訊內建函式,因為它們速度慢、笨重,並且如果最佳化過程沒有預料到它們,就會造成混淆。過去指令序列看起來像這樣
%add = add i32 %foo, %bar
call void @llvm.dbg.value(metadata %add, ...
%sub = sub i32 %add, %tosub
call void @llvm.dbg.value(metadata %sub, ...
call void @a_normal_function()
其中 dbg.value
內建函式代表偵錯資訊記錄,現在改為列印成
%add = add i32 %foo, %bar
#dbg_value(%add, ...
%sub = sub i32 %add, %tosub
#dbg_value(%sub, ...
call void @a_normal_function()
偵錯記錄不是指令,不會出現在指令列表中,也不會出現在您的最佳化過程中,除非您刻意去挖掘它們。
太棒了,我需要做些什麼?¶
很少 - 我們已經對所有 LLVM 進行了檢測,以處理這些新記錄(”DbgRecords
”),並且其行為與過去的 LLVM 行為完全相同。這目前預設為開啟,因此 DbgRecords
將在記憶體、IR 和位元碼中預設使用。
API 變更¶
有兩個需要注意的重大變化。首先,我們要將一個與偵錯相關的資料位元新增到 BasicBlock::iterator
類別中(這樣我們就可以判斷範圍是否打算在區塊開頭包含偵錯資訊)。這意味著在編寫插入 LLVM IR 指令的過程時,您需要使用 BasicBlock::iterator
而不是僅僅使用裸露的 Instruction *
來識別位置。大多數情況下,這意味著在確定要插入內容的位置後,您還必須在指令位置上呼叫 getIterator
- 但是,在區塊開頭插入時,您 *必須* 使用 getFirstInsertionPt
、getFirstNonPHIIt
或 begin
並使用該迭代器進行插入,而不是僅僅取得第一個指令的指標。
第二個問題是,如果您手動將指令序列從一個地方傳輸到另一個地方,例如重複使用 moveBefore
而您可能使用了 splice
,那麼您應該改用 moveBeforePreserving
方法。moveBeforePreserving
將傳輸偵錯資訊記錄及其附加到的指令。這是今天自動發生的事情 - 如果您對指令序列中的每個元素都使用 moveBefore
,那麼偵錯內建函式將在您的程式碼正常執行過程中被移動,但是我們在非指令偵錯資訊中失去了這種行為。
如需有關如何更新現有程式碼以支援偵錯記錄的更深入說明,請參閱以下指南。
文字 IR 變更¶
隨著我們從使用偵錯內嵌函式轉向使用偵錯記錄,任何依賴解析 LLVM 生成的 IR 的工具都需要處理新的格式。在大多數情況下,列印形式的偵錯內嵌函式呼叫和偵錯記錄之間的差異很小
新增兩個額外的縮排空格。
文字
(tail|notail|musttail)? call void @llvm.dbg.<type>
會替換為#dbg_<type>
。會從內嵌函式的每個參數中移除前導
metadata
。DILocation 從格式為
!dbg !<Num>
的指令附件變更為作為最後一個參數傳遞給偵錯記錄的普通參數,即!<Num>
。
遵循這些規則,我們有這個偵錯內嵌函式和等效偵錯記錄的範例
; Debug Intrinsic:
call void @llvm.dbg.value(metadata i32 %add, metadata !10, metadata !DIExpression()), !dbg !20
; Debug Record:
#dbg_value(i32 %add, !10, !DIExpression(), !20)
測試更新¶
由於使用記錄的變更,主 LLVM 儲存庫下游測試 LLVM IR 輸出的任何測試都可能會中斷。考慮到上述更新規則,將個別測試更新為預期記錄而不是內嵌函式應該很簡單。然而,更新許多測試可能會很繁重;若要更新主儲存庫中的 lit 測試,請使用下列步驟
將失敗的 lit 測試清單收集到一個檔案
failing-tests.txt
中,以換行符號分隔(並以換行符號結尾)。使用以下行將失敗的測試分成使用 update_test_checks 的測試和不使用的測試
$ while IFS= read -r f; do grep -q "Assertions have been autogenerated by" "$f" && echo "$f" >> update-checks-tests.txt || echo "$f" >> manual-tests.txt; done < failing-tests.txt
對於使用 update_test_checks 的測試,請執行適當的 update_test_checks 腳本 - 對於主 LLVM 儲存庫,這是透過以下方式實現的
$ xargs ./llvm/utils/update_test_checks.py --opt-binary ./build/bin/opt < update-checks-tests.txt $ xargs ./llvm/utils/update_cc_test_checks.py --llvm-bin ./build/bin/ < update-checks-tests.txt
其餘的測試可以手動更新,但如果測試數量很多,則以下腳本可能會有所幫助;首先,用於從檔案中提取檢查行首碼的腳本
$ cat ./get-checks.sh #!/bin/bash # Always add CHECK, since it's more effort than it's worth to filter files where # every RUN line uses other check prefixes. # Then detect every instance of "check-prefix(es)=..." and add the # comma-separated arguments as extra checks. for filename in "$@" do echo "$filename,CHECK" allchecks=$(grep -Eo 'check-prefix(es)?[ =][A-Z0-9_,-]+' $filename | sed -E 's/.+[= ]([A-Z0-9_,-]+).*/\1/g; s/,/\n/g') for check in $allchecks; do echo "$filename,$check" done done
然後是第二個腳本,用於執行實際更新每個失敗測試中的檢查行的操作,並使用一系列簡單的替換模式
$ cat ./substitute-checks.sh #!/bin/bash file="$1" check="$2" # Any test that explicitly tests debug intrinsic output is not suitable to # update by this script. if grep -q "write-experimental-debuginfo=false" "$file"; then exit 0 fi sed -i -E -e " /(#|;|\/\/).*$check[A-Z0-9_\-]*:/!b /DIGlobalVariableExpression/b /!llvm.dbg./bpostcall s/((((((no|must)?tail )?call.*)?void )?@)?llvm.)?dbg\.([a-z]+)/#dbg_\7/ :postcall /declare #dbg_/d s/metadata //g s/metadata\{/{/g s/DIExpression\(([^)]*)\)\)(,( !dbg)?)?/DIExpression(\1),/ /#dbg_/!b s/((\))?(,) )?!dbg (![0-9]+)/\3\4\2/ s/((\))?(, ))?!dbg/\3/ " "$file"
這兩個腳本可以結合使用在
manual-tests.txt
中的清單上,如下所示$ cat manual-tests.txt | xargs ./get-checks.sh | sort | uniq | awk -F ',' '{ system("./substitute-checks.sh " $1 " " $2) }'
這些腳本成功處理了
clang/test
和llvm/test
中的絕大多數檢查。驗證產生的測試是否通過,並檢測任何失敗的測試
$ xargs ./build/bin/llvm-lit -q < failing-tests.txt ******************** Failed Tests (5): LLVM :: DebugInfo/Generic/dbg-value-lower-linenos.ll LLVM :: Transforms/HotColdSplit/transfer-debug-info.ll LLVM :: Transforms/ObjCARC/basic.ll LLVM :: Transforms/ObjCARC/ensure-that-exception-unwind-path-is-visited.ll LLVM :: Transforms/SafeStack/X86/debug-loc2.ll Total Discovered Tests: 295 Failed: 5 (1.69%)
某些測試可能已失敗 - 更新腳本過於簡化,並且不會跨行保留任何上下文,因此在某些情況下它們將無法處理;其餘情況必須手動更新(或由其他腳本處理)。
C-API 變更¶
新增的一些函式是暫時的,將來會被棄用。其目的是在過渡期間幫助下游項目適應。
Deleted functions
-----------------
LLVMDIBuilderInsertDeclareBefore # Insert a debug record (new debug info format) instead of a debug intrinsic (old debug info format).
LLVMDIBuilderInsertDeclareAtEnd # Same as above.
LLVMDIBuilderInsertDbgValueBefore # Same as above.
LLVMDIBuilderInsertDbgValueAtEnd # Same as above.
New functions (to be deprecated)
--------------------------------
LLVMIsNewDbgInfoFormat # Returns true if the module is in the new non-instruction mode.
LLVMSetIsNewDbgInfoFormat # Convert to the requested debug info format.
New functions (no plans to deprecate)
-------------------------------------
LLVMDIBuilderInsertDeclareRecordBefore # Insert a debug record (new debug info format).
LLVMDIBuilderInsertDeclareRecordAtEnd # Same as above. See info below.
LLVMDIBuilderInsertDbgValueRecordBefore # Same as above. See info below.
LLVMDIBuilderInsertDbgValueRecordAtEnd # Same as above. See info below.
LLVMPositionBuilderBeforeDbgRecords # See info below.
LLVMPositionBuilderBeforeInstrAndDbgRecords # See info below.
LLVMDIBuilderInsertDeclareRecordBefore
、LLVMDIBuilderInsertDeclareRecordAtEnd
、LLVMDIBuilderInsertDbgValueRecordBefore
和 LLVMDIBuilderInsertDbgValueRecordAtEnd
正在取代已刪除的 LLVMDIBuilderInsertDeclareBefore
類型函數。
除了插入位置設置在目標指令之前的偵錯記錄之前外,LLVMPositionBuilderBeforeDbgRecords
和 LLVMPositionBuilderBeforeInstrAndDbgRecords
的行為與 LLVMPositionBuilder
和 LLVMPositionBuilderBefore
相同。請注意,這並不意味著選定指令之前的偵錯內建函式會被跳過,只有偵錯記錄(與偵錯內建函式不同,它們本身不是指令)才會被跳過。
如果您不知道要呼叫哪個函式,請遵循以下規則:如果您嘗試在區塊的開頭插入,或者出於任何其他原因特意跳過偵錯內建函式來確定插入點,則呼叫新的函式。
LLVMPositionBuilder
和 LLVMPositionBuilderBefore
保持不變。它們插入在指示的指令之前,但在任何附加的偵錯記錄之後。
新的「偵錯記錄」模型¶
以下是取代偵錯內建函式的新的表示法的簡要概述;有關更新舊程式碼的說明性指南,請參閱這裡。
您究竟用什麼取代了偵錯內建函式?¶
我們正在使用一個專用的 C++ 類別,稱為 DbgRecord
來儲存偵錯資訊,在任何 LLVM IR 程式中,每個偵錯內建函式的實例與每個 DbgRecord
物件之間都存在一對一的關係;這些 DbgRecord
在 IR 中表示為非指令偵錯記錄,如 [原始碼級偵錯](project:SourceLevelDebugging.rst#Debug Records) 文件中所述。這個類別有一組子類別,它們儲存與偵錯內建函式中儲存的資訊完全相同的資訊。每個子類別也幾乎具有完全相同的方法集,它們的行為方式也相同
https://llvm.dev.org.tw/docs/doxygen/classllvm_1_1DbgRecord.html https://llvm.dev.org.tw/docs/doxygen/classllvm_1_1DbgVariableRecord.html https://llvm.dev.org.tw/docs/doxygen/classllvm_1_1DbgLabelRecord.html
這允許您將 DbgVariableRecord
視為 dbg.value
/dbg.declare
/dbg.assign
內建函式,例如在泛型(自動參數)lambda 中,以及 DbgLabelRecord
和 dbg.label
也是如此。
這些 DbgRecords
如何融入指令流?¶
像這樣
+---------------+ +---------------+
---------------->| Instruction +--------->| Instruction |
+-------+-------+ +---------------+
|
|
|
|
v
+-------------+
<-------+ DbgMarker |<-------
/ +-------------+ \
/ \
/ \
v ^
+-------------+ +-------------+ +-------------+
| DbgRecord +--->| DbgRecord +-->| DbgRecord |
+-------------+ +-------------+ +-------------+
每個指令都有一個指向 DbgMarker
的指標(這將變成可選的),其中包含一個 DbgRecord
物件列表。指令列表中根本沒有出現任何偵錯記錄。DbgRecord
有一個指向其所屬 DbgMarker
的父指標,而每個 DbgMarker
都有一個指回其所屬指令的指標。
圖中沒有顯示的是從 DbgRecord 到 Value
/Metadata
階層其他部分的連結:DbgRecord
子類別具有指向它們使用的 DIMetadata 的追蹤指標,而 DbgVariableRecord
具有對儲存在 DebugValueUser
基類中的 Value
的參考。這是指透過 TrackingMetadata
機制,參考 Value
的 ValueAsMetadata
物件。
各種偵錯內建函式(值、宣告、賦值、標籤)都儲存在 DbgRecord
子類別中,其中「RecordKind」欄位用於區分 DbgLabelRecord
和 DbgVariableRecord
,而 DbgVariableRecord
類別中的 LocationType
欄位進一步區分它可以表示的各種偵錯變數內建函式。
如何更新現有程式碼¶
任何以某種方式與偵錯內建函式互動的現有程式碼都需要更新,以便以相同的方式與偵錯記錄互動。更新程式碼時,請牢記以下幾條快速規則
迭代指令時不會看到偵錯記錄;若要尋找緊接在指令之前的偵錯記錄,您需要迭代
Instruction::getDbgRecordRange()
。偵錯記錄具有與偵錯內建函式相同的介面,這表示任何對偵錯內建函式進行操作的程式碼都可以輕鬆地應用於偵錯記錄。例外情況是不適用於偵錯記錄的
Instruction
或CallInst
方法,以及isa
/cast
/dyn_cast
方法,它們由DbgRecord
類別本身的方法取代。偵錯記錄不能出現在也包含偵錯內建函式的模組中;這兩者互斥。由於偵錯記錄是未來的格式,因此在新程式碼中應優先考慮正確處理記錄。
在不再支援內建函式之前,對於僅處理偵錯內建函式且更新起來不輕鬆的程式碼,一個有效的熱修補程式是使用
Module::setIsNewDbgInfoFormat
將模組轉換為內建函式格式,然後再轉換回來。這也可以使用
ScopedDbgInfoFormatSetter
類別在模組或個別函式的詞彙範圍內執行
void handleModule(Module &M) { { ScopedDbgInfoFormatSetter FormatSetter(M, false); handleModuleWithDebugIntrinsics(M); } // Module returns to previous debug info format after exiting the above block. }
以下是如何將目前支援偵錯內建函式的現有程式碼更新為支援偵錯記錄的粗略指南。
建立偵錯記錄¶
啟用新格式後,DIBuilder
類別將自動建立偵錯記錄。與指令一樣,也可以呼叫 DbgRecord::clone
來建立現有記錄的未附加副本。
跳過偵錯記錄,忽略 Values
的偵錯使用,穩定地計算指令等等。¶
這一切都會透明地發生,不需要您費心!
for (Instruction &I : BB) {
// Old: Skips debug intrinsics
if (isa<DbgInfoIntrinsic>(&I))
continue;
// New: No extra code needed, debug records are skipped by default.
...
}
尋找偵錯記錄¶
像 findDbgUsers
等工具現在都有一個可選的參數,它將返回引用 Value
的 DbgVariableRecord
記錄集。您應該能夠將它們視為內建函數一樣處理。
// Old:
SmallVector<DbgVariableIntrinsic *> DbgUsers;
findDbgUsers(DbgUsers, V);
for (auto *DVI : DbgUsers) {
if (DVI->getParent() != BB)
DVI->replaceVariableLocationOp(V, New);
}
// New:
SmallVector<DbgVariableIntrinsic *> DbgUsers;
SmallVector<DbgVariableRecord *> DVRUsers;
findDbgUsers(DbgUsers, V, &DVRUsers);
for (auto *DVI : DbgUsers)
if (DVI->getParent() != BB)
DVI->replaceVariableLocationOp(V, New);
for (auto *DVR : DVRUsers)
if (DVR->getParent() != BB)
DVR->replaceVariableLocationOp(V, New);
檢查特定位置的偵錯記錄¶
呼叫 Instruction::getDbgRecordRange()
以取得附加到指令的 DbgRecord
物件的範圍。
for (Instruction &I : BB) {
// Old: Uses a data member of a debug intrinsic, and then skips to the next
// instruction.
if (DbgInfoIntrinsic *DII = dyn_cast<DbgInfoIntrinsic>(&I)) {
recordDebugLocation(DII->getDebugLoc());
continue;
}
// New: Iterates over the debug records that appear before `I`, and treats
// them identically to the intrinsic block above.
// NB: This should always appear at the top of the for-loop, so that we
// process the debug records preceding `I` before `I` itself.
for (DbgRecord &DR = I.getDbgRecordRange()) {
recordDebugLocation(DR.getDebugLoc());
}
processInstruction(I);
}
這也可以通過 filterDbgVars
函數傳遞,以專門迭代更常用的 DbgVariableRecords。
for (Instruction &I : BB) {
// Old: If `I` is a DbgVariableIntrinsic we record the variable, and apply
// extra logic if it is an `llvm.dbg.declare`.
if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
recordVariable(DVI->getVariable());
if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
recordDeclareAddress(DDI->getAddress());
continue;
}
// New: `filterDbgVars` is used to iterate over only DbgVariableRecords.
for (DbgVariableRecord &DVR = filterDbgVars(I.getDbgRecordRange())) {
recordVariable(DVR.getVariable());
// Debug variable records are not cast to subclasses; simply call the
// appropriate `isDbgX()` check, and use the methods as normal.
if (DVR.isDbgDeclare())
recordDeclareAddress(DVR.getAddress());
}
// ...
}
處理個別的偵錯記錄¶
在大多數情況下,任何對偵錯內建函數進行操作的程式碼都可以提取到一個模板函數或自動 lambda(如果它還不在其中)中,該函數可以應用於偵錯內建函數和偵錯記錄 - 但請記住主要例外是 isa
/cast
/dyn_cast
不適用於 DbgVariableRecord
類型。
// Old: Function that operates on debug variable intrinsics in a BasicBlock, and
// collects llvm.dbg.declares.
void processDbgInfoInBlock(BasicBlock &BB,
SmallVectorImpl<DbgDeclareInst*> &DeclareIntrinsics) {
for (Instruction &I : BB) {
if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
processVariableValue(DebugVariable(DVI), DVI->getValue());
if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
Declares.push_back(DDI);
else if (!isa<Constant>(DVI->getValue()))
DVI->setKillLocation();
}
}
}
// New: Template function is used to deduplicate handling of intrinsics and
// records.
// An overloaded function is also used to handle isa/cast/dyn_cast operations
// for intrinsics and records, since those functions cannot be directly applied
// to DbgRecords.
DbgDeclareInst *DynCastToDeclare(DbgVariableIntrinsic *DVI) {
return dyn_cast<DbgDeclareInst>(DVI);
}
DbgVariableRecord *DynCastToDeclare(DbgVariableRecord *DVR) {
return DVR->isDbgDeclare() ? DVR : nullptr;
}
template<typename DbgVarTy, DbgDeclTy>
void processDbgVariable(DbgVarTy *DbgVar,
SmallVectorImpl<DbgDeclTy*> &Declares) {
processVariableValue(DebugVariable(DbgVar), DbgVar->getValue());
if (DbgDeclTy *DbgDeclare = DynCastToDeclare(DbgVar))
Declares.push_back(DbgDeclare);
else if (!isa<Constant>(DbgVar->getValue()))
DbgVar->setKillLocation();
};
void processDbgInfoInBlock(BasicBlock &BB,
SmallVectorImpl<DbgDeclareInst*> &DeclareIntrinsics,
SmallVectorImpl<DbgVariableRecord*> &DeclareRecords) {
for (Instruction &I : BB) {
if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I))
processDbgVariable(DVI, DeclareIntrinsics);
for (DbgVariableRecord *DVR : filterDbgVars(I.getDbgRecordRange()))
processDbgVariable(DVR, DeclareRecords);
}
}
移動和刪除偵錯記錄¶
您可以使用 DbgRecord::removeFromParent
從其標記中取消連結 DbgRecord
,然後使用 BasicBlock::insertDbgRecordBefore
或 BasicBlock::insertDbgRecordAfter
將 DbgRecord
重新插入到其他位置。您不能在 DbgRecord
列表中的任意位置插入 DbgRecord
(如果您使用 llvm.dbg.value
執行此操作,則不太可能是正確的)。
通過呼叫 eraseFromParent
刪除 DbgRecord
。
// Old: Move a debug intrinsic to the start of the block, and delete all other intrinsics for the same variable in the block.
void moveDbgIntrinsicToStart(DbgVariableIntrinsic *DVI) {
BasicBlock *ParentBB = DVI->getParent();
DVI->removeFromParent();
for (Instruction &I : ParentBB) {
if (auto *BlockDVI = dyn_cast<DbgVariableIntrinsic>(&I))
if (BlockDVI->getVariable() == DVI->getVariable())
BlockDVI->eraseFromParent();
}
DVI->insertBefore(ParentBB->getFirstInsertionPt());
}
// New: Perform the same operation, but for a debug record.
void moveDbgRecordToStart(DbgVariableRecord *DVR) {
BasicBlock *ParentBB = DVR->getParent();
DVR->removeFromParent();
for (Instruction &I : ParentBB) {
for (auto &BlockDVR : filterDbgVars(I.getDbgRecordRange()))
if (BlockDVR->getVariable() == DVR->getVariable())
BlockDVR->eraseFromParent();
}
DVR->insertBefore(ParentBB->getFirstInsertionPt());
}
懸空的偵錯記錄怎麼辦?¶
如果您有一個像這樣的區塊
foo:
%bar = add i32 %baz...
dbg.value(metadata i32 %bar,...
br label %xyzzy
您的優化過程可能希望刪除終止符,然後對區塊執行某些操作。當偵錯信息保存在指令中時,這很容易做到,但是使用 DbgRecord
時,一旦終止符被刪除,在上面的區塊中就沒有尾隨指令可以附加變量信息。對於這種退化的區塊,DbgRecord
會臨時存儲在 LLVMContext
的映射中,並在終止符重新插入區塊或在 end()
處插入其他指令時重新插入。
在極少數情況下,當優化過程刪除終止符然後決定刪除整個區塊時,這在技術上可能會導致問題。(我們建議不要這樣做)。
還有其他嗎?¶
上述指南並未涵蓋所有可能適用於除錯內建函式的模式;如同在指南開頭所述,您可以暫時將目標模組從除錯記錄轉換為內建函式作為權宜之計。大多數可在除錯內建函式上執行的操作,對於除錯記錄都有完全相同的對應操作,但如果您遇到任何例外情況,請閱讀類別文件(連結在此)可能會有所幫助,現有程式碼庫中可能會有範例,您也可以隨時在論壇上尋求幫助。