如何更新调试信息: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 中的指令 I1 移动到 BB2 中的 I2 之前,如果 I1I2 具有相同的源位置,则可以保留 I1 的源位置。

何时合并指令位置

如果转换用单个合并指令替换多个指令,并且该合并指令与任何原始指令的位置都不对应,则转换应合并指令位置。要使用的 API 是 Instruction::applyMergedLocation

此规则的目的是确保 a) 单个合并指令具有附加了准确作用域的位置,以及 b) 防止误导性的单步执行(或断点)行为。通常,合并的指令是可能导致陷阱的内存访问:附加准确的作用域通过识别发生错误内存访问的(可能是内联的)函数极大地帮助了崩溃的排查。此规则也旨在通过禁止将包含合并指令的块的样本错误地归因于包含要合并的指令之一的块的场景来帮助 SamplePGO。

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

  • 合并出现在 CFG 菱形两侧的相同加载/存储(参见 MergedLoadStoreMotion Pass)。

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

  • 将多个指令组合在一起的窥孔优化,例如 (add (mul A B) C) => llvm.fma.f32(A, B, C)。请注意, fma 的位置与 muladd 指令的位置不完全对应。

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

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

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

  • 将出现在多个后继块中的相同指令提升到前驱块中(参见 BranchFolder::HoistCommonCodeInSuccs)。在这种情况下,没有单个合并指令。适用 丢弃位置 的规则。

何时丢弃指令位置

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

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

为了处理没有位置的指令,DWARF 生成器默认为允许标签之后的最后一个设置的位置级联向前,或者如果以前没有可用位置,则设置具有可行作用域信息的行 0 位置。

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

更新调试值的规则

删除 IR 级别的指令

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

  • 如果可以对垂死指令进行 RAUW,则执行此操作。 Value::replaceAllUsesWith API 透明地更新垂死指令的调试使用以指向替换值。

  • 如果无法对垂死指令进行 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
}

现在,以下是替换不必要的截断指令 %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 会负责将必要的转换操作插入到更新的调试使用的 DIExpressions 中。

删除 MIR 级别 MachineInstr

待办事项

更新 DIAssignID 附加项的规则

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

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

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

IR 级别转换的变异测试

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

debugify 实用程序传递

debugify 测试实用程序只是一对传递: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 传递可以在每个后端传递之前运行,请参阅:MIR 级别转换的变异测试

回归测试中的 debugify

debugify 传递的输出必须足够稳定,才能用于回归测试。对该传递的更改不允许破坏现有测试。

注意

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

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

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

# 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 级别传递非常相似,mir-debugify 将顺序递增的行位置插入到 Module 中的每个 MachineInstr 中。MIR 级别 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 使用。如果指令没有定义寄存器,但可以后跟调试指令,则 MIRDebugify 会插入一个引用常量的 DBG_VALUE。可以通过设置 -debugify-level=locations 来禁用 DBG_VALUE 的插入。

要运行一次 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 ...

要在管道中的每个传递之前运行 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>

如果要在管道中的每个传递之后检查它,请使用 -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 来识别在存在调试信息的情况下出现故障的后端转换可能很有用。例如,要使用所有正常传递“夹在”MIRDebugify 和 MIRStripDebugify 变异传递之间的 AArch64 后端测试运行,请运行

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

使用 LostDebugLocObserver

待办事项