调试信息迁移:从指令内在(intrinsics)到记录(records)
我们计划从LLVM中移除调试信息指令内在(debug info intrinsics),因为它们执行缓慢、难以维护,并且在优化pass没有预料到时可能引起混淆。现有的代码序列如下:
%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 指令内在用于表示调试信息。未来,新的IR打印格式如下:
%add = add i32 %foo, %bar
#dbg_value(%add, ...)
%sub = sub i32 %add, %tosub
#dbg_value(%sub, ...)
call void @a_normal_function()
新的调试记录(record)不是指令,不会出现在指令列表中,优化pass中默认也不会出现,除非你刻意去查找它们。
太棒了,我需要做什么?
我们已经基本完成了这项迁移。剩下的注意点是:今后,插入指令到基本块时需要使用迭代器(iterator)而不是指令指针。大多数场景下,你只需在指令指针上调用 getIterator。但是,如果你调用返回基本块开始位置的函数(比如):
- BasicBlock::begin
- BasicBlock::getFirstNonPHIIt
- BasicBlock::getFirstInsertionPt
那么请直接把这个迭代器传递给插入函数(iterator对象带有调试信息相关的比特位)。就这么多!下面有更详细的解释。
API变更
需要注意两个重大变化。首先,BasicBlock::iterator 类新增了一个调试相关的比特位,这样我们能区分某个区间是否包含了基本块起始的调试信息。因此,在插入LLVM IR指令时,应使用 BasicBlock::iterator来标识位置,而不是指令指针(Instruction *)。通常,你定位插入位置后,需在指令对象上调用getIterator;但如果需要插入到基本块开始,必须通过getFirstInsertionPt、getFirstNonPHIIt或begin获取迭代器并传递。
第二点,当你需要手动把成组指令搬移到新位置(例如你依次用moveBefore而不是splice),应使用moveBeforePreserving。它会把绑定在指令上的调试记录一起移动。之前用moveBefore会自动带过调试信息内在,但用新记录格式则不会,所以推荐用moveBeforePreserving。
更多更新已有代码以支持调试记录的指南详见下方文档。
文本IR的变化
IR中的调试信息由内在变为记录后,所有依赖LLVM产出的IR文本的工具都要适配新格式。总体区别主要包括:
- 调试记录行相比之前多了2个空格缩进;
(tail|notail|musttail)? call void @llvm.dbg.<type>被替换为#dbg_<type>;- 遗弃原先参数中的
metadata关键字; DILocation从原本作为!dbg !<Num>指令附加属性,变成作为记录的最后一个参数直接传递,如!<Num>。
例子对比如下:
旧调试内在:
call void @llvm.dbg.value(metadata i32 %add, metadata !10, metadata !DIExpression()), !dbg !20
新调试记录:
#dbg_value(i32 %add, !10, !DIExpression(), !20)
测试更新
任何测试下游仓库若断言了LLVM生成的IR输出,都可能因为调试记录替换而报错。根据上述格式变化,单个测试的修正非常直观。但是涉及大量测试时,更新操作会很繁琐。官方仓库的主要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
- 对于自动断言测试,分别运行:
$ 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
- 剩余测试需手动修正,对大量测试可用以下脚本辅助:
get-checks.sh 用于提取check-line前缀:
#!/bin/bash
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
substitute-checks.sh 用于执行批量替换:
#!/bin/bash
file="$1"
check="$2"
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_\4/
/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"
结合使用上述脚本:
$ 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 变更
有些API函数已新增但属于过渡期临时接口,将来会废弃。它们主要帮助下游项目完成迁移。
已删除的函数
- LLVMDIBuilderInsertDeclareBefore # 旧接口,已删除
- LLVMDIBuilderInsertDeclareAtEnd # 同上
- LLVMDIBuilderInsertDbgValueBefore # 同上
- LLVMDIBuilderInsertDbgValueAtEnd # 同上
新增(未来会废弃)
- LLVMIsNewDbgInfoFormat # 判断模块是否采用新调试信息格式
- LLVMSetIsNewDbgInfoFormat # 切换调试信息格式
新增(不准备废弃)
- LLVMGetFirstDbgRecord
- LLVMGetLastDbgRecord
- LLVMGetNextDbgRecord
- LLVMGetPreviousDbgRecord
- LLVMDIBuilderInsertDeclareRecordBefore
- LLVMDIBuilderInsertDeclareRecordAtEnd
- LLVMDIBuilderInsertDbgValueRecordBefore
- LLVMDIBuilderInsertDbgValueRecordAtEnd
- LLVMPositionBuilderBeforeDbgRecords
- LLVMPositionBuilderBeforeInstrAndDbgRecords
LLVMDIBuilderInsertDeclareRecordBefore、LLVMDIBuilderInsertDeclareRecordAtEnd、LLVMDIBuilderInsertDbgValueRecordBefore、LLVMDIBuilderInsertDbgValueRecordAtEnd 用于替代被删除的插入调试信息相关接口。
LLVMPositionBuilderBeforeDbgRecords 和 LLVMPositionBuilderBeforeInstrAndDbgRecords 类似于老的LLVMPositionBuilder/LLVMPositionBuilderBefore,但能够在目标指令之前的调试记录前定位插入位置。注意,这些函数只针对调试记录(非指令的),调试内在(intrinsics)不会被跳过。
如果你不确定该用哪个函数,有个简单原则:如果要插入到块头,或者明确要跳过调试内在,再用新接口。
LLVMGetFirstDbgRecord、LLVMGetLastDbgRecord、LLVMGetNextDbgRecord 和 LLVMGetPreviousDbgRecord 可用于遍历绑定在某条指令(LLVMValueRef)上的调试记录:
C代码示意:
LLVMDbgRecordRef DbgRec;
for (DbgRec = LLVMGetFirstDbgRecord(Inst); DbgRec;
DbgRec = LLVMGetNextDbgRecord(DbgRec)) {
// do something with DbgRec
}
LLVMDbgRecordRef DbgRec;
for (DbgRec = LLVMGetLastDbgRecord(Inst); DbgRec;
DbgRec = LLVMGetPreviousDbgRecord(DbgRec)) {
// do something with DbgRec
}
新“调试记录”模型
下述为新调试记录取代调试内在的简要说明,详细的更新指导见这里。
究竟用什么替换了调试内在?
我们引入了专用的C++类 DbgRecord 存储调试信息,每个旧有调试内在都严格一一对应一个 DbgRecord 实例,IR里表现为非指令类型的“调试记录”。该类有多个子类,存储与旧有调试内在完全相同的信息,方法集合几乎一样:
通常你可以使用 DbgVariableRecord 就像处理 dbg.value/dbg.declare/dbg.assign 形式的内在一样,对调试标签也同理。
DbgRecord 如何与指令流结合?
示意图如下:
+---------------+ +---------------+
---------------->| Instruction ±-------->| Instruction |
±------±------+ ±--------------+
|
|
|
|
v
±------------+
<-------+ DbgMarker |<-------
/ ±------------+
/
/
v ^
±------------+ ±------------+ ±------------+
| DbgRecord ±–>| DbgRecord ±->| DbgRecord |
±------------+ ±------------+ ±------------+
每一条指令拥有一个指向 DbgMarker 的指针(未来可选),后者保存了该指令的调试记录列表。调试记录不再出现在指令表中。DbgRecord 拥有指向所属 DbgMarker 的parent指针,DbgMarker 指向所属指令。
未绘出的还有 DbgRecord 到更广泛Value/Metadata的链接:DbgRecord 子类通过 TrackingMetadata 等机制追踪DIMetadata,DbgVariableRecord通过DebugValueUser基类指向相关Value。各种调试内在现在归为一组 DbgRecord 子类,用“RecordKind”区分变量、标签等;DbgVariableRecord 的 “LocationType” 字段则能进一步区分不同的变量内在。
如何更新已有代码
任何处理调试内在的代码都需要升级以支持调试记录。几个关键规则:
- 正常遍历基本块指令将自动略过调试记录。要访问出现在指令之前的调试记录,请用
Instruction::getDbgRecordRange()。 - 调试记录的接口与调试内在完全一致,只有
Instruction/CallInst特有方法,以及isa/cast/dyn_cast需由DbgRecord自己提供。 - 同一个模块不能同时存在调试内在和调试记录。调试记录是未来方向,推荐优先支持。
- 在尚未彻底弃用调试内在之前,如果无法立刻改造某代码,也可以调用
Module::setIsNewDbgInfoFormat转换格式后再处理。而这种切换在函数或模块级可以用ScopedDbgInfoFormatSetter完成:
void handleModule(Module &M) {
{
ScopedDbgInfoFormatSetter FormatSetter(M, false);
handleModuleWithDebugIntrinsics(M);
}
// 离开作用域后,模块恢复之前的调试格式
}
下面是典型模式的代码更新对照。
创建调试记录
新格式下,DIBuilder 会自动创建调试记录,也可以用 DbgRecord::clone 复制已有记录并未绑定。
跳过调试记录、忽略调试use、稳定计数指令等
一切都无需特殊处理,实现见下:
for (Instruction &I : BB) {
// 旧写法:跳过调试内在
if (isa<DbgInfoIntrinsic>(&I))
continue;
// 新写法:遍历自然就跳过调试记录
...
}
查找调试记录
如findDbgUsers等工具现在增加了可选参数用于返回指向DbgVariableRecord的集合。依然可以像处理内在那样处理对应的记录。
// 旧写法:
SmallVector<DbgVariableIntrinsic *> DbgUsers;
findDbgUsers(DbgUsers, V);
for (auto *DVI : DbgUsers) {
if (DVI->getParent() != BB)
DVI->replaceVariableLocationOp(V, 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() 获得附在指令前的调试记录range。示例如下:
for (Instruction &I : BB) {
// 旧写法
if (DbgInfoIntrinsic *DII = dyn_cast<DbgInfoIntrinsic>(&I)) {
recordDebugLocation(DII->getDebugLoc());
continue;
}
// 新写法
for (DbgRecord &DR : I.getDbgRecordRange()) {
recordDebugLocation(DR.getDebugLoc());
}
processInstruction(I);
}
也可以用 filterDbgVars 仅遍历变量记录:
for (Instruction &I : BB) {
// 旧写法
if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
recordVariable(DVI->getVariable());
if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
recordDeclareAddress(DDI->getAddress());
continue;
}
// 新写法
for (DbgVariableRecord &DVR : filterDbgVars(I.getDbgRecordRange())) {
recordVariable(DVR.getVariable());
if (DVR.isDbgDeclare())
recordDeclareAddress(DVR.getAddress());
}
}
单独处理调试记录
建议用c++模板或自动lambda统一处理指令和调试记录,但注意isa/cast/dyn_cast不可直接用于DbgRecord,需要新实现:
DbgDeclareInst *DynCastToDeclare(DbgVariableIntrinsic *DVI) {
return dyn_cast<DbgDeclareInst>(DVI);
}
DbgVariableRecord *DynCastToDeclare(DbgVariableRecord *DVR) {
return DVR->isDbgDeclare() ? DVR : nullptr;
}
template<typename DbgVarTy, typename 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 解除与 DbgMarker 的关联,
然后用BasicBlock::insertDbgRecordBefore或BasicBlock::insertDbgRecordAfter重新插入到指定位置。
删除则调用eraseFromParent。
如下例:
// 旧写法
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());
}
// 新写法
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());
}
孤立调试记录怎么办?
类似如下IR片段:
foo:
%bar = add i32 %baz...
dbg.value(metadata i32 %bar, ...)
br label %xyzzy
你的优化pass可能会删除终结指令(terminator),然后处理此块。过去这种情况下,调试内在依附在指令上非常容易。但调试记录不属于指令,如果末尾没有指令可依附,调试记录会暂时存储在LLVMContext一个map里,待有新终结指令插入or其它操作发生时再复位到正确位置。
极少数情况下,如果删除了终结指令后又删除整个块,可能造成麻烦(官方建议不要这么做)。
还有其它注意点吗?
本文并未穷尽一切调试内在相关代码模式;如开头所述(参考指南),你可在必要时临时在模块上切换回指令内在格式进行修复。绝大多数对调试内在的操作都有等价于调试记录的实现,例外情况可参考 相关类文档,或查找现有代码库的例子,如有疑问亦可到 LLVM论坛 求助。
原文地址:https://llvm.org/docs/RemoveDIsDebugInfo.html
5436

被折叠的 条评论
为什么被折叠?



