编写 LLVM Pass(旧版 PM 版本)

简介 — 什么是 Pass?

警告

本文档涉及旧版 Pass Manager。LLVM 使用新的 Pass Manager 用于优化流水线(代码生成流水线仍然使用旧版 Pass Manager),它有自己的定义 Pass 的方式。更多详细信息,请参阅 编写 LLVM Pass使用新的 Pass Manager

LLVM Pass 框架是 LLVM 系统的重要组成部分,因为 LLVM Pass 是编译器大部分有趣部分所在的地方。Pass 执行构成编译器的转换和优化,它们构建这些转换使用的分析结果,并且最重要的是,它们是编译器代码的结构化技术。

所有 LLVM Pass 都是 Pass 类的子类,它们通过覆盖从 Pass 继承的虚方法来实现功能。根据你的 Pass 的工作方式,你应该从 ModulePassCallGraphSCCPassFunctionPassLoopPass 或者 RegionPass 类继承,这会为系统提供更多关于你的 Pass 做了什么以及如何与其他 Pass 组合的信息。LLVM Pass 框架的主要功能之一是它根据你的 Pass 满足的约束(由它们派生的类指示)以有效的方式安排 Pass 的运行。

Pass 类和要求

在设计新的 Pass 时,你首先要做的事情之一就是决定应该为你的 Pass 选择哪个类作为父类。在这里,我们讨论了可用的类,从最通用到最具体。

在为你的 Pass 选择父类时,你应该选择最具体的类,同时仍然能够满足列出的要求。这为 LLVM Pass 基础设施提供了优化 Pass 运行方式所需的信息,以便生成的编译器不会不必要地变慢。

ImmutablePass

最简单和最普通的 Pass 类型是“ImmutablePass”类。此 Pass 类型用于不需要运行、不更改状态且永远不需要更新的 Pass。这不是正常的转换或分析类型,但可以提供有关当前编译器配置的信息。

虽然此 Pass 类很少使用,但它对于提供有关正在编译的目标机器和其他可能影响各种转换的静态信息非常重要。

ImmutablePass 永远不会使其他转换失效,永远不会失效,并且永远不会“运行”。

ModulePass

ModulePass 类是你可以使用的所有父类中最通用的一个。从 ModulePass 派生表示你的 Pass 使用整个程序作为单元,以不可预测的顺序引用函数体,或添加和删除函数。因为关于 ModulePass 子类的行为一无所知,所以无法对其执行进行优化。

模块 Pass 可以使用函数级 Pass(例如支配者)使用 getAnalysis 接口 getAnalysis<DominatorTree>(llvm::Function *) 提供要检索分析结果的函数,如果函数 Pass 不需要任何模块或不可变 Pass。请注意,这只能对分析运行的函数执行,例如,在支配者的案例中,你应该只请求函数定义的 DominatorTree,而不是声明。

要编写正确的 ModulePass 子类,请从 ModulePass 派生并覆盖具有以下签名的 runOnModule 方法

runOnModule 方法

virtual bool runOnModule(Module &M) = 0;

runOnModule 方法执行 Pass 的有趣工作。如果模块被转换修改,则应返回 true,否则返回 false

CallGraphSCCPass

CallGraphSCCPass 用于需要在调用图上自底向上遍历程序(调用者之前的被调用者)的 Pass。从 CallGraphSCCPass 派生提供了一些构建和遍历 CallGraph 的机制,但也允许系统优化 CallGraphSCCPass 的执行。如果你的 Pass 满足下面列出的要求,并且不满足 FunctionPass 的要求,则你应该从 CallGraphSCCPass 派生。

TODO:简要解释 SCC、Tarjan 算法和 B-U 的含义。

明确地说,CallGraphSCCPass 子类

  1. 不允许检查或修改当前 SCC 以及 SCC 的直接调用者和直接被调用者以外的任何 Function

  2. 要求保留当前的 CallGraph 对象,并更新它以反映对程序所做的任何更改。

  3. 不允许从当前模块添加或删除 SCC,尽管它们可能会更改 SCC 的内容。

  4. 允许从当前模块添加或删除全局变量。

  5. 允许runOnSCC 的调用之间维护状态(包括全局数据)。

在某些情况下,实现 CallGraphSCCPass 有点棘手,因为它必须处理包含多个节点的 SCC。下面描述的所有虚方法都应在修改程序时返回 true,或者在未修改时返回 false

doInitialization(CallGraph &) 方法

virtual bool doInitialization(CallGraph &CG);

