调试信息迁移:从内联函数到记录¶
我们计划从 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 的工具都需要处理新的格式。在大多数情况下,调试内联函数调用的打印形式与调试记录之间的区别微不足道
添加了额外的 2 个空格缩进。
文本
(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-style
函数。
LLVMPositionBuilderBeforeDbgRecords
和 LLVMPositionBuilderBeforeInstrAndDbgRecords
的行为与 LLVMPositionBuilder
和 LLVMPositionBuilderBefore
相同,只是插入位置设置在目标指令之前的调试记录之前。请注意,这并不意味着跳过选定指令之前的调试内联函数,而只是调试记录(与调试记录不同,调试记录本身不是指令)。
如果您不知道要调用哪个函数,请遵循以下规则:如果您尝试在块的开头插入,或出于任何其他原因故意跳过调试内联函数以确定插入点,则调用新函数。
LLVMPositionBuilder
和 LLVMPositionBuilderBefore
未更改。它们在指示的指令之前但任何附加的调试记录之后插入。
新的“调试记录”模型¶
以下是替换调试内联函数的新表示形式的简要概述;有关更新旧代码的说明性指南,请参阅此处。
您到底用什么替换了调试内联函数?¶
我们使用一个名为 DbgRecord
的专用 C++ 类来存储调试信息,在每个调试内联函数实例和任何 LLVM IR 程序中的每个 DbgRecord
对象之间存在一对一的关系;这些 DbgRecord
在 IR 中表示为非指令调试记录,如[源级调试](project:SourceLevelDebugging.rst#Debug Records) 文档中所述。此类有一组子类,用于存储与调试内联函数中存储的信息完全相同的信息。每个子类还几乎具有完全相同的函数集,其行为方式相同
https://llvm.net.cn/docs/doxygen/classllvm_1_1DbgRecord.html https://llvm.net.cn/docs/doxygen/classllvm_1_1DbgVariableRecord.html https://llvm.net.cn/docs/doxygen/classllvm_1_1DbgLabelRecord.html
例如,在泛型(自动参数)lambda 中,您可以将 DbgVariableRecord
视为 dbg.value
/dbg.declare
/dbg.assign
内联函数,对于 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()
处插入其他指令时重新插入。
从技术上讲,这可能导致在优化传递删除终止符然后决定删除整个块的极少情况下出现问题。(我们建议不要这样做)。
其他任何事?¶
以上指南并未全面涵盖可能应用于调试内联函数的每种模式;如指南开头所述,您可以暂时将目标模块从调试记录转换为内联函数作为权宜之计。可以在调试内联函数上执行的大多数操作都有调试记录的精确等价物,但如果您遇到任何异常,阅读类文档(此处链接)可能会提供一些见解,现有代码库中可能有一些示例,您也可以随时在论坛上寻求帮助。