如何更新调试信息:LLVM Pass 作者指南

简介

某些类型的代码转换可能会无意中导致调试信息丢失,或者更糟的是,使调试信息错误地表示程序的状态。

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

有关 LLVM 调试信息背后的理念的更多信息,请参阅 使用 LLVM 进行源代码级调试

更新调试位置的规则

何时保留指令位置

如果指令仍然在其基本块中,或者其基本块被折叠到无条件分支的前驱中,则转换应保留指令的调试位置。要使用的 API 是 IRBuilderInstruction::setDebugLoc

此规则的目的是确保常见的块内优化保留在与它们触及的指令对应的源位置设置断点的能力。如果这种能力丧失,调试、崩溃日志和 SamplePGO 准确性将受到严重影响。

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

  • 指令调度。块内指令重排序不应删除源位置,即使这可能导致跳跃式的单步执行行为。

  • 简单跳转线程化。例如,如果块 B1 无条件跳转到 B2并且是其唯一的前驱,则可以将来自 B2 的指令提升到 B1 中。应保留来自 B2 的源位置。

  • 替换或扩展指令的窥孔优化,例如 (add X X) => (shl X 1)shl 指令的位置应与 add 指令的位置相同。

  • 尾部复制。例如,如果块 B1B2 都无条件分支到 B3,并且 B3 可以折叠到其前驱中,则应保留来自 B3 的源位置。

此规则适用的转换示例包括

  • LICM。例如,如果指令从循环体移动到前置标头,则 丢弃位置 的规则适用。

除了上述规则外,如果目标块已经包含具有相同调试位置的指令,则转换还应保留在基本块之间移动的指令的调试位置。

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

  • 在基本块之间移动指令。例如,如果 BB1 中的指令 I1BB2 中的 I2 之前移动,如果 I1I2 具有相同的源位置,则可以保留 I1 的源位置。

何时合并指令位置

如果转换用一个或多个新指令替换多个指令,并且新指令产生多个原始指令的输出,则转换应合并指令位置。要使用的 API 是 Instruction::applyMergedLocation。对于每个新指令 I,其新位置应是所有输出由 I 产生的指令的位置的合并。通常,这包括任何被新指令 RAUWed 的指令,并排除任何仅产生 RAUWed 指令使用的中间值的指令。

此规则的目的是确保 a) 单个合并指令具有附加了准确范围的位置,并且 b) 防止误导性的单步执行(或断点)行为。通常,合并的指令是可能陷入困境的内存访问:附加准确的范围极大地有助于通过识别发生错误内存访问的(可能内联的)函数来辅助崩溃分类。此规则还旨在通过禁止包含合并指令的块的样本被错误地归因于包含要合并的指令之一的块的情况来帮助 SamplePGO。

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

  • 从条件分支的所有后继者提升相同的指令,或从到后支配块的所有路径中下沉这些指令。例如,合并 CFG 菱形两侧出现的相同加载/存储(请参阅 MergedLoadStoreMotion pass)。对于提升/下沉的每组相同指令,应将所有位置的合并应用于合并的指令。

  • 合并相同的循环不变存储(请参阅 LICM 实用程序 llvm::promoteLoopAccessesToScalars)。

  • 标量指令被组合成向量指令,例如 (add A1, B1), (add A2, B2) => (add (A1, A2), (B1, B2))。由于新的向量 add 同时计算两个原始 add 指令的结果,因此它应使用两个位置的合并。类似地,如果先前的优化已经产生了向量 (A1, A2)(B2, B1),那么我们可能会创建一个 (shufflevector (1, 0), (B2, B1)) 指令来生成向量 add(B1, B2);在这种情况下,我们创建了两个指令来替换原始 adds,因此两个新指令都应使用合并的位置。

