调试信息指令引用

本文档解释了 LLVM 如何使用值跟踪或指令引用来确定编译代码生成阶段中调试信息的变量位置。此内容针对从事代码生成目标和优化流程的人员。也可能对任何对低级调试信息处理感兴趣的人有所帮助。

问题陈述

在编译结束时,LLVM 必须生成一个 DWARF 位置列表(或类似内容),描述在每个变量的词法作用域中的每个指令中可以找到该变量的哪个寄存器或堆栈位置。我们可以跟踪变量在编译过程中所在的虚拟寄存器,但是这容易受到寄存器分配期间的寄存器优化和指令移动的影响。

解决方案:指令引用

与其识别变量值所在的虚拟寄存器,不如在指令引用模式下,LLVM 引用定义该值的机器指令和操作数位置。考虑 LLVM IR 指向指令值的方式

%2 = add i32 %0, %1
  #dbg_value(metadata i32 %2,

在 LLVM IR 中,IR 值与计算该值的指令同义,在内存中,值是指向计算指令的指针。指令引用在 LLVM 的代码生成后端实现了这种关系,在指令选择之后。考虑下面的 X86 汇编和指令引用调试信息,对应于之前的 LLVM IR

%2:gr32 = ADD32rr %0, %1, implicit-def $eflags, debug-instr-number 1
DBG_INSTR_REF 1, 0, !123, !456, debug-location !789

虽然函数保持 SSA 形式,虚拟寄存器 %2 足以识别指令计算的值 - 但是函数最终会离开 SSA 形式,并且寄存器优化会掩盖所需值所在的寄存器。相反,识别指令值的更一致方法是引用定义该值的 MachineOperand:独立于哪个寄存器由该 MachineOperand 定义。在上面的代码中,DBG_INSTR_REF 指令引用指令号 1,操作数 0,而 ADD32rr 附加了一个 debug-instr-number 属性,指示它是指令号 1。

将变量位置与寄存器解耦避免了涉及寄存器分配和优化的困难,但在优化指令时需要额外的检测。将指令替换为计算相同值的优化版本的优化必须要么保留指令号,要么记录从旧指令/操作数对到新指令/操作数对的替换 - 请参阅 MachineFunction::substituteDebugValuesForInst。如果未执行调试信息维护,或指令被消除为死代码,则变量位置将安全地删除并标记为“已优化”。例外情况是经过变异而非替换的指令,这些指令始终需要调试信息维护。

寄存器分配注意事项

当寄存器分配器运行时,调试指令不直接引用任何虚拟寄存器,因此在寄存器分配期间不需要昂贵的定位维护(即 LiveDebugVariables)。调试指令与函数断开链接,然后在寄存器分配完成后重新链接。

例外情况是 PHI 指令:一旦寄存器分配完成,这些指令就会在控制流合并处成为隐式定义,并且附加到 PHI 指令的任何调试号都会丢失。为了解决这个问题,在寄存器分配开始时记录 PHI 的调试号(phi-node-elimination),然后在寄存器分配完成后插入 DBG_PHI 指令。这需要在寄存器分配期间维护变量所在的哪个寄存器,但在单个位置(块入口点)而不是指令范围。

一个示例,在寄存器分配之前

bb.2:
  %2 = PHI %1, %bb.0, %2, %bb.1, debug-instr-number 1

之后

bb.2:
  DBG_PHI $rax, 1

LiveDebugValues

在优化和代码布局完成后,必须将有关变量值的信息转换为变量位置,即寄存器和堆栈槽。这在 [LiveDebugValues 传递][LiveDebugValues] 中执行,其中调试指令和机器代码被分离到两个独立的函数中

  • 一个将值分配给变量名,

  • 一个将值分配给机器寄存器和堆栈槽。

LLVM 的现有 SSA 工具用于为每个函数放置 PHI,在变量值和机器位置中包含的值之间,值传播消除了任何不必要的 PHI。然后可以将这两个连接起来,将变量映射到值,然后将值映射到位置,用于函数中的每个指令。

此过程的关键是能够识别值在寄存器和堆栈位置之间移动,以便在值驻留在机器中的整个时间内保留其位置。

所需的靶标支持和过渡指南

指令引用可以在任何目标上工作,但覆盖范围可能很差。良好地支持指令引用需要

  • 实现目标钩子,以允许 LiveDebugValues 跟踪机器中的值,

  • 检测特定于目标的优化,以保留指令号。

目标钩子

TargetInstrInfo::isCopyInstrImpl 必须实现以识别任何类似复制的指令 - LiveDebugValues 使用此功能来识别值何时在寄存器之间移动。

TargetInstrInfo::isLoadFromStackSlotPostFETargetInstrInfo::isStoreToStackSlotPostFE 需要识别溢出和恢复指令。每个都应分别返回目标或源寄存器。LiveDebugValues 将跟踪值从堆栈槽到堆栈槽的移动。此外,任何写入堆栈溢出的指令都应附加 MachineMemoryOperand,以便 LiveDebugValues 可以识别槽已被覆盖。

特定于目标的优化检测

优化分为两种:一种是变异 MachineInstr 以使其执行不同的操作,另一种是创建新的指令来替换旧指令的操作。

前者 *必须* 进行检测 - 相关问题是任何操作数中的任何寄存器定义是否会产生不同的值,这是变异的结果。如果答案是肯定的,那么引用该操作数的 DBG_INSTR_REF 指令可能会将不同的值分配给变量,从而向调试开发人员显示意外的变量值。在这种情况下,在变异的指令上调用 MachineInstr::dropDebugNumber() 以删除其指令号。任何引用它的 DBG_INSTR_REF 将改为生成一个空的变量位置,在调试器中显示为“已优化”。

对于后一种优化,为了提高覆盖率,您应该记录指令号替换:从旧指令号/操作数对到新指令号/操作数对的映射。考虑如果我们用二地址加法替换三地址加法指令

%2:gr32 = ADD32rr %0, %1, debug-instr-number 1

变成

%2:gr32 = ADD32rr %0(tied-def 0), %1, debug-instr-number 2

MachineFunction 中记录了从“指令号 1 操作数 0”到“指令号 2 操作数 0”的替换。在 LiveDebugValues 中,DBG_INSTR_REF 将通过替换表映射以找到它引用的值的最新指令号/操作数号。

使用 MachineFunction::substituteDebugValuesForInst 自动生成旧指令和新指令之间的替换。它假设旧指令中是定义的任何操作数在新指令中在相同操作数位置也是定义的。这在大多数情况下都有效,例如在上面的示例中。

如果操作数号在新旧指令之间不一致,请使用 MachineInstr::getDebugInstrNum 获取新指令的指令号,并使用 MachineFunction::makeDebugValueSubstitution 记录旧指令和新指令中寄存器定义之间的映射。如果旧指令计算的一些值不再由新指令计算,则不记录任何替换 - LiveDebugValues 将安全地删除现在不可用的变量值。

如果您的目标克隆指令,与 TailDuplicator 优化传递非常相似,请不要尝试保留指令号或记录任何替换。MachineFunction::CloneMachineInstr 应删除任何克隆指令的指令号,以避免重复的数字显示给 LiveDebugValues。处理重复指令是当前未实现的指令引用的自然扩展。

[LiveDebugValues]: project:SourceLevelDebugging.rst#LiveDebugValues 变量位置的扩展