LLVM - 更新调试信息的方法

更新调试信息的方法(How To Update Debug Info)

介绍

某些类型的代码变换可能会意外导致调试信息丢失,或更糟的是,使调试信息错误地反映程序状态。调试信息的可用性对于 SamplePGO 也至关重要。

本文档规范了在各种代码变换中如何正确更新调试信息,并为如何为任意变换创建有针对性的调试信息测试提供建议。

有关 LLVM 调试信息哲学的更多内容,请参见《Source Level Debugging with LLVM》。

更新调试位置(debug locations)的规则

何时保留指令的位置

如果指令仍然保留在它的基本块中,或者它的基本块被折叠到一个无条件跳转的前驱块中,则变换应保留该指令的调试位置。应使用的 API 包括 IRBuilderInstruction::setDebugLoc

此规则的目的是确保常见的块局部优化不会丢失设置断点到其所涉及源位置的能力。如果丧失这种能力,调试、崩溃日志以及 SamplePGO 的准确性都会受到严重影响。

应遵循此规则的变换示例包括:

  • 指令调度(instruction scheduling)。块内的指令重排不应丢弃源位置,即便这可能导致单步调试时“跳跃”式的行为。
  • 简单的跳转合并(jump threading)。例如,若块 B1 无条件跳转到 B2,且是 B2 的唯一前驱,则可以把 B2 的指令提升到 B1。来自 B2 的源位置应被保留。
  • 替换或扩展指令的 peephole 优化,例如将 (add X X) 替为 (shl X 1)shl 指令的位置应与原 add 指令相同。
  • 尾部复制(tail duplication)。例如,若 B1B2 都无条件跳转到 B3B3 可以折叠到其前驱块,来自 B3 的源位置应被保留。

下列变换不适用此规则:

  • 循环不变代码外提(LICM)。例如若某指令从循环体移到 preheader,则应适用“丢弃位置”的规则。

此外,如果指令在基本块间移动,而目标块已经包含一个具有相同调试位置的指令,变换也应保留该指令的位置。

示例:在基本块间移动指令。如果将 I1BB1 移动到 BB2 中并置于 I2 之前,若 I1I2 具有相同的源位置,则可以保留 I1 的源位置。

何时合并指令的位置

当变换用一个或多个新指令替换多个原始指令,并且新的指令产生了多个原始指令的输出时,应合并这些指令的位置。应使用的 API 为 Instruction::applyMergedLocation。对于每个新指令 I,它的新位置应当是所有由 I 产生输出的原始指令位置的合并。通常,这包括被新指令 RAUW(replace-all-uses-with)替换的任何指令,但不包括仅用于被 RAUW 指令的中间值的指令。

此规则的目的是确保:a) 合并后的单个指令具有带有准确作用域的位置信息;b) 防止误导性的单步调试或断点行为。合并后的指令通常是可能发生陷阱的内存访问:如果带有准确的作用域信息,在崩溃分析中可帮助识别实际发生错误的(可能内联的)函数。

为保持 SamplePGO 的不同源位置标识,通常有利于在合并时保留任意但确定性的位置信息,而不是在合并过程中丢弃行/列信息。特别是对于调用(call)位置的丢失,会抑制诸如间接调用提升(indirect call promotion)等优化。在可以选择的情况下,可以临时保留位置信息,直到实现能在行表中准确表达合并指令为止。

应遵循此规则的变换示例包括:

  • 将分支的所有后续块中相同的指令提升或将它们下沉到后支配块(例如 MergedLoadStoreMotion pass)。对每组要提升/下沉的相同指令,应对合并后的指令应用所有原始位置的合并。
  • 合并相同的循环不变 store(见 LICM 的 llvm::promoteLoopAccessesToScalars)。
  • 将标量指令合并成向量指令,例如 (add A1, B1), (add A2, B2) 合并为 (add (A1, A2), (B1, B2))。新向量 add 同时计算两个原 add 的结果,因此应使用两个位置的合并。同样地,如果已有 (A1, A2)(B2, B1),需要用 shufflevector 生成 (B1, B2),则该 shufflevector 与随后向量 add 都应使用合并的位置(因为两个新指令共同替代了原来的 adds)。

不适用此规则的变换示例包括:

  • 在块局部的 peephole 中删除冗余指令,例如 (sext (zext i8 %x to i16) to i32) => (zext i8 %x to i32)。内部 zext 被修改但仍在其块中,应适用“保留位置”规则。
  • 将多个指令合并为单个复杂指令的 peephole,例如 (add (mul A B) C) => llvm.fma.f32(A, B, C)。注意 mul 的结果在程序中不再出现,而 add 的结果由 fma 产生,因此 add 的位置应被用于 fma
  • 把 if-then-else 的 CFG 菱形转换为 select。保留被推测的指令的位置可能会让单步调试显示条件为真或为假而令人困惑,因此应适用“丢弃位置”规则。
  • 当提升/下沉会让某个位置在之前不可到达的路径变得可到达时。例如从 switch 的两个 case 中提升两个具有相同位置的相同指令(而第三个 case 不含该指令),合并它们的位置会在第三个 case 被选中时使该位置变得可达。此时应适用“丢弃位置”规则。