此规则适用的转换示例包括

  • 删除冗余指令的块内窥孔,例如 (sext (zext i8 %x to i16) to i32) => (zext i8 %x to i32)。内部 zext 被修改但仍保留在其块中,因此 保留位置 的规则应适用。

  • 将多个指令组合在一起的窥孔优化,例如 (add (mul A B) C) => llvm.fma.f32(A, B, C)。请注意,mul 的结果不再出现在程序中,而 add 的结果现在由 fma 产生,因此应使用 add 的位置。

  • 将 if-then-else CFG 菱形转换为 select。保留推测指令的调试位置可能会使条件看起来为真,而实际上并非如此(反之亦然),这会导致令人困惑的单步执行体验。丢弃位置 的规则应在此处适用。

  • 当位置以前不可达时,提升/下沉会使其可达。考虑从具有三个 case 的 switch 的前两个 case 中提升两个具有相同位置的相同指令。合并它们的位置将使前两个 case 的位置在采用第三个 case 时可达。丢弃位置 的规则适用。

何时丢弃指令位置

如果 保留合并 调试位置的规则不适用,则转换应丢弃调试位置。要使用的 API 是 Instruction::dropLocation()

此规则的目的是防止在指令与源位置没有明确、明确关系的情况下出现不稳定或误导性的单步执行行为。

为了处理没有位置的指令,DWARF 生成器默认允许在标签之后最后设置的位置向前级联,或者在没有先前位置可用时设置具有可行范围信息的第 0 行位置。

有关何时应用丢弃位置规则的示例,请参阅有关 合并位置 的部分中的讨论。

更新调试值的规则

删除 IR 级别的指令

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

  • 如果可以 RAUW dying 指令,请执行此操作。Value::replaceAllUsesWith API 透明地更新 dying 指令的调试用途,以指向替换值。

  • 如果 dying 指令无法 RAUW,请对其调用 llvm::salvageDebugInfo。这会尽最大努力通过将 dying 指令的效果描述为 DIExpression 来重写 dying 指令的调试用途。

  • 如果 dying 指令的操作数之一将变得微不足道地死亡,请使用 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
}

现在,这是在不必要的截断指令 %d 被简化指令替换后发生的情况

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

请注意,在删除 %d 后,其操作数 %c 的所有用途都变得微不足道地死亡。曾经指向 %c 的调试用途现在是 undef,并且调试信息不必要地丢失了。

要解决此问题,请执行

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

这会产生更好的调试信息,因为 %c 的调试用途得以保留

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

您可能已经注意到 %simplified%c 更窄:这不是问题,因为 llvm::replaceAllDbgUsesWith 负责将必要的转换操作插入到更新的调试用途的 DIExpression 中。

删除 MIR 级别的 MachineInstr

待办

更新 DIAssignID 附件的规则

DIAssignID 元数据附件由赋值跟踪使用,赋值跟踪当前是一种实验性调试模式。

有关如何更新它们以及有关赋值跟踪的更多信息,请参阅 调试信息赋值跟踪

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

IR 级别转换的突变测试

在许多情况下,可以自动突变转换的 IR 测试用例,以测试该转换中的调试信息处理。这是一种测试正确调试信息处理的简单方法。

debugify 实用程序 pass

debugify 测试实用程序只是一对 pass:debugifycheck-debugify

第一个将合成调试信息应用于模块的每个指令,第二个检查在优化发生后此 DI 是否仍然可用,并在执行此操作时报告任何错误/警告。

指令被分配顺序递增的行位置,并立即被尽可能多的调试值记录使用。

例如,这是一个之前的模块

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.dbg.cu = !{!0}
!llvm.debugify = !{!3, !4}
!llvm.module.flags = !{!5}

