调试信息迁移:从内联函数到记录

我们计划从 LLVM 中移除调试信息内联函数,因为它们速度慢、难以使用,并且如果优化过程没有预期它们,可能会造成混淆。与其使用如下所示的指令序列

    %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 内联函数表示调试信息记录,不如将其打印为

    %add = add i32 %foo, %bar
      #dbg_value(%add, ...
    %sub = sub i32 %add, %tosub
      #dbg_value(%sub, ...
    call void @a_normal_function()

调试记录不是指令,不会出现在指令列表中,并且除非您刻意查找,否则不会出现在您的优化过程。

太好了,我需要做什么?

很少——我们已经为所有 LLVM 添加了处理这些新记录("DbgRecords")的工具,并且行为与以前的 LLVM 行为相同。目前默认启用此功能,以便 DbgRecords 将默认在内存、IR 和比特码中使用。

API 更改

需要注意两个重要的更改。首先,我们在 BasicBlock::iterator 类中添加了一位与调试相关的有效数据(这样我们就可以确定范围是否打算在块的开头包含调试信息)。这意味着在编写插入 LLVM IR 指令的过程时,您需要使用 BasicBlock::iterator 而不是裸 Instruction * 来识别位置。大多数情况下,这意味着在确定您打算插入内容的位置后,您还必须在指令位置上调用 getIterator——但是当在块的开头插入时,您*必须*使用 getFirstInsertionPtgetFirstNonPHIItbegin 并使用该迭代器进行插入,而不仅仅是获取指向第一个指令的指针。

第二个问题是,如果您手动将指令序列从一个位置转移到另一个位置,例如重复使用 moveBefore 而您可能使用了 splice,那么您应该改用 moveBeforePreserving 方法。 moveBeforePreserving 会将调试信息记录与其附加到的指令一起转移。这在今天会自动发生——如果您在指令序列的每个元素上使用 moveBefore,则调试内联函数将在代码的正常过程中移动,但对于非指令调试信息,我们会失去这种行为。

有关如何更新现有代码以支持调试记录的更深入概述,请参阅下面的指南

文本 IR 更改

随着我们从使用调试内联函数到使用调试记录的转变,任何依赖于解析 LLVM 生成的 IR 的工具都需要处理新的格式。在大多数情况下,调试内联函数调用的打印形式与调试记录之间的区别微不足道

  1. 添加了额外的 2 个空格缩进。

  2. 文本 (tail|notail|musttail)? call void @llvm.dbg.<type> 被替换为 #dbg_<type>

  3. 从每个内联函数参数中删除了前导 metadata

  4. DILocation 从以 !dbg !<Num> 格式的指令附件更改为普通参数,即 !<Num>,作为最终参数传递给调试记录。

遵循这些规则,我们有以下调试内联函数和等效的调试记录示例

; Debug Intrinsic:
  call void @llvm.dbg.value(metadata i32 %add, metadata !10, metadata !DIExpression()), !dbg !20
; Debug Record:
    #dbg_value(i32 %add, !10, !DIExpression(), !20)

测试更新

LLVM 主代码库下游的任何测试 LLVM 的 IR 输出的测试,都可能由于更改为使用记录而导致中断。鉴于上述更新规则,将单个测试更新为预期记录而不是内联函数应该很简单。但是,更新许多测试可能很繁琐;为了更新主存储库中的 lit 测试,使用了以下步骤

  1. 将失败的 lit 测试列表收集到单个文件 failing-tests.txt 中,用(并以)换行符分隔。

  2. 使用以下行将失败的测试拆分为使用 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
    
  3. 对于使用 update_test_checks 的测试,运行相应的 update_test_checks 脚本——对于 LLVM 主代码库,这是通过以下方式实现的

    $ 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
    
  4. 其余测试可以手动更新,但如果测试数量很大,则以下脚本可能有用;首先,一个用于从文件中提取检查行前缀的脚本

    $ cat ./get-checks.sh
    #!/bin/bash
    
    # Always add CHECK, since it's more effort than it's worth to filter files where
    # every RUN line uses other check prefixes.
    # Then detect every instance of "check-prefix(es)=..." and add the
    # comma-separated arguments as extra checks.
    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
    

    然后是第二个脚本,用于执行实际更新每个失败测试中的检查行的工作,并使用一系列简单的替换模式

    $ cat ./substitute-checks.sh
    #!/bin/bash
    
    file="$1"
    check="$2"
    
    # Any test that explicitly tests debug intrinsic output is not suitable to
    # update by this script.
    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_\7/
    :postcall
    /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"
    

    这两个脚本组合起来,可以对 manual-tests.txt 中的列表执行以下操作

    $ cat manual-tests.txt | xargs ./get-checks.sh | sort | uniq | awk -F ',' '{ system("./substitute-checks.sh " $1 " " $2) }'
    

    这些脚本成功处理了 clang/testllvm/test 中绝大多数的检查。

  5. 验证生成的测试是否通过,并检测任何失败的测试

    $ 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%)
    
  6. 某些测试可能失败了——更新脚本很简单,并且不会跨行保留任何上下文,因此有些情况它们无法处理;其余情况必须手动更新(或由其他脚本处理)。

