在2018年编写LLVM遍——部分IV(完)

原文地址:https://medium.com/@mshockwave/writing-llvm-pass-in-2018-part-iv-d69dac57171d

作者:Bekket McClane

在StackOverflow上最常问到的LLVM问题之一是:我写了一个Hello World遍,我怎么使用clang而不是opt运行它?

最常用的解决方案之一是通过-Xclang -load -Xclang MyPass.so命令行选项,使用(旧式的)PassManager扩展点。不过,这使得我好奇:

我能够只向clang传递一个命令行选项来运行我的遍或定制特性?

当然,这要求在LLVM源代码树做一些修改。但我相信这是学习clang内部以及它与LLVM交互的一个好方式。因此对你来说下面是一个简单但仍然有趣的教程。让我们开始吧!

  • 目标

我们准备启用ExtraProteinPass,一个通过向clang给出命令行选项-add-extra-protein,会修改你代码里所有循环的行程计数的遍。

这个选项有几个变种:

  1. -add-extra-protein=2x。所有循环的行程计数加倍。
  2. -add-extra-protein=5g。仅向所有循环增加额外5次行程计数。
  3. -add-extra-protein1lb。向所有循环增加额外454次行程计数。因为一磅=453.59克。

默认的,-add-extra-protein等于-add-extra-protein=2x。

  • 代码

本文中LLVM/Clang源代码树所有的修改可以在这里找到:

我不准备在这里讨论遍的细节。这个遍应该放在lib/Transforms/Scalar/ExtraProtein.cpp以及include/llvm/Transforms/Scalar/ExtraProtein.h。使用头文件里的createExtraProteininLegacyPass (uint32_t, uint32_t)来构建一个新的遍实例。

  • Clang内部:驱动、前端及CodeGen概览

轻量级回合:clang是一个“编译器“吗?

严格地说,答案是否。

一个典型的编译器在进入真正的编译过程之前,需要许多自展(bootstrapping,即词法分析器、语法解析器……)。例如,查找默认/系统头文件路径。现代“编译器”,如gcc及clang,通常把这种简单任务分流到另外单独的实例,称为编译器驱动,或简称驱动。因此你运行的可执行clang,实际上是一个驱动,在设置好所有的要求后,它将调用“真正的编译器”。

在目录lib/Driver/ToolChains中(相对于clang项目的根),我们可以看到各种编译器驱动。例如,Fuchsia OS的开发者在Fuchsia.cpp与Fuchsia.h里创建了自己的驱动,它可以获取Fuchsia OS里正确的头文件路径以及设置缺省标记等。严格地说,在这个目录下的文件不仅是驱动,还是描述了编译流水线里其他部分的工具链,比如它要使用的汇编器与链接器。

“真正编译器”的开端是前端,这是我们从课本知道的:词法分析器与语法解析器。在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里的前端最终构造一棵AST(抽象语法树)时,它需要生成相应的LLVM IR代码。这个阶段称为CodeGen,这可能与LLVM里CodeGen混淆,后者从LLVM IR生成本地代码。

    • Clang里的CodeGen:AST -> LLVM IR
    • LLVM里的CodeGen:LLVM IR -> 本地代码
  • 步骤一. 为驱动添加新命令行选项

Clang与LLVM不仅以生成高质量代码而著称,还因为它们有极好的框架。在这个情形里,为驱动添加新命令行选项仅需要你不超过5行代码。

用于驱动的常用命令行选项定义在一个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<…>,是描述该标记的。例如,Joined<[“-“, “-“], “add-extra-protein=”>显示了用于命令行中时的格式。下半部定义了别名规则。这样在你传递没有任何值的-add-extra-protein时,它将仍然向选项extra_protein_EQ提供一个缺省值。

现在,你可以对clang使用-add-extra-protein选项——但当然没有任何事发生。后面我们将定义它相关的活动。在这之前,我们首先准备向前端添加新选项。

  • 步骤二. 为前端添加新命令行选项

如之前看到的,驱动负责自展过程,它将把驱动选项“展开”为另一组选项,这些选项将被传递给前端。因为驱动与前端是两个不同的实例,基本上它们有不同的选项集。

前端的选项也定义在一个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">;

  • 步骤三. 连接驱动与前端

现在,我们准备将驱动的命令行选项转换为前端的命令行选项。我们要修改“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行,我们使用前端的命令行选项向下传递我们的信息。

  • 步骤四. 添加新CodeGen选项

最后,我们来到最后的阶段:CodeGen。虽然clang里的CodeGen不是一个单独的实例或可执行对象,它有自己的选项集,这些放在inc/clang/Frontend/CodeGenOptions.h的CodeGenOptions类中。我们准备为它添加一个简单的成员:

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

Duplicate域保存2x,3x类型的protein数量,Amend域以“克”为单位保存protein数量。

接着,我们准备使用从前端传来的命令行选项配置ExtraProteinAmount CodeGen选项。我们准备修改ParseCodeGenArgs函数,它在lib/Frontend/CompilerInovation.cpp里,汇集了大多数CodeGen选项。将下面行放在函数的任一处。

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;

  }

}

这是我们最终把我们的protein数量的文本表示转换为内存内值的地方。

  • 步骤五(最后). 添加LLVM

在最后一步,我们准备向由clang的CodeGen执行的遍流水线添加我们的ExtraProteinPass。我们要修改的东西放在lib/CodeGen/BackendUtil.cpp里。EmitAssemblyHelper::CreatePasses方法,如其名字所示,创建将在CodeGen后运行的LLVM遍。我们将把添加ExtraProteinPass的代码放在这里。

if(!CodeGenOpts.ExtraProteinAmount.empty()) {

  MPM.add(createExtraProteinLegacyPass(CodeGenOpts.ExtraProteinAmount.Duplicate,

                                       CodeGenOpts.ExtraProteinAmount.Amend));

}

代码本身相当简单,这次我们把代码放在EmitAssemblyHelper::CreatePasses的最后,因为我们需要先运行两个优化遍:SROA与Mem2Reg。这两个遍可以使我们的ExtraProteinPass有更精简的代码形式。

不过,如果没有给出额外的优化标记,默认地clang以优化级别0(即-O0)运行。在-O0,SROA与Mem2Reg不会加入遍流水线。如果即使在-O0中,我们也想使用clang命令行选项来启用我们的特性,我们还需要向遍流水线加入SROA与Mem2Reg:

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。该属性将防止在附属的函数上运行任何优化遍。因此,如果有任何“额外的protein”,我们需要告诉clang不要添加这个属性。我们准备修改的函数是lib/CodeGen/CodeGenModule.cpp里的CodeGenModule:: SetLLVMFunctionAttributesForDefinition。通过增加一个新的保护语句,修改ShouldAddOptNone变量相关的代码行,这个变量用于控制optnone生成过程:

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

本教程提供了一个令人愉悦但仍然详尽的方式来观察Clang的内部结构。正如你看到的,本文里的代码并不难,大多数是自解释的。希望这会激发你对Clang/LLVM神奇世界的好奇心?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值