!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "debugify", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, enums: !2)
!1 = !DIFile(filename: "debugify-sample.ll", directory: "/")
!2 = !{}
!3 = !{i32 5}
!4 = !{i32 2}
!5 = !{i32 2, !"Debug Info Version", i32 3}
!6 = distinct !DISubprogram(name: "f", linkageName: "f", scope: null, file: !1, line: 1, type: !7, isLocal: false, isDefinition: true, scopeLine: 1, isOptimized: true, unit: !0, retainedNodes: !8)
!7 = !DISubroutineType(types: !2)
!8 = !{!9, !11}
!9 = !DILocalVariable(name: "1", scope: !6, file: !1, line: 1, type: !10)
!10 = !DIBasicType(name: "ty64", size: 64, encoding: DW_ATE_unsigned)
!11 = !DILocalVariable(name: "2", scope: !6, file: !1, line: 3, type: !10)
!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

使用 debugify 的一种简单方法如下

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

这将将合成 DI 注入到 sample.ll,运行 pass-to-test,然后检查是否缺少 DI。-check-debugify 步骤当然可以省略,以支持更可定制的 FileCheck 指令。

还有其他一些运行 debugify 的方法可用

# Same as the above example.
$ opt -enable-debugify -pass-to-test sample.ll

# Suppresses verbose debugify output.
$ opt -enable-debugify -debugify-quiet -pass-to-test sample.ll

# Prepend -debugify before and append -check-debugify -strip after
# each pass on the pipeline (similar to -verify-each).
$ opt -debugify-each -O2 sample.ll

为了使 check-debugify 工作,DI 必须来自 debugify。因此,将跳过具有现有 DI 的模块。

debugify 可用于测试后端,例如

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

还有一个 MIR 级别的 debugify pass,可以在每个后端 pass 之前运行,请参阅:MIR 级别转换的突变测试

debugify 在回归测试中

debugify pass 的输出必须足够稳定才能在回归测试中使用。不允许对此 pass 的更改破坏现有测试。

注意

回归测试必须是健壮的。避免在检查行中硬编码行号/变量号。如果无法避免这种情况(例如,如果测试不够精确),则首选将测试移动到其自己的文件中。

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

除了自动生成调试信息外,debugify 实用程序 pass 提供的检查还可以用于测试预先存在的调试信息元数据的保留。它可以按如下方式运行

# Run the pass by checking original Debug Info preservation.
$ opt -verify-debuginfo-preserve -pass-to-test sample.ll

# Check the preservation of original Debug Info after each pass.
$ opt -verify-each-debuginfo-preserve -O2 sample.ll

限制观察到的函数数量以加快分析速度

# Test up to 100 functions (per compile unit) per pass.
$ 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 脚本生成 HTML 页面,其中以更易于阅读的形式报告问题,如下所示

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

可以从前端级别调用原始调试信息保留的测试,如下所示

# Test each pass.
$ clang -Xclang -fverify-debuginfo-preserve -g -O2 sample.c

# Test each pass and export the issues report into the JSON file.
$ clang -Xclang -fverify-debuginfo-preserve -Xclang -fverify-debuginfo-preserve-export=sample.json -g -O2 sample.c

请注意,对于源位置和调试记录检查,存在一些已知的误报,因此将在未来的工作中解决。

MIR 级别转换的突变测试

IR 级别转换的突变测试 中描述的 debugify 实用程序的变体也可以用于 MIR 级别转换:与 IR 级别 pass 非常相似,mir-debugify 将顺序递增的行位置插入到 Module 中的每个 MachineInstr。MIR 级别的 mir-check-debugify 类似于 IR 级别的 check-debugify pass。

例如,这是一个之前的代码片段

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 对该 def 的使用。如果指令未定义寄存器,但可以跟随调试指令,则 MIRDebugify 会插入一个引用常量的 DBG_VALUEDBG_VALUE 的插入可以通过设置 -debugify-level=locations 来禁用。

要运行 MIRDebugify 一次,只需将 mir-debugify 插入到您的 llc 调用中,例如

# Before some other pass.
$ llc -run-pass=mir-debugify,other-pass ...

# After some other 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 突变 pass 之间,请运行

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

使用 LostDebugLocObserver

待办