C-API 更改

添加的一些新函数是临时的,将来会弃用。目的是在过渡期间帮助下游项目适应。

Deleted functions
-----------------
LLVMDIBuilderInsertDeclareBefore   # Insert a debug record (new debug info format) instead of a debug intrinsic (old debug info format).
LLVMDIBuilderInsertDeclareAtEnd    # Same as above.
LLVMDIBuilderInsertDbgValueBefore  # Same as above.
LLVMDIBuilderInsertDbgValueAtEnd   # Same as above.

New functions (to be deprecated)
--------------------------------
LLVMIsNewDbgInfoFormat     # Returns true if the module is in the new non-instruction mode.
LLVMSetIsNewDbgInfoFormat  # Convert to the requested debug info format.

New functions (no plans to deprecate)
-------------------------------------
LLVMDIBuilderInsertDeclareRecordBefore   # Insert a debug record (new debug info format).
LLVMDIBuilderInsertDeclareRecordAtEnd    # Same as above. See info below.
LLVMDIBuilderInsertDbgValueRecordBefore  # Same as above. See info below.
LLVMDIBuilderInsertDbgValueRecordAtEnd   # Same as above. See info below.

LLVMPositionBuilderBeforeDbgRecords          # See info below.
LLVMPositionBuilderBeforeInstrAndDbgRecords  # See info below.

LLVMDIBuilderInsertDeclareRecordBeforeLLVMDIBuilderInsertDeclareRecordAtEndLLVMDIBuilderInsertDbgValueRecordBeforeLLVMDIBuilderInsertDbgValueRecordAtEnd 正在替换已删除的 LLVMDIBuilderInsertDeclareBefore-style 函数。

LLVMPositionBuilderBeforeDbgRecordsLLVMPositionBuilderBeforeInstrAndDbgRecords 的行为与 LLVMPositionBuilderLLVMPositionBuilderBefore 相同,只是插入位置设置在目标指令之前的调试记录之前。请注意,这并不意味着跳过选定指令之前的调试内联函数,而只是调试记录(与调试记录不同,调试记录本身不是指令)。

如果您不知道要调用哪个函数,请遵循以下规则:如果您尝试在块的开头插入,或出于任何其他原因故意跳过调试内联函数以确定插入点,则调用新函数。

LLVMPositionBuilderLLVMPositionBuilderBefore 未更改。它们在指示的指令之前但任何附加的调试记录之后插入。

新的“调试记录”模型

以下是替换调试内联函数的新表示形式的简要概述;有关更新旧代码的说明性指南,请参阅此处

您到底用什么替换了调试内联函数?

