原文地址: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,会修改你代码里所有循环的行程计数的遍。
这个选项有几个变种:
- -add-extra-protein=2x。所有循环的行程计数加倍。
- -add-extra-protein=5g。仅向所有循环增加额外5次行程计数。
- -add-extra-protein1lb。向所有循环增加额外454次行程计数。因为一磅=453.59克。
默认的,-add-extra-protein等于-add-extra-protein=2x。
- 代码
本文中LLVM/Clang源代码树所有的修改可以在这里找到:
-
- LLVM树(extra-protein分支):https://github.com/mshockwave/llvm/tree/extra-protein
- Clang树(也在extra-protein分支):https://github.com/mshockwave/clang/tree/extra-protein
- LLVM遍
我不准备在这里讨论遍的细节。这个遍应该放在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神奇世界的好奇心?