何时丢弃指令的位置

如果既不适用“保留位置”规则也不适用“合并位置”规则,则变换应丢弃调试位置。应使用的 API 为 Instruction::dropLocation()

此规则的目的是在指令与某个源位置没有明确且无歧义的关系时,防止单步调试行为变得混乱或误导。

当指令没有位置时,DWARF 生成器默认允许标签之后最后设置的位置向前传播,或者若没有先前的位置可用,则设置一个行号为 0 的位置并保留有效的作用域信息。

关于何时丢弃位置的例子,请参见上文“合并位置”部分的讨论。

何时对调试位置做重映射(remap)

当代码路径被复制(如循环展开或跳转合并时),必须使用 mapAtomInstanceRemapSourceAtom 对 DILocation 附件做重映射。这是为了支持 Key Instructions 调试信息特性。详见《Key Instructions debug info in LLVM and Clang》。

为新指令设置位置

每当创建新指令且没有合适位置时,应为该指令做相应注解。有一组特殊的 DebugLoc 值可以设置到指令上,以注释该指令没有有效位置的原因,分别如下:

  • DebugLoc::getCompilerGenerated():指示指令为编译器生成的指令,即不对应用户源代码。
  • DebugLoc::getDropped():指示指令已被有意根据“丢弃位置”规则移除源位置;此值由 Instruction::dropLocation() 自动设置。
  • DebugLoc::getUnknown():指示指令没有已知或当前可确定的源位置,例如无法判断正确源位置或源位置以 LLVM 目前无法表示的方式存在二义性。
  • DebugLoc::getTemporary():用于那些不期望被发射的指令(例如 UnreachableInst);如果最终把一个临时位置发射到目标文件/汇编中,说明出现了问题。

在适用的情形下,应使用这些特殊位置之一,而不是把指令留为无位置或显式设置为空位置 DebugLoc()。在带有覆盖追踪的 LLVM 编译(通过 -DLLVM_ENABLE_DEBUGLOC_COVERAGE_TRACKING="COVERAGE")下,这些特殊位置会被记录用于检测意外丢失的位置;因此最重要的规则是:只有在确实明确适合时才使用这些特殊位置 —— 缺失的位置能被检测并修复,而错误地注释了某条指令会更难发现和纠正。但如果这些特殊位置确实适用,则应该使用它们以避免产生误报。

更新调试值(debug values)的规则

删除 IR 级别的指令

当删除一个 Instruction 时,其调试使用(debug uses)会变为 undef。这导致调试信息的丢失:一个或多个源变量的值变得不可用,表现为 #dbg_value(undef, ...)。当无法重构被删除指令的值时,这是最好的结果。但通常可以做得更好:

  • 如果要删除的指令可以被 RAUW(replace all uses with)替换,则应这样做。Value::replaceAllUsesWith 会透明地更新被删除指令的调试使用,使其指向替换值。
  • 如果不能 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
}

在删除不必要的 trunc 后,可能得到:

define i16 @foo(i16 %a) {
    #dbg_value(i32 undef, ...)
  %simplified = and i16 %a, 15
  ret i16 %simplified
}

注意,在删除 %d 后,%c 的所有使用变为可去除(trivially dead),原本指向 %cdbg_value 现在成了 undef,从而不必要地丢失了调试信息。

解决方法是:

llvm::replaceAllDbgUsesWith(%c, theSimplifiedAndInstruction, ...)

这样可以保留调试信息:

define i16 @foo(i16 %a) {
  %simplified = and i16 %a, 15
    #dbg_value(i16 %simplified, ...)
  ret i16 %simplified
}

你可能注意到 %simplified 的类型比 %c 更窄:这不是问题,因为 llvm::replaceAllDbgUsesWith 会在被更新的 debug uses 的 DIExpression 中插入必要的转换操作。

删除 MIR 级别的 MachineInstr

TODO(待补充)

更新 DIAssignID 附件的规则

DIAssignID 元数据附件用于 Assignment Tracking(赋值追踪),该功能目前为实验性调试模式。

有关如何更新它们与 Assignment Tracking 的更多信息,请参见《Debug Info Assignment Tracking》。

如何自动将测试转换为调试信息测试

针对 IR 级别变换的突变测试(mutation testing)

对于很多 IR 级别的变换,可以自动把一个变换的 IR 测试用例突变(mutate)为测试该变换的调试信息处理情况的测试。这是测试调试信息处理正确性的简单方法。

debugify 工具 pass

debugify 测试工具是两个 pass 的组合:debugifycheck-debugify

第一个 pass 给模块的每条指令注入合成的调试信息(synthetic DI),第二个 pass 在优化发生后检查这些 DI 是否仍然存在,并在过程中报告任何错误/警告。

debugify 为指令分配顺序递增的行位置信息,并尽可能地通过 dbg_value 记录立即使用这些指令。

例如,运行前的模块:

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.debugify = !{!3, !4}
...
!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

一个简单的用法示例:

$ opt -debugify -pass-to-test -check-debugify sample.ll

