LLVM bugpoint 工具:设计与用法

描述

bugpoint 缩小了 LLVM 工具和 Pass 中问题根源的范围。它可以用来调试三种类型的错误:优化器崩溃、优化器导致的错误编译或错误的原生代码生成(包括静态和 JIT 编译器中的问题)。它的目标是将大型测试用例缩减为小型、有用的用例。例如,如果 opt 在优化文件时崩溃,它将识别导致崩溃的优化(或优化组合),并将文件缩减为触发崩溃的小型示例。

有关详细的案例场景,例如调试 opt 或其中一个 LLVM 代码生成器,请参阅 如何提交 LLVM 错误报告

设计理念

bugpoint 旨在成为一个无需任何 LLVM 基础设施挂钩即可使用的实用工具。它适用于任何和所有 LLVM Pass 和代码生成器,并且不需要“了解”它们的工作原理。因此,它可能看起来会做一些愚蠢的事情或错过明显的简化。 bugpoint 还旨在在编译器调试过程中用程序员时间换取计算机时间;因此,缩减测试用例可能需要很长时间(无人值守),但我们认为它仍然值得。请注意,除非调试错误编译(每个程序测试都需要执行程序并且需要很长时间),否则 bugpoint 通常非常快。

自动调试器选择

bugpoint 读取命令行上指定的每个 .bc.ll 文件,并将它们链接到一个称为测试程序的单个模块中。如果命令行上指定了任何 LLVM Pass,它将在测试程序上运行这些 Pass。如果任何 Pass 崩溃,或者它们产生格式错误的输出(导致验证器中止),bugpoint 将启动 崩溃调试器

否则,如果未指定 -output 选项,bugpoint 将使用“安全”后端(假设生成良好的代码)运行测试程序以生成参考输出。一旦 bugpoint 拥有测试程序的参考输出,它就会尝试使用选定的代码生成器执行它。如果选定的代码生成器崩溃,bugpoint 将在代码生成器上启动 崩溃调试器。否则,如果生成的输出与参考输出不同,则假设差异是由代码生成器错误导致的,并启动 代码生成器调试器

最后,如果选定的代码生成器的输出与参考输出匹配,bugpoint 将在应用所有 LLVM Pass 后运行测试程序。如果其输出与参考输出不同,则假设差异是由其中一个 LLVM Pass 中的错误导致的,并进入 错误编译调试器。否则,没有 bugpoint 可以调试的问题。

崩溃调试器

如果优化器或代码生成器崩溃,bugpoint 将尽其所能缩减 Pass 列表(对于优化器崩溃)和测试程序的大小。首先,bugpoint 找出触发错误的优化器 Pass 组合。例如,在调试 opt 暴露的问题时,这很有用,因为它运行超过 38 个 Pass。

接下来,bugpoint 尝试从测试程序中删除函数以减小其大小。通常,在调试过程内优化时,它能够将测试程序缩减为单个函数。一旦函数数量减少,它就会尝试删除控制流图中的各种边,以尽可能减小函数的大小。最后,bugpoint 删除任何其缺失不会消除错误的 LLVM 指令。最后,bugpoint 应该会告诉您哪些 Pass 崩溃,提供一个 bitcode 文件,并提供有关如何使用 optllc 重现错误的说明。

代码生成器调试器

代码生成器调试器尝试缩小选定代码生成器错误编译的代码量。为此,它获取测试程序并将其划分为两部分:一部分使用“安全”后端(编译成共享对象)进行编译,另一部分使用 JIT 或静态 LLC 编译器运行。它使用多种技术来减少通过 LLVM 代码生成器推送的代码量,以减少问题的潜在范围。完成后,它会发出两个 bitcode 文件(分别称为“test”(将使用代码生成器编译)和“safe”(将使用“安全”后端编译)),以及用于重现问题的说明。代码生成器调试器假设“安全”后端会生成良好的代码。

错误编译调试器

错误编译调试器的工作方式类似于代码生成器调试器。它的工作原理是将测试程序拆分为两部分,对一部分运行指定的优化,将这两部分重新链接在一起,然后执行结果。它试图将 Pass 列表缩小到导致错误编译的一个(或几个)Pass,然后缩减正在被错误编译的测试程序的部分。错误编译调试器假设选定的代码生成器工作正常。

使用 bugpoint 的建议