doInitialization 方法允许执行大多数 CallGraphSCCPass 不允许的操作。它们可以添加和移除函数,获取函数指针等。 doInitialization 方法旨在执行简单的初始化类型操作,这些操作不依赖于正在处理的 SCC。 doInitialization 方法调用不会与任何其他 Pass 执行重叠调度(因此它应该非常快)。

runOnSCC 方法

virtual bool runOnSCC(CallGraphSCC &SCC) = 0;

runOnSCC 方法执行 Pass 的主要工作,如果模块被转换修改,则应返回 true,否则返回 false

doFinalization(CallGraph &) 方法

virtual bool doFinalization(CallGraph &CG);

doFinalization 方法是一个不常用的方法,在 Pass 框架完成对程序中每个 SCC 调用 runOnSCC 之后会被调用。

FunctionPass

ModulePass 子类相比,FunctionPass 子类具有可预测的局部行为,系统可以预期这种行为。所有 FunctionPass 都独立于程序中的其他函数,对每个函数执行。 FunctionPass 不需要按特定顺序执行,并且 FunctionPass 不会修改外部函数。

明确地说,FunctionPass 子类不允许:

  1. 检查或修改除当前正在处理的 Function 以外的任何函数。

  2. 向当前 Module 中添加或移除 Function

  3. 向当前 Module 中添加或移除全局变量。

  4. runOnFunction 的多次调用之间维护状态(包括全局数据)。

实现 FunctionPass 通常很简单。 FunctionPass 可以重写三个虚方法来完成其工作。所有这些方法如果修改了程序,都应该返回 true,否则返回 false

doInitialization(Module &) 方法

virtual bool doInitialization(Module &M);

doInitialization 方法允许执行大多数 FunctionPass 不允许的操作。它们可以添加和移除函数,获取函数指针等。 doInitialization 方法旨在执行简单的初始化类型操作,这些操作不依赖于正在处理的函数。 doInitialization 方法调用不会与任何其他 Pass 执行重叠调度(因此它应该非常快)。

此方法使用方法的一个很好的例子是 LowerAllocations Pass。此 Pass 将 mallocfree 指令转换为平台相关的 malloc()free() 函数调用。它使用 doInitialization 方法获取对所需 mallocfree 函数的引用,并在必要时向模块添加原型。

runOnFunction 方法

virtual bool runOnFunction(Function &F) = 0;

您的子类必须实现 runOnFunction 方法来执行 Pass 的转换或分析工作。像往常一样,如果函数被修改,则应返回 true

doFinalization(Module &) 方法

virtual bool doFinalization(Module &M);

doFinalization 方法是一个不常用的方法,在 Pass 框架完成对程序中每个函数调用 runOnFunction 之后会被调用。

LoopPass

所有 LoopPass 都独立于函数中的其他循环,对函数中的每个 循环 执行。 LoopPass 按循环嵌套顺序处理循环,最外层循环最后处理。

LoopPass 子类允许使用 LPPassManager 接口更新循环嵌套。实现循环 Pass 通常很简单。 LoopPass 可以重写三个虚方法来完成其工作。所有这些方法如果修改了程序,都应该返回 true,否则返回 false

旨在作为主循环 Pass 管道一部分运行的 LoopPass 子类需要保留其管道中其他循环 Pass 所需的所有相同的函数分析。为了简化操作,LoopUtils.h 提供了一个 getLoopAnalysisUsage 函数。可以在子类的 getAnalysisUsage 重写中调用它以获得一致且正确的行为。类似地,INITIALIZE_PASS_DEPENDENCY(LoopPass) 将初始化此函数分析集。

doInitialization(Loop *, LPPassManager &) 方法

virtual bool doInitialization(Loop *, LPPassManager &LPM);

doInitialization 方法旨在执行简单的初始化类型操作,这些操作不依赖于正在处理的函数。 doInitialization 方法调用不会与任何其他 Pass 执行重叠调度(因此它应该非常快)。应使用 LPPassManager 接口访问 FunctionModule 级别的分析信息。

runOnLoop 方法

virtual bool runOnLoop(Loop *, LPPassManager &LPM) = 0;

您的子类必须实现 runOnLoop 方法来执行 Pass 的转换或分析工作。像往常一样,如果函数被修改,则应返回 true。应使用 LPPassManager 接口更新循环嵌套。

doFinalization() 方法

virtual bool doFinalization();

doFinalization 方法是一个不常用的方法,在 Pass 框架完成对程序中每个循环调用 runOnLoop 之后会被调用。

