自己动手写LLVM Pass 四

在stackoverflow上有关LLVM的最常见问题之一是:我写了一个Hello World Pass,如何使用clang来运行它,而不是opt?

最常见的解决方案之一是单独使用(传统)PassManager扩展点和-Xclang -load -Xclang MyPass.so命令行选项。

然而,我想知道:

我可以通过简单地将一个命令行选项传递给clang来运行我的Pass或自定义功能吗?

当然,这需要对LLVM源代码树进行一些更改。但我相信这将是学习clang内部以及它如何与LLVM交互的好方法。所以这里有一个简单但又有趣的教程。让我们开始吧!

目标

我们将启用ExtraProteinPass,通过clang的一个命令行选项-add-extra-protein,它将修改代码中所有循环的行程计数。

该选项有几种变体:

  • -add-extra-protein=2x 所有循环的行程计数加倍。
  • -add-extra-protein=5g 仅为所有循环添加额外的5次迭代。
  • -add-extra-protein=1lb 为所有循环添加额外的454次迭代。因为一磅=453.59克

默认情况下,-add-extra-protein等于-add-extra-protein=2x

源代码

可以在此处找到本教程中对LLVM / Clang源代码树的所有修改:

LLVM Pass

我不会在这里详细介绍Pass。Pass将被放入lib/Transforms/Scalar/ExtraProtein.cppinclude/llvm/Transforms/Scalar/ExtraProtein.h 。我们将使用createExtraProteinLegacyPass(uint32_t, uint32_t)头文件中的factory函数稍后构造一个新的Pass实例。

Clang 内部

在进入真正的编译过程之前,典型的编译器需要大量的前期工作(即词法分析器,解析器……)。例如,查找默认/系统标头路径。现代的“编译器”,例如gcc和clang,经常将这种琐碎的任务卸载到另一个分离的实例中,称为编译器驱动程序,或简称为驱动程序。所以你运行的可执行文件clang实际上是一个驱动程序,它会在设置完所有需求后调用“真正的编译器”。

在文件夹中lib/Driver/ToolChains(相对于clang的项目根目录),我们可以看到各种编译器驱动程序。例如,从开发商Fuchsia OS创建自己的驱动程序Fuchsia.cppFuchsia.h,它可以正确设置Fuchsia OS标题路径和设置默认标志等等。严格地说,该文件夹中的文件不仅仅是驱动程序,而是工具链,它们还描述了编译管道中的其他部分,比如它将使用的汇编器和链接器。

“真正的编译器”的开头是前端,这是我们从教科书中学到的:lexer和parser。在clang中,前端也称为cc1。有时你在网上发现的一些神奇的解决方案告诉你运行如下命令:

clang -cc1 -fsome_flag -some_option ...
#或者
clang -Xclang -fsome_flag -Xclang -some_option

这相当于将标志或选项直接传递给前端。
您还可以通过添加-v选项查看驱动程序传递给前端的选项:

$ clang++ -v -c hello.cc
...
"/path/to/clang" -cc1 -triple x86_64-apple-macosx10.13.0 -Wdeprecated-objc-isa-usage -Werror=deprecated-objc-isa-usage -emit-obj -mrelax-all -disable-free -disable-llvm-verifier -discard-value-names ... -o hello.o -x c++ ./hello.cc

正如您在上面看到的那样,最初我们只提供选项-c hello.cc。但是驱动程序添加了许多其他选项,如-cc1后面所显示的,他们将传递给clang前端。

当clang中的前端最终构造AST(抽象语法树)时,它需要生成相应的LLVM IR代码。此阶段称为CodeGen,可能与LLVM中的CodeGen混淆,后者从LLVM IR生成本机代码。

  • en in clang:AST - > LLVM IR
  • LLVM中的CodeGen:LLVM IR - >本机代码

步骤1.为驱动程序添加新的命令行选项

Clang和LLVM不仅因其生成的代码质量而闻名,而且还因其出色的框架而闻名。在这种情况下,为驱动程序添加新的命令行选项只需要少于五行

驱动程序的常用命令行选项在TableGen文件中定义:include/clang/Driver/Options.td。(如果您不熟悉TableGen,那没关系,因为这里使用的语法非常简单,您可能在几分钟内自己解决)在文件中的某处找到并添加以下行:

def extra_protein_EQ : Joined<["-", "--"], "add-extra-protein=">, Flags<[DriverOption]>,
HelpText<"Add extra protein for all loops.">;

def extra_protein : Flag<["-", "--"], "add-extra-protein">, Flags<[DriverOption]>,
Alias<extra_protein_EQ>, AliasArgs<["2x"]>,
HelpText<"Add 2x extra protein for all loops.">;

第一行的extra_protein_EQ是标记变量,冒号之后的列(如Joined<…>, Flags<…>, HelpText<…> )是用来对Flag的描述。例如Joined<[“-”, “ — “], “add-extra-protein=”>
说明该选型在命令行中使用的格式,下面的部分定义了别名规则。
这样,当您没有给-add-extra-protein传递任何值时,它仍然会为该extra_protein_EQ选项提供默认值。

现在你可以在clang中使用-add-extra-protein选项。但当然没有任何事情会发生。我们稍后将定义其相关操作。在此之前,我们将首先为前端添加新选项。

步骤2.为前端添加新的命令行选项

如前所述,驱动程序负责引导过程,它会将驱动程序选项“扩展”为另一组选项,然后将其传递给前端。由于驱动程序和前端是两个不同的实例,基本上,它们有不同的选项集。