bugpoint 可以是一个非常有用的工具,但它有时会以非明显的方式工作。以下是一些提示和技巧

  • 在代码生成器和错误编译调试器中,bugpoint 仅适用于具有确定性输出的程序。因此,如果程序输出 argv[0]、日期、时间或任何其他“随机”数据,bugpoint 可能会错误地将这些数据在输出时的差异解释为错误编译的结果。程序应暂时修改为禁用可能在每次运行时发生变化的输出。

  • 崩溃调试器 中,bugpoint 不会区分缩减过程中的不同崩溃。因此,如果发生新的崩溃或错误编译,bugpoint 将继续处理新的崩溃。如果您希望坚持特定的崩溃,则应编写检查脚本以验证错误消息,请参阅 bugpoint - 自动测试用例缩减工具 中的 -compile-command

  • 在代码生成器和错误编译调试器中,如果您手动修改程序或其输入以减少运行时间,但仍然存在问题,则调试速度会更快。

  • bugpoint 在处理新的优化时非常有用:它有助于快速跟踪回归。但是,为了避免每次更改优化时都必须重新链接 bugpoint,请使用 -load 选项让 bugpoint 动态加载您的优化。

  • bugpoint 可能会生成大量输出并运行很长时间。将程序的输出捕获到文件中通常很有用。例如,在 C shell 中,您可以运行

    $ bugpoint  ... |& tee bugpoint.log
    

    以在文件 bugpoint.log 中以及您的终端上获取 bugpoint 输出的副本。

  • bugpoint 无法调试 LLVM 链接器的问题。如果 bugpoint 在您看到其“All input ok”消息之前崩溃,您可以尝试在同一组输入文件上使用 llvm-link -v。如果这也崩溃,则您可能遇到链接器错误。

  • bugpoint 可用于主动查找 LLVM 中的错误。使用 -find-bugs 选项调用 bugpoint 将导致指定的优化列表随机化并应用于程序。此过程将重复进行,直到找到错误或用户终止 bugpoint

  • bugpoint 生成的 IR 可能包含长名称。在 IR 上运行 opt -passes=metarenamer 以使用易于阅读的元语法名称重命名所有内容。或者,运行 opt -passes=strip,instnamer 以使用非常短(通常纯数字)的名称重命名所有内容。

当 bugpoint 不够用时该怎么办

有时,bugpoint 不够用。特别是,InstCombine 和 TargetLowering 都有基于访问者的结构化代码,其中包含许多潜在的转换。如果使用 bugpoint 后仍然有太多代码难以理解,并且问题似乎出现在 instcombine 中,则以下步骤可能会有所帮助。这些相同的技巧也适用于 TargetLowering。

开启 -debug-only=instcombine 并查看 instcombine 中哪些转换正在触发,方法是选择包含“IC”的行。

此时,您需要做出决定。转换的数量是否足够少,可以使用调试器单步执行?如果是,那就尝试一下。

如果转换太多,则修改源代码的方法可能会有所帮助。在这种方法中,您可以修改 instcombine 的源代码以仅禁用对测试输入执行的转换,并在转换集中执行二分查找。一组修改位置是 InstCombiner 的“visit*”方法(例如 visitICmpInst),通过在方法的第一行添加“return false”。

如果这仍然无法删除足够的转换,则更改 InstCombiner::DoOneIteration 的调用者 InstCombiner::runOnFunction 以限制迭代次数。

您现在可能还会发现使用“-stats”很有用,以查看 instcombine 的哪些部分正在触发。这可以指导您在哪里放置额外的报告代码。

此时,如果转换数量仍然过大,则插入代码以限制是否执行 visit 函数体内的代码可能会有所帮助。添加一个静态计数器,在每次调用该函数时递增。然后添加代码,在所需的范围内简单地返回 false。例如

static int calledCount = 0;
calledCount++;
LLVM_DEBUG(if (calledCount < 212) return false);
LLVM_DEBUG(if (calledCount > 217) return false);
LLVM_DEBUG(if (calledCount == 213) return false);
LLVM_DEBUG(if (calledCount == 214) return false);
LLVM_DEBUG(if (calledCount == 215) return false);
LLVM_DEBUG(if (calledCount == 216) return false);
LLVM_DEBUG(dbgs() << "visitXOR calledCount: " << calledCount << "\n");
LLVM_DEBUG(dbgs() << "I: "; I->dump());

可以添加到 visitXOR 中,以限制 visitXor 仅应用于第 212 和 217 次调用。这来自一个实际的测试用例,并提出了一个重要点——简单的二分查找可能不够,因为相互作用的转换可能需要隔离多个调用。在 TargetLowering 中,使用 return SDNode(); 代替 return false;

现在转换数量已减少到可管理的数量,请尝试检查输出以查看是否可以找出正在执行哪些转换。如果可以确定,则执行通常的调试操作。如果哪个代码对应于正在执行的转换不明显,请在基于调用计数的禁用后设置断点并单步执行代码。或者,您可以使用“printf”风格的调试来报告途径点。