RegionPass

RegionPass 类似于 LoopPass,但对函数中的每个单入口单出口区域执行。 RegionPass 按嵌套顺序处理区域,最外层区域最后处理。

RegionPass 子类允许通过使用 RGPassManager 接口更新区域树。您可以重写 RegionPass 的三个虚方法来实现自己的区域 Pass。所有这些方法如果修改了程序,都应该返回 true,否则返回 false

doInitialization(Region *, RGPassManager &) 方法

virtual bool doInitialization(Region *, RGPassManager &RGM);

doInitialization 方法旨在执行简单的初始化类型操作,这些操作不依赖于正在处理的函数。 doInitialization 方法调用不会与任何其他 Pass 执行重叠调度(因此它应该非常快)。应使用 RPPassManager 接口访问 FunctionModule 级别的分析信息。

runOnRegion 方法

virtual bool runOnRegion(Region *, RGPassManager &RGM) = 0;

您的子类必须实现 runOnRegion 方法来执行传递的转换或分析工作。像往常一样,如果区域被修改,则应返回真值。应使用 RGPassManager 接口更新区域树。

doFinalization() 方法

virtual bool doFinalization();

doFinalization 方法是一个不常用方法,在传递框架完成对程序中每个区域调用 runOnRegion 后被调用。

MachineFunctionPass

MachineFunctionPass 是 LLVM 代码生成器的一部分,它在程序中每个 LLVM 函数的机器相关表示上执行。

代码生成器传递由 TargetMachine::addPassesToEmitFile 和类似的例程专门注册和初始化,因此通常无法从 optbugpoint 命令运行它们。

MachineFunctionPass 也是一个 FunctionPass,因此适用于 FunctionPass 的所有限制也适用于它。MachineFunctionPass 还有其他限制。特别是,不允许 MachineFunctionPass 执行以下任何操作

  1. 修改或创建任何 LLVM IR InstructionBasicBlockArgumentFunctionGlobalVariableGlobalAliasModule

  2. 修改除当前正在处理的 MachineFunction 之外的任何 MachineFunction

  3. runOnMachineFunction 的调用之间(包括全局数据)维护状态。

runOnMachineFunction(MachineFunction &MF) 方法

virtual bool runOnMachineFunction(MachineFunction &MF) = 0;

runOnMachineFunction 可以被认为是 MachineFunctionPass 的主要入口点;也就是说,您应该重写此方法来完成 MachineFunctionPass 的工作。

runOnMachineFunction 方法在 Module 中的每个 MachineFunction 上调用,以便 MachineFunctionPass 可以对函数的机器相关表示执行优化。如果您想获取正在处理的 MachineFunction 的 LLVM Function,请使用 MachineFunctiongetFunction() 访问器方法——但请记住,您不能从 MachineFunctionPass 修改 LLVM Function 或其内容。

传递注册

传递使用 RegisterPass 模板注册。模板参数是要在命令行上用于指定应将传递添加到程序中的传递的名称。第一个参数是传递的名称,该名称将用于程序的 -help 输出,以及 –debug-pass 选项生成的调试输出。

如果您希望您的传递易于转储,则应实现虚拟打印方法

print 方法

virtual void print(llvm::raw_ostream &O, const Module *M) const;

"分析" 必须实现 print 方法,以便打印分析结果的人类可读版本。这对于调试分析本身以及其他人了解分析的工作原理很有用。使用 opt -analyze 参数调用此方法。

llvm::raw_ostream 参数指定要写入结果的流,Module 参数提供指向已分析程序的顶级模块的指针。但是请注意,在某些情况下(例如从调试器调用 Pass::dump()),此指针可能是 NULL,因此它仅应用于增强调试输出,不应依赖于它。

指定传递之间的交互

PassManager 的主要职责之一是确保传递彼此正确交互。因为 PassManager 试图 优化传递的执行,所以它必须知道传递如何彼此交互以及各个传递之间存在哪些依赖关系。为了跟踪这一点,每个传递都可以声明在当前传递之前需要执行的一组传递,以及当前传递使哪些传递无效。

通常,此功能用于要求在运行传递之前计算分析结果。运行任意转换传递可能会使计算出的分析结果无效,这就是无效集指定的。如果传递没有实现 getAnalysisUsage 方法,则默认为没有任何先决条件传递,并使所有其他传递无效。

getAnalysisUsage 方法

virtual void getAnalysisUsage(AnalysisUsage &Info) const;