前端的选项也在TableGen文件中定义:include/clang/Driver/CC1Options.td。将以下行添加到此文件的任何位置,(例如 let Group = Action\_Group in{…} )。

def extra_protein_amount : Joined<["-"], "extra-protein-amount=">,
HelpText<"Amount of extra protein want to add for all loops">;

步骤3.连接驱动程序和前端

现在我们要将驱动程序的命令行选项转换为前端的选项。我们要修改“clang”驱动程序。在档案中lib/Driver/ToolChains/Clang.cpp 。我们将以下行添加到Clang::ConstructJob方法中:

if(const Arg *A = Args.getLastArg(options::OPT_extra_protein_EQ)) {
StringRef Val = A->getValue();
uint32_t NumAmount = 0;
Val.consumeInteger(10, NumAmount);
if(Val == "lb") {
// Turn 'pound' to 'gram'
NumAmount *= 454; // 1 pound == 453.59 gram
Val = "g";
}

// Create command line options for frontend
SmallString<8> ProteinAmount;
ProteinAmount.assign(std::to_string(NumAmount));
ProteinAmount.append(Val);
CmdArgs.push_back(
Args.MakeArgString(Twine("-extra-protein-amount=") + ProteinAmount)
);
}

基本上我们什么都不做,只能将“磅”换成“克”。然后在第15行到第17行中,我们使用前端的命令行选项来传递我们的信息。

步骤4.添加新的CodeGen选项

我们终于到了最后阶段:CodeGen。虽然clang中的CodeGen不是单个实例或可执行文件,但它有自己的选项集,它放在CodeGenOptions类中include/clang/Frontend/CodeGenOptions.h 。我们将为它添加一个简单的成员字段:

struct ProteinAmount {
uint32_t Duplicate;
uint32_t Amend;
ProteinAmount() : Duplicate(0U), Amend(0U) {}
inline bool empty() const {
return !Duplicate && !Amend;
}
};
ProteinAmount ExtraProteinAmount;

Duplicate字段存储2x3x种蛋白质的量,并修正字段存储那些使用“克”作为蛋白质单元。

接下来,我们ExtraProteinAmount将使用从前端传递的命令行选项配置CodeGen选项。我们将修改该ParseCodeGenArgs函数,该函数用于填充大部分CodeGen选项lib/Frontend/CompilerInvocation.cpp 。将以下代码放在函数中的任何位置。

 for(const auto& Arg : Args.getAllArgValues(OPT_extra_protein_amount)) {
StringRef Val(Arg);
if(Val.endswith("x")) {
// Duplicate
uint32_t Num = 0;
Val.consumeInteger(10, Num);
Opts.ExtraProteinAmount.Duplicate += Num;
} else {
// Amend
uint32_t Num = 0;
Val.consumeInteger(10, Num);
Opts.ExtraProteinAmount.Amend += Num;
}
}

这是我们最终将蛋白质量的文本表示转换为记忆内值的地方。

第5步 添加LLVM Pass

在最后一步,我们将把我们添加ExtraProteinPass到由clang CodeGen运行的Pass管道。我们将触及的东西就是lib/CodeGen/BackendUtil.cppEmitAssemblyHelper::CreatePasses方法,顾名思义,创建将在CodeGen之后运行的LLVM Pass。我们将在此添加我们的代码ExtraProteinPass

if(!CodeGenOpts.ExtraProteinAmount.empty()) {
MPM.add(createExtraProteinLegacyPass(CodeGenOpts.ExtraProteinAmount.Duplicate,
CodeGenOpts.ExtraProteinAmount.Amend));
}

代码本身非常简单,这次我们在EmitAssemblyHelper::CreatePasses最后一行添加代码,因为我们需要两个优化Passes来运行:SROA和Mem2Reg。这两个可以为我们提供更简洁的代码形状供我们ExtraProteinPass处理。

但是,如果没有给出额外的优化标志,则clang在优化级别零(即-O0)中运行。并且SROA和Mem2Reg不会添加到Pass管道中。如果我们只想使用一个clang命令行选项来启用我们的功能,我们需要将SROA和Mem2Reg添加到Pass管道中,即使在-O0

 if(!CodeGenOpts.ExtraProteinAmount.empty()) {
// We need mem2reg and sroa for better code shape
// these two would be added by default when OptLevel >= 1
// so make sure they're added even when OptLevel == 0
if(PMBuilder.OptLevel < 1) {
FPM.add(createSROAPass());
MPM.add(createPromoteMemoryToRegisterPass());
}
MPM.add(createExtraProteinLegacyPass(CodeGenOpts.ExtraProteinAmount.Duplicate,CodeGenOpts.ExtraProteinAmount.Amend));
}

另外,在-O0,clang会为所有函数添加一个 optnone属性。该属性将阻止任何优化Passes在附加函数上运行。因此,如果有任何“额外的蛋白质”,我们还需要告诉clang不要添加这个属性。我们要调用的函数是CodeGenModule::SetLLVMFunctionAttributesForDefinitionlib/CodeGen/CodeGenModule.cpp。通过添加新的guard语句来修改与ShouldAddOptNone变量相关的行,用于控制optnone生成过程:

...
ShouldAddOptNone &= !D->hasAttr<AlwaysInlineAttr>();
ShouldAddOptNone &= CodeGenOpts.ExtraProteinAmount.empty();

本教程提供了一种有趣而又彻底的方式来查看Clang的内部结构。正如您所看到的,本文中的代码并不难,大多数都是自我解释的。