我们使用一个名为 DbgRecord 的专用 C++ 类来存储调试信息,在每个调试内联函数实例和任何 LLVM IR 程序中的每个 DbgRecord 对象之间存在一对一的关系;这些 DbgRecord 在 IR 中表示为非指令调试记录,如[源级调试](project:SourceLevelDebugging.rst#Debug Records) 文档中所述。此类有一组子类,用于存储与调试内联函数中存储的信息完全相同的信息。每个子类还几乎具有完全相同的函数集,其行为方式相同

https://llvm.net.cn/docs/doxygen/classllvm_1_1DbgRecord.html https://llvm.net.cn/docs/doxygen/classllvm_1_1DbgVariableRecord.html https://llvm.net.cn/docs/doxygen/classllvm_1_1DbgLabelRecord.html

例如,在泛型(自动参数)lambda 中,您可以将 DbgVariableRecord 视为 dbg.value/dbg.declare/dbg.assign 内联函数,对于 DbgLabelRecorddbg.label 也是如此。

这些 DbgRecords 如何融入指令流?

如下所示

                 +---------------+          +---------------+
---------------->|  Instruction  +--------->|  Instruction  |
                 +-------+-------+          +---------------+
                         |
                         |
                         |
                         |
                         v
                  +-------------+
          <-------+  DbgMarker  |<-------
         /        +-------------+        \
        /                                 \
       /                                   \
      v                                     ^
 +-------------+    +-------------+   +-------------+
 |  DbgRecord  +--->|  DbgRecord  +-->|  DbgRecord  |
 +-------------+    +-------------+   +-------------+

每个指令都有一个指向 DbgMarker 的指针(这将成为可选的),其中包含一个 DbgRecord 对象列表。没有任何调试记录出现在指令列表中。 DbgRecord 有一个指向其所属 DbgMarker 的父指针,每个 DbgMarker 都有一个指向其所属指令的反向指针。

未显示的是从 DbgRecord 到 Value/Metadata 层次结构的其他部分的链接: DbgRecord 子类具有指向其使用的 DIMetadata 的跟踪指针, DbgVariableRecord 具有对存储在 DebugValueUser 基类中的 Value 的引用。这指的是通过 TrackingMetadata 机制引用 ValueValueAsMetadata 对象。

各种调试内联函数(值、声明、赋值、标签)都存储在DbgRecord子类中,使用“RecordKind”字段区分DbgLabelRecordDbgVariableRecord,并且DbgVariableRecord类中的LocationType字段进一步区分它可以表示的各种调试变量内联函数。

如何更新现有代码

任何以某种方式与调试内联函数交互的现有代码都需要更新为以相同方式与调试记录交互。更新代码时请记住以下几个快速规则

  • 在迭代指令时不会看到调试记录;要查找紧接在指令之前的调试记录,您需要迭代Instruction::getDbgRecordRange()

  • 调试记录的接口与调试内联函数的接口相同,这意味着任何在调试内联函数上操作的代码也可以轻松地应用于调试记录。对此的例外情况是InstructionCallInst方法(对调试记录没有逻辑意义),以及isa/cast/dyn_cast方法,它们被DbgRecord类本身上的方法所取代。

  • 调试记录不能出现在也包含调试内联函数的模块中;两者是互斥的。由于调试记录是未来格式,因此在新的代码中应优先正确处理记录。

  • 在不再支持内联函数之前,对于仅处理调试内联函数且更新起来不简单的代码,一个有效的修复方法是使用Module::setIsNewDbgInfoFormat将模块转换为内联函数格式,并在之后转换回来。

    • 这也可以在模块或单个函数的词法范围内使用ScopedDbgInfoFormatSetter类执行。

    void handleModule(Module &M) {
      {
        ScopedDbgInfoFormatSetter FormatSetter(M, false);
        handleModuleWithDebugIntrinsics(M);
      }
      // Module returns to previous debug info format after exiting the above block.
    }
    

以下是关于如何将当前支持调试内联函数的现有代码更新为支持调试记录的粗略指南。

创建调试记录

启用新格式后,DIBuilder类将自动创建调试记录。与指令一样,也可以调用DbgRecord::clone来创建现有记录的未附加副本。

跳过调试记录、忽略Values的调试使用、稳定地计数指令等

所有这些都将透明地发生,无需考虑!

for (Instruction &I : BB) {
  // Old: Skips debug intrinsics
  if (isa<DbgInfoIntrinsic>(&I))
    continue;
  // New: No extra code needed, debug records are skipped by default.
  ...
}

查找调试记录

诸如findDbgUsers之类的实用程序现在有一个可选参数,它将返回引用ValueDbgVariableRecord记录集。您应该能够像对待内联函数一样对待它们。

// Old:
  SmallVector<DbgVariableIntrinsic *> DbgUsers;
  findDbgUsers(DbgUsers, V);
  for (auto *DVI : DbgUsers) {
    if (DVI->getParent() != BB)
      DVI->replaceVariableLocationOp(V, New);
  }
// 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()以获取附加到指令的DbgRecord对象的范围。

for (Instruction &I : BB) {
  // Old: Uses a data member of a debug intrinsic, and then skips to the next
  // instruction.
  if (DbgInfoIntrinsic *DII = dyn_cast<DbgInfoIntrinsic>(&I)) {
    recordDebugLocation(DII->getDebugLoc());
    continue;
  }
  // New: Iterates over the debug records that appear before `I`, and treats
  // them identically to the intrinsic block above.
  // NB: This should always appear at the top of the for-loop, so that we
  // process the debug records preceding `I` before `I` itself.
  for (DbgRecord &DR = I.getDbgRecordRange()) {
    recordDebugLocation(DR.getDebugLoc());
  }
  processInstruction(I);
}

这也可以通过filterDbgVars函数传递,以专门迭代更常用的DbgVariableRecords。

for (Instruction &I : BB) {
  // Old: If `I` is a DbgVariableIntrinsic we record the variable, and apply
  // extra logic if it is an `llvm.dbg.declare`.
  if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
    recordVariable(DVI->getVariable());
    if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
      recordDeclareAddress(DDI->getAddress());
    continue;
  }
  // New: `filterDbgVars` is used to iterate over only DbgVariableRecords.
  for (DbgVariableRecord &DVR = filterDbgVars(I.getDbgRecordRange())) {
    recordVariable(DVR.getVariable());
    // Debug variable records are not cast to subclasses; simply call the
    // appropriate `isDbgX()` check, and use the methods as normal.
    if (DVR.isDbgDeclare())
      recordDeclareAddress(DVR.getAddress());
  }
  // ...
}