通过实现 getAnalysisUsage 方法,可以为您的转换指定必需集和无效集。实现应使用有关哪些传递是必需且未失效的信息填充 AnalysisUsage 对象。为此,传递可以在 AnalysisUsage 对象上调用以下任何方法

AnalysisUsage::addRequired<>AnalysisUsage::addRequiredTransitive<> 方法

如果您的传递需要先执行先前的传递(例如分析),则可以使用其中一种方法来安排在您的传递之前运行它。LLVM 有许多不同类型的分析和传递可以要求,范围从 DominatorSetBreakCriticalEdges。例如,要求 BreakCriticalEdges 可以保证在运行您的传递时 CFG 中不会有关键边。

一些分析链接到其他分析以完成其工作。例如,AliasAnalysis 实现需要 链接 到其他别名分析传递。在分析链接的情况下,应使用 addRequiredTransitive 方法而不是 addRequired 方法。这会通知 PassManager,只要需要传递存在,传递地需要传递也应存在。

AnalysisUsage::addPreserved<> 方法

PassManager 的工作之一是优化分析的运行方式和时间。特别是,它尝试避免重新计算数据,除非需要。出于这个原因,允许传递声明它们保留(即,它们不会使之无效)现有的分析(如果可用)。例如,简单的常量折叠传递不会修改 CFG,因此它不可能影响支配者分析的结果。默认情况下,假设所有传递都会使所有其他传递无效。

AnalysisUsage 类提供了一些在某些情况下与 addPreserved 相关的有用方法。特别是,可以调用 setPreservesAll 方法来指示传递根本不会修改 LLVM 程序(这对于分析是正确的),并且可以由修改程序中指令但不修改 CFG 或终止指令的转换使用 setPreservesCFG 方法。

addPreserved 对于像 BreakCriticalEdges 这样的转换特别有用。此传递知道如何在存在时更新一小组与循环和支配者相关的分析,因此它可以保留它们,尽管它会修改 CFG。

getAnalysisUsage 的示例实现

// This example modifies the program, but does not modify the CFG
void LICM::getAnalysisUsage(AnalysisUsage &AU) const {
  AU.setPreservesCFG();
  AU.addRequired<LoopInfoWrapperPass>();
}

getAnalysis<>getAnalysisIfAvailable<> 方法

Pass::getAnalysis<> 方法会自动被您的类继承,使您可以访问您在 getAnalysisUsage 方法中声明需要使用的 Pass。它接受一个模板参数,指定您想要的 Pass 类,并返回对该 Pass 的引用。例如:

bool LICM::runOnFunction(Function &F) {
  LoopInfo &LI = getAnalysis<LoopInfoWrapperPass>().getLoopInfo();
  //...
}

此方法调用返回对所需 Pass 的引用。如果您尝试获取未在 getAnalysisUsage 实现中声明为必需的分析,则可能会导致运行时断言失败。此方法可以由您的 run* 方法实现或由您的 run* 方法调用的任何其他本地方法调用。

模块级 Pass 可以使用此接口使用函数级分析信息。例如:

bool ModuleLevelPass::runOnModule(Module &M) {
  //...
  DominatorTree &DT = getAnalysis<DominatorTree>(Func);
  //...
}

在上面的示例中,Pass 管理器在返回对所需 Pass 的引用之前,会调用 DominatorTreerunOnFunction

如果您的 Pass 能够在分析存在时更新它们(例如,如上所述的 BreakCriticalEdges),您可以使用 getAnalysisIfAvailable 方法,如果分析处于活动状态,则该方法会返回指向该分析的指针。例如:

if (DominatorSet *DS = getAnalysisIfAvailable<DominatorSet>()) {
  // A DominatorSet is active.  This code will update it.
}

Pass 统计信息

Statistic 类旨在成为一种简单的方法,用于公开来自 Pass 的各种成功指标。在运行结束时,当命令行上启用了 -stats 命令行选项时,会打印这些统计信息。有关详细信息,请参阅程序员手册中的 统计信息部分

PassManager 的作用

PassManager 接收一系列 Pass,确保其 先决条件 设置正确,然后安排 Pass 高效运行。所有运行 Pass 的 LLVM 工具都使用 PassManager 来执行这些 Pass。

