LLVM bugpoint 工具:设计与使用

描述

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

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

设计理念

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 找出触发 Bug 的优化器 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,请让 bugpoint 使用 -load 选项动态加载您的优化。

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

    $ bugpoint  ... |& tee bugpoint.log
    

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

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

  • bugpoint 可用于主动查找 LLVM 中的 Bug。使用 -find-bugs 选项调用 bugpoint 将导致指定的优化列表被随机化并应用于程序。此过程将重复进行,直到找到 Bug 或用户终止 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”样式调试来报告路标。