处理单个调试记录

在大多数情况下,任何在调试内联函数上操作的代码都可以提取到一个模板函数或自动lambda(如果它还没有在一个函数中),该函数可以应用于调试内联函数和调试记录——但请记住主要例外情况是isa/cast/dyn_cast不适用于DbgVariableRecord类型。

// Old: Function that operates on debug variable intrinsics in a BasicBlock, and
// collects llvm.dbg.declares.
void processDbgInfoInBlock(BasicBlock &BB,
                           SmallVectorImpl<DbgDeclareInst*> &DeclareIntrinsics) {
  for (Instruction &I : BB) {
    if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
      processVariableValue(DebugVariable(DVI), DVI->getValue());
      if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
        Declares.push_back(DDI);
      else if (!isa<Constant>(DVI->getValue()))
        DVI->setKillLocation();
    }
  }
}

// New: Template function is used to deduplicate handling of intrinsics and
// records.
// An overloaded function is also used to handle isa/cast/dyn_cast operations
// for intrinsics and records, since those functions cannot be directly applied
// to DbgRecords.
DbgDeclareInst *DynCastToDeclare(DbgVariableIntrinsic *DVI) {
  return dyn_cast<DbgDeclareInst>(DVI);
}
DbgVariableRecord *DynCastToDeclare(DbgVariableRecord *DVR) {
  return DVR->isDbgDeclare() ? DVR : nullptr;
}

template<typename DbgVarTy, 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::removeFromParentDbgRecord从其标记中分离,然后使用BasicBlock::insertDbgRecordBeforeBasicBlock::insertDbgRecordAfterDbgRecord重新插入到其他位置。您不能将DbgRecord插入到DbgRecord列表中的任意位置(如果您使用llvm.dbg.value执行此操作,则它不太可能是正确的)。

通过调用eraseFromParent来删除DbgRecord

// Old: Move a debug intrinsic to the start of the block, and delete all other intrinsics for the same variable in the block.
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());
}

// New: Perform the same operation, but for a debug record.
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());
}

悬空调试记录怎么办?

如果您有一个如下所示的块

    foo:
      %bar = add i32 %baz...
      dbg.value(metadata i32 %bar,...
      br label %xyzzy

您的优化传递可能希望删除终止符,然后对块执行某些操作。当调试信息保存在指令中时,这很容易做到,但是使用DbgRecord,在删除终止符后,上面块中没有尾随指令可以将变量信息附加到该块中。对于此类退化的块,DbgRecord临时存储在LLVMContext中的映射中,并在将终止符重新插入到块中或在end()处插入其他指令时重新插入。

从技术上讲,这可能导致在优化传递删除终止符然后决定删除整个块的极少情况下出现问题。(我们建议不要这样做)。

其他任何事?

以上指南并未全面涵盖可能应用于调试内联函数的每种模式;如指南开头所述,您可以暂时将目标模块从调试记录转换为内联函数作为权宜之计。可以在调试内联函数上执行的大多数操作都有调试记录的精确等价物,但如果您遇到任何异常,阅读类文档(此处链接)可能会提供一些见解,现有代码库中可能有一些示例,您也可以随时在论坛上寻求帮助。