这将在 sample.ll 中注入合成 DI,运行 pass-to-test,然后用 -check-debugify 检查缺失的 DI。-check-debugify 可以省略,改用更可定制的 FileCheck 指令。

其他用法示例:

  • 与上例等同:
    $ opt -enable-debugify -pass-to-test sample.ll
    
  • 抑制冗长输出:
    $ opt -enable-debugify -debugify-quiet -pass-to-test sample.ll
    
  • 在流水线中于每个 pass 前追加 -debugify 并在末尾追加 -check-debugify -strip(类似 -verify-each):
    $ opt -debugify-each -O2 sample.ll
    

为使 check-debugify 生效,DI 必须来自 debugify;因此带已有 DI 的模块会被跳过。

debugify 也可用于测试后端,例如:

$ opt -debugify < sample.ll | llc -o -

还有一个 MIR 级别的 debugify,可在每个后端 pass 前运行,详见下文关于 MIR 级别突变测试的章节。

debugify 在回归测试中的使用

debugify 的输出须足够稳定以用于回归测试。不得更改该 pass 以破坏现有测试。

注意:回归测试必须具有鲁棒性。避免在 check 行中写死行号或变量编号;如果无法避免(例如测试无法精确),则最好把该测试移到独立文件。

使用覆盖追踪去除误报

如上所述,有合理原因导致某些指令没有源位置。因此在检测被丢弃或未生成的源位置时,可能希望避免把那些有意缺失位置的情况当作错误。可以在 LLVM 中启用“覆盖追踪(coverage tracking)”特性来忽略这类情况:在构建 LLVM 时设置 CMake 标志 -DLLVM_ENABLE_DEBUGLOC_COVERAGE_TRACKING=COVERAGE。启用后,LLVM 会在运行时跟踪 DebugLoc 注释,从而使 debugify 忽略那些被显式记录为没有源位置的指令。

为便于定位 debugify 检测到的源位置错误,你可以启用“origin tracking”:
-DLLVM_ENABLE_DEBUGLOC_COVERAGE_TRACKING=COVERAGE_AND_ORIGIN。该标志会在 debugify 输出中附带一或多条栈追踪,记录创建空源位置的点以及该位置被拷贝到其它指令的每个点,从而在多数情况下很容易找到问题根源。如果启用 origin tracking,建议同时带调试信息构建 LLVM,以便栈追踪能被准确符号化。

注意:覆盖追踪特性主要用于 debugify 的“原始调试信息保留(original debug info preservation)”模式,因此在其它设置下可能不可靠。使用该模式时,COVERAGE_AND_ORIGIN 产生的栈追踪会以易读格式打印,作为 llvm-original-di-preservation.py 脚本生成的报告的一部分。

测试优化中的原始调试信息保留

除了自动生成调试信息之外,debugify 工具的检查也可用于测试对预先存在的调试信息元数据的保留。可以这样运行:

# 运行 pass,并检查原始 Debug Info 的保留
$ opt -verify-debuginfo-preserve -pass-to-test sample.ll

# 在每个 pass 之后检查原始 Debug Info 的保留
$ opt -verify-each-debuginfo-preserve -O2 sample.ll

为了加速分析,可以限制观察的函数数量:

# 每个编译单元每个 pass 最多测试 100 个函数
$ 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 将 JSON 转为可读的 HTML 报告:

$ llvm-original-di-preservation.py sample.json --report-file sample.html

可以从前端层面调用原始调试信息保留测试:

# 测试每个 pass
$ clang -Xclang -fverify-debuginfo-preserve -g -O2 sample.c

# 测试每个 pass 并导出问题到 JSON
$ clang -Xclang -fverify-debuginfo-preserve -Xclang -fverify-debuginfo-preserve-export=sample.json -g -O2 sample.c

请注意,目前仍存在一些关于源位置与调试记录检测的已知误报问题,将在未来工作中解决。

针对 MIR 级别变换的突变测试

debugify 在 IR 级别的变体也可用于 MIR 级别变换:mir-debugify 会向 Module 中的每个 MachineInstr 插入顺序递增的行位置,mir-check-debugify 类似于 IR 级别的 check-debugify

例如,变换前:

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。如果某指令不定义寄存器但可以跟随一个 debug 指令,MIRDebugify 会插入一个引用常量的 DBG_VALUE。可以通过 -debugify-level=locations 禁用 DBG_VALUE 的插入。

运行 MIRDebugify:

  • 在某个 pass 之前运行:
    $ llc -run-pass=mir-debugify,other-pass ...
    
  • 在某个 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-debugifymir-check-debugify 与/或 mir-strip-debug 结合使用,可帮助识别在带有调试信息时会出现问题的后端变换。例如,要在 AArch64 后端测试中把正常的 pass 在 MIRDebugify 与 MIRStripDebugify 之间“夹”入,可运行:

$ llvm-lit test/CodeGen/AArch64 -Dllc="llc -debugify-and-strip-all-safe"

使用 LostDebugLocObserver

TODO(待补充)

原文地址:https://llvm.org/docs/HowToUpdateDebugInfo.html

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csdddn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值