PassManager 主要执行两项操作来尝试减少一系列 Pass 的执行时间

  1. **共享分析结果。** PassManager 尝试尽可能避免重新计算分析结果。这意味着跟踪哪些分析已可用,哪些分析已失效以及哪些分析需要为 Pass 运行。一项重要的工作是 PassManager 跟踪所有分析结果的确切生命周期,从而允许它在不再需要时 释放分配给保存分析结果的内存

  2. **对程序流水线执行 Pass。** PassManager 尝试通过将 Pass 连接在一起进行流水线处理,从一系列 Pass 中获得更好的缓存和内存使用行为。这意味着,给定一系列连续的 FunctionPass,它将对第一个函数执行所有 FunctionPass,然后对第二个函数执行所有 FunctionPasses,依此类推……直到整个程序都通过这些 Pass 运行。

    这提高了编译器的缓存行为,因为它一次只接触 LLVM 程序表示的单个函数,而不是遍历整个程序。它减少了编译器的内存消耗,例如,一次只需要计算一个 DominatorSet。

PassManager 的有效性直接受到它拥有多少关于正在安排的 Pass 行为的信息的影响。例如,在未实现的 getAnalysisUsage 方法面前,“保留”集有意地保持保守。不实现它应该实现的部分将导致不允许任何分析结果跨越您的 Pass 的执行。

PassManager 类公开了一个 --debug-pass 命令行选项,该选项对于调试 Pass 执行、查看工作原理以及诊断何时应该保留比当前更多的分析很有用。(要获取有关 --debug-pass 选项的所有变体的信息,只需键入“llc -help-hidden”)。

例如,通过使用 –debug-pass=Structure 选项,我们可以查看默认优化流水线,例如(输出已修剪):

$ llc -mtriple=arm64-- -O3 -debug-pass=Structure file.ll > /dev/null
(...)
ModulePass Manager
Pre-ISel Intrinsic Lowering
FunctionPass Manager
  Expand large div/rem
  Expand large fp convert
  Expand Atomic instructions
SVE intrinsics optimizations
  FunctionPass Manager
    Dominator Tree Construction
FunctionPass Manager
  Simplify the CFG
  Dominator Tree Construction
  Natural Loop Information
  Canonicalize natural loops
(...)

releaseMemory 方法

virtual void releaseMemory();

PassManager 自动确定何时计算分析结果以及保留多长时间。由于 Pass 对象本身的生命周期实际上是整个编译过程的持续时间,因此我们需要一些方法在分析结果不再有用时释放它们。 releaseMemory 虚拟方法就是执行此操作的方法。

如果您正在编写一个分析或任何其他保留大量状态的 Pass(供另一个“需要”您的 Pass 并使用 getAnalysis 方法的 Pass 使用),则应实现 releaseMemory 以释放分配给维护此内部状态的内存。此方法在类的 run* 方法之后、Pass 中下一次调用 run* 之前调用。

注册动态加载的 Pass

使用 LLVM 构建生产质量工具时,大小很重要,这既是为了分发的目的,也是为了在目标系统上运行时控制驻留代码大小。因此,需要有选择地使用一些 Pass,同时省略其他 Pass 并保持以后更改配置的灵活性。您希望能够做到所有这些,并向用户提供反馈。这就是 Pass 注册发挥作用的地方。

Pass 注册的基本机制是 MachinePassRegistry 类和 MachinePassRegistryNode 的子类。

MachinePassRegistry 的实例用于维护 MachinePassRegistryNode 对象的列表。此实例维护列表并将添加和删除传达给命令行界面。

MachinePassRegistryNode 子类的实例用于维护关于特定 Pass 提供的信息。此信息包括命令行名称、命令帮助字符串以及用于创建 Pass 实例的函数的地址。这些实例之一的全局静态构造函数会注册到相应的 MachinePassRegistry,静态析构函数会取消注册。因此,静态链接到工具中的 Pass 将在启动时注册。动态加载的 Pass 将在加载时注册,在卸载时取消注册。

使用现有的注册表

存在预定义的注册表来跟踪指令调度 (RegisterScheduler) 和寄存器分配 (RegisterRegAlloc) 机器 Pass。在这里,我们将描述如何注册寄存器分配器机器 Pass。

实现您的寄存器分配器机器 Pass。在您的寄存器分配器 .cpp 文件中添加以下包含:

#include "llvm/CodeGen/RegAllocRegistry.h"

同样在您的寄存器分配器 .cpp 文件中,以以下形式定义创建器函数:

FunctionPass *createMyRegisterAllocator() {
  return new MyRegisterAllocator();
}

请注意,此函数的签名应与 RegisterRegAlloc::FunctionPassCtor 的类型匹配。在同一文件中添加“安装”声明,格式如下:

