什么是Driver
在单独编译文件时,我们经常会使用到如下的一些命令
1 | clang -ccc-print-phases main.m |
这里我们使用的’clang’其实不是我们通常所说的llvm的编译前端clang,而是一个命令行工具,指的就是Clang Driver,它是一个驱动,是面向用户提供接口,内部解析参数,调用编译的中的工具完成编译的过程。
Driver怎么设计
关于这点,clang官方文档有介绍,我这里做个总结
特点和目标
与GCC的兼容性
官方的解释是与GCC的兼容可以让用户在他们的工程中快速使用clang,我理解隐含的意思是为了和GCC抢占用户,这是必须的。
灵活性
这个很好理解,和写代码一样,随着clang和LLVM的发展,足够的灵活性保证了新功能的扩展。
开销小
和编译工作相比,Driver的工作量并不大,但是也要遵循一些基本的规则来保持它的高效。
- 尽可能避免内存分配和字符串拷贝
- 不要多次解析参数
- 提供一些简单的接口来有效地搜索参数
简单
尽管为了兼容GCC给Driver带来了很多复杂的逻辑,但是整个Driver的设计还是尽可能简单。为了达到这个目的,将Driver分为多个独立的阶段而不是一个大的任务。
设计细节
这是一张来自官网的图,介绍了Driver内部设计的几个阶段。其中,橙色的代表数据,绿色代表操作这些数据的阶段,蓝色的代表辅助组件。
Input Strings
这条不用多解释,就是我们调用clang的时候后面传入的标示和数据
Parse: Option Parsing
这一步就是将我们传入的String类型输入,解析成具体的参数对象,方便后面使用和传递。关于这点,和其他一些命令行工具其实是差不多的,比如python-fire 和 CLAide。具体解析细节我们后面再介绍,如果像看大概的解析过程,使用
-###
就可以。1
2
3
4
5
6
7$ clang -### -Xarch_i386 -fomit-frame-pointer -Wa,-fast -Ifoo -I foo t.c
Option 0 - Name: "-Xarch_", Values: {"i386", "-fomit-frame-pointer"}
Option 1 - Name: "-Wa,", Values: {"-fast"}
Option 2 - Name: "-I", Values: {"foo"}
Option 3 - Name: "-I", Values: {"foo"}
Option 4 - Name: "<input>", Values: {"t.c"}Pipeline: Compilation Action Construction
一旦解析了参数,就会构造出后续编译所需要的子任务。这涉及到确定输入文件及其类型,要对它们做哪些工作(预处理、编译、组装、链接等),以及为每个任务构造一个Action实例列表。其结果是一个由一个或多个顶层Action组成的列表,每个Action通常对应一个单一的输出(例如,一个对象或链接的可执行文件)。
使用
-ccc-print-phases
可以打印出这个阶段的内容:1
2
3
4
5
6
7
8$ clang -ccc-print-phases -x c t.c -x assembler t.s
0: input, "t.c", c
1: preprocessor, {0}, cpp-output
2: compiler, {1}, assembler
3: assembler, {2}, object
4: input, "t.s", assembler
5: assembler, {4}, object
6: linker, {3, 5}, image当这个阶段完成之后,编译过程将会以被分成多个小的Action,每个Action的阶段就是我们熟知的”预处理→编译→汇编→连接”等。
Bind: Tool & Filename Selection
这个阶段和后面的Trasnlate一起将将Actions转化成真正的进程。Driver自上而下匹配,将Actioins分配给分配给Tools,ToolChain负责为每个Action选择合适的Tool,一旦选择了Tool,Driver就会与Tool交互,看它是否能够匹配更多的Action。
一旦所有的Action都选择了Tool,Driver就会决定如何连接工具(例如,使用进程内模块、管道、临时文件或用户提供的文件名)。
Driver驱动程序与ToolChain交互,以执行Tool的绑定。ToolChain包含了特定架构、平台和操作系统编译所需的所有工具的信息,一次编译过程中,单个Driver调用可能会查询多个ToolChain,以便与不同架构的工具进行交互。
可以通过
-ccc-print-bindings
可以查看Bind的大致情况,以下展示了在i386和ppc上编译t0.c文件Bing过程。1
2
3
4
5
6
7
8$ clang -ccc-print-bindings -arch i386 -arch ppc t0.c
# "i386-apple-darwin9" - "clang", inputs: ["t0.c"], output: "/tmp/cc-Sn4RKF.s"
# "i386-apple-darwin9" - "darwin::Assemble", inputs: ["/tmp/cc-Sn4RKF.s"], output: "/tmp/cc-gvSnbS.o"
# "i386-apple-darwin9" - "darwin::Link", inputs: ["/tmp/cc-gvSnbS.o"], output: "/tmp/cc-jgHQxi.out"
# "ppc-apple-darwin9" - "gcc::Compile", inputs: ["t0.c"], output: "/tmp/cc-Q0bTox.s"
# "ppc-apple-darwin9" - "gcc::Assemble", inputs: ["/tmp/cc-Q0bTox.s"], output: "/tmp/cc-WCdicw.o"
# "ppc-apple-darwin9" - "gcc::Link", inputs: ["/tmp/cc-WCdicw.o"], output: "/tmp/cc-HHBEBh.out"
# "i386-apple-darwin9" - "darwin::Lipo", inputs: ["/tmp/cc-jgHQxi.out", "/tmp/cc-HHBEBh.out"], output: "a.out"Translate: Tool Specific Argument Translation
一旦选择了一个Tool来执行一个特定的Action,该Tool必须构建具体的Commands,并在编译过程中执行。该阶段主要的工作是将gcc风格的命令行选项翻译成子进程所期望的任何选项。
这个阶段的结果是一些列将要执行Commands(包含执行路径和参数字符)。
Execute
执行阶段,之前上一阶段输出的命令,并且产出结果。
以上关于Driver的设计都可以在LLVM的官网能找到,可能很多介绍比较抽象,下面我介绍下更多的代码相关的细节。
Driver的代码分析
入口
在clang/tools/driver/driver.cpp
我们可以找到Driver的入口,其中入口逻辑都集中在main之中。
1 | int main(int argc_, const char **argv_) { |
可以看到,driver.cpp
中大致做了几件事情:
- 做些环境的检测和各自对象数据的准备
- 解析参数中target,mode信息
- 初始化
Driver
对象,并对其赋值target,mode等等需要的数据 - 通过
Driver
对象生成Compilation
对象并且执行编译命令
BuildCompilation&ExecuteCompilation
上面driver.cpp
中很重要的一步是BuildCompilation
,这一步做了很多事情,我们看下代码。
1 | Compilation *Driver::BuildCompilation(ArrayRef<const char *> ArgList) { |
通过省略干扰我们理解的其他代码,可以发现,BuildCompilation
主要做了以下几件事情:
- 解析参数
- 翻译参数
- 构造Actions和Jobs
基本和官方设计所提到的几个流程能对应上,同时我们也能看到之前提到的ccc-print-phases
是在这里处理Actions的打印。
而ExecuteCompilation
的则比较简单,就是我们之前设计图中最后的执行步骤。同时也能看到为什么我们输入-###
能够打印出Jobs的信息。
1 | int Driver::ExecuteCompilation( |
Parse
我们根据上面BuildCompilation
中的步骤,来看看Parse这一步是怎么实现的。
1 | InputArgList Driver::ParseArgStrings(ArrayRef<const char *> ArgStrings, |
上面有些代码只是简单封装,主要流程就是从参数String中逐个解析生成Arg
对象,最后组成InputArgList
。Opt.accept
逻辑我们在这里就不说了,其实就是根据不同类型的参数解析然后返回,我们重点说下Option
,因为Opt.accept
处理的核心需要依赖Option
对象。
1 | Option(const OptTable::Info *Info, const OptTable *Owner); |
Option
的生成需要OptTable::Info
和OptTable
两个参数,通过跟踪代码,我们可以在Driver
的构造函数中找到OptTable
的生成。
1 | std::unique_ptr<OptTable> clang::driver::createDriverOptTable() { |
可以看到,OptTable
的生成是依靠预先定义好的InfoTable[]
来生成的。这里以宏的形式定了一个Option应该包含哪些属性,同时引入了Options.inc
。
1 |
|
我这里截取了几个定义,都是我们比较熟悉和常见的,我们支持的每个命令都能在这里找到定义。
Actions
因为BuildUniversalActions
最终会调用BuildActions
,所以我们来看看BuildActions
的处理逻辑。
1 | void Driver::BuildActions(Compilation &C, DerivedArgList &Args, |
可以看到,不同的参数决定了我们这一次过程的最后阶段不一样,比如如果我们参数列表里带了-fsyntax-only
,那我们最后久之后走到编译这一步,后面的汇编,链接等阶段是不会涉及的。
Bind
在执行编译的过程中,Bind会为不同的Action选定不同的Tool。
1 | InputInfo Driver::BuildJobsForActionNoCache( |
可以看到,Bind会尝试去绑定不同的Tool,BackCompile/Backend/Compile/Preprocessor等。
Jobs
有了前面的参数,Actions,ToolChain等,Job的构建就水到渠成,具体代码可以在下面找到。
1 | void Driver::BuildJobs(Compilation &C) const { |
Execute
执行的逻辑其实就是在Jobs准备之后,根据相关信息执行相关的命令。
1 | int Driver::ExecuteCompilation( |
我们也可以通过-ftime-report
来查看执行的过程和时间。
小结
虽然Driver这一层还没有涉及到真正的编译,但是作为整个编译过程的驱动,了解Driver对我们后续了解编译的全过程还是有帮助的,特别是Driver的设计,以pipline形式将各个环节的输入输出抽象,解决了参数解析和翻译,任务封装和执行等过程,以满足我们各种各样的使用需求。