static RegisterRegAlloc myRegAlloc("myregalloc",
                                   "my register allocator help string",
                                   createMyRegisterAllocator);

请注意,帮助字符串之前的两个空格会在 -help 查询中产生整齐的结果。

$ llc -help
  ...
  -regalloc                    - Register allocator to use (default=linearscan)
    =linearscan                -   linear scan register allocator
    =local                     -   local register allocator
    =simple                    -   simple register allocator
    =myregalloc                -   my register allocator help string
  ...

就是这样。用户现在可以自由使用 -regalloc=myregalloc 作为选项。注册指令调度程序类似,只是使用 RegisterScheduler 类。请注意,RegisterScheduler::FunctionPassCtorRegisterRegAlloc::FunctionPassCtor 显著不同。

要强制将您的寄存器分配器加载/链接到 llc/lli 工具中,请将创建器函数的全局声明添加到 Passes.h 中,并将“伪”调用行添加到 llvm/Codegen/LinkAllCodegenComponents.h 中。

创建新的注册表

最简单的入门方法是克隆现有的注册表之一;我们建议使用 llvm/CodeGen/RegAllocRegistry.h。需要修改的关键内容是类名和 FunctionPassCtor 类型。

然后您需要声明注册表。例如:如果您的 Pass 注册表是 RegisterMyPasses,则定义:

MachinePassRegistry<RegisterMyPasses::FunctionPassCtor> RegisterMyPasses::Registry;

最后,声明 Pass 的命令行选项。例如:

cl::opt<RegisterMyPasses::FunctionPassCtor, false,
        RegisterPassParser<RegisterMyPasses> >
MyPassOpt("mypass",
          cl::init(&createDefaultMyPass),
          cl::desc("my pass option help"));

这里命令选项是“mypass”,createDefaultMyPass 作为默认创建器。

使用 GDB 调试动态加载的 Pass

不幸的是,使用 GDB 调试动态加载的 Pass 并不像它应该的那样容易。首先,您无法在尚未加载的共享对象中设置断点,其次,共享对象中内联函数存在问题。以下是一些使用 GDB 调试 Pass 的建议。

为了便于讨论,我将假设您正在调试由 opt 调用的转换,尽管此处描述的内容不依赖于此。

在 Pass 中设置断点

首先,你需要在 opt 进程上启动 gdb。

$ gdb opt
GNU gdb 5.0
Copyright 2000 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "sparc-sun-solaris2.6"...
(gdb)

请注意,opt 包含大量调试信息,因此加载需要一些时间。请耐心等待。由于我们还无法在 Pass 中设置断点(共享对象直到运行时才会加载),因此我们必须执行该进程,并在其调用我们的 Pass 之前、加载共享对象之后使其停止。最可靠的方法是在 PassManager::run 中设置断点,然后使用所需的参数运行进程。

$ (gdb) break llvm::PassManager::run
Breakpoint 1 at 0x2413bc: file Pass.cpp, line 70.
(gdb) run test.bc -load $(LLVMTOP)/llvm/Debug+Asserts/lib/[libname].so -[passoption]
Starting program: opt test.bc -load $(LLVMTOP)/llvm/Debug+Asserts/lib/[libname].so -[passoption]
Breakpoint 1, PassManager::run (this=0xffbef174, M=@0x70b298) at Pass.cpp:70
70      bool PassManager::run(Module &M) { return PM->run(M); }
(gdb)

一旦 optPassManager::run 方法中停止,你就可以在 Pass 中设置断点,以便跟踪执行过程或执行其他标准调试操作。

其他问题

掌握了基础知识后,GDB 会遇到一些问题,有些有解决方法,有些则没有。

  • 内联函数具有错误的栈信息。通常,GDB 在获取栈跟踪和单步执行内联函数方面做得非常好。但是,当 Pass 被动态加载时,它以某种方式完全失去了这种能力。我所知道的唯一解决方法是取消内联函数(将其从类的主体移动到 .cpp 文件中)。

  • 重新启动程序会破坏断点。按照上述信息操作后,你成功地在 Pass 中设置了一些断点。接下来,你重新启动程序(即,再次键入“run”),并开始收到有关断点无法设置的错误。我发现的唯一“解决”此问题的方法是删除 Pass 中已设置的断点,运行程序,并在 PassManager::run 中停止执行后重新设置断点。

希望这些技巧能帮助解决常见的调试情况。如果你想贡献一些自己的技巧,请联系 Chris