Clang架构
(待更新)
Clang文件架构
下面是clang源文件库库名,进行逐个分析从而理解clang源码架构(从文件分布方面)。
1 | docs |
功能参考“Clang” CFE Internals Manual(Clang C Front-End内部手册)。
Basic文件夹
这个库无疑需要一个更好的文件名。这个”基本”库包含了许多低级(low-level)的应用程序,用于追踪和操纵 源缓冲区、源缓冲区的位置、”诊断”系统、记号、目标抽象(提取)、以及正在编译的语言的子集信息。
该基础架构的一部分是特定于C(TargetInfo类),其他部分可以用于非C的语言(SourceLocation,SourceManager、Diagnostics,FileManager)。以后如果有需求可以从Basic中将其分离出来构建新库。
Driver文件夹
Clang 驱动程序通过命令行交互为Clang编译器和工具提供了访问通道,并且兼容gcc的驱动程序。尽管驱动程序被Clang项目所驱动,但实际它从逻辑上来看是一个独立的工具,与Clang有着共同的目标。下面是它的特征:
- GCC兼容性:
也就是clang driver的操作和gcc driver的操作具有兼容性,用户在使用过程中可以很好的从gcc转向clang。 - 灵活性(Flexible):
clang驱动程序被设计成灵活的以及易适应的,这样可以很好顺应Clang和LLVM的发展。并且,大多数驱动程序函数被保存在库中,这样可以通过修改该库来构建其他想要实现工具或者接收类似GCC的接口的工具。 - 低开销:
在实际过程中,我们发现gcc driver编译许多小文件时产生了一笔小但是有意义的开销。但是在clang driver和编译过程相比并不需要做太多事,我们在保持效率的同时还遵循一些简单的法则:- 尽可能避免内存分配和字符串拷贝
- 参数只进行一次解析
- 提供了一些简单接口用来有效的寻找参数
- 简单:
最后,考虑到其他用途,驱动程序需要尽可能设计地简单一点。值得注意的是,尝试去完全兼容gcc driver会增加大量的复杂性,这与简单的设计里面不符。我们试图通过将一个任务分解为多个阶段,而不是单个整体的任务来减轻driver设计的复杂性。
内部介绍
上图是Driver结构下重要的组件以及组件之间的联系。其中黄色框代表着driver构建的具体的数据结构;绿色框是操纵这些数据结构的不同阶段,可以理解为一个函数;而蓝色是重要的工具类。因此通过绿色框,可以将Driver架构细分为五个不同的阶段:
Parse阶段(解析参数处理):选项解析阶段
命令行字符串被解析为参数(Arg类的实例)。驱动程序会去接收每一个参数。每个参数对应一个抽象Option类的定义,该定义描述了通过其他的元数据,这些参数如何被解析。Arg实例时轻量级的,仅仅包含足够的信息给用户去确定选项类别以及它们的值(可能有多个)。
例如命令行选项中”-lfoo”和”-l foo”,将会解析为两个Arg实例,分别为”JoinedArg”以及”SeprateArg”,但是这两个Arg实例都指向相同的Option。
Option是延迟创建的(Lazily created),在driver运行过程中需要才去创建它。这样可以避免了加载驱动程序时避免创建所有的类,而是创建需要的类。大多数驱动程序代码只需要通过选项唯一的ID(例如clang driver中options::OPT_I)来处理选项。
Arg实例中实际上不存放参数的值,因为这样会导致创建不必要的字符串副本。取而代之的实现是Arg实例始终签到在ArgList这种数据结构中,该数据结构包含每个参数的参数字符串(向量),每个Arg实例只需要一个索引即可从向量中找到字符串。
clang driver可以通过”-###”打印出Parse阶段的结果,如下:1
2
3
4
5
6
7clang -### -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"} 在这个阶段后,命令行被分成了定义好的option对象,并且有适当的参数(也就是获取到了有用的选项数据)。随后阶段将基本不会在对字符串进行处理。
Pipeline阶段(流水线处理):编译行为(Actions)的构建
一旦参数被解析后,就需要构建编译序列(Compilation Sequence)所需的子流程工作的树结构。这个结构涉及到确定输入文件以及类型,以及要在这些文件上做的工作(预处理、编译、汇编、链接等),并且构建一个Action实例的列表给每个任务。这个阶段的结果是有一个或者多个高层(Top-level)的actions,每个action通常对应于单个输出(例如,一个目标文件或者需要链接的文件)。
大多数Actions对应于实际的任务,但是有两个特殊的Actions,第一个是InputAction,只是简单用于改造输入参数,结果作为其他Actions的输入。第二个是BindArchAction,一个用于将某个 Action(动作)的目标架构绑定到特定架构的类,从概念上将被用到的所有输入Actions修改其架构。 clang driver可以打印输出Pipeline阶段的结果,通过-ccc-print-phases,下面代码中driver构建了七个不同的actions,四个用于编译”t.c”文件为二进制文件,两个用于处理”t.s”输入(汇编器),一个用于将它们链接起来
1
2
3
4
5
6
7
8
9clang -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 下面再举一个不同的Pipeline阶段:在这个例子中有两个顶层(Top Level)Actions去编译输入文件为两个独立的目标文件,每个目标文件都是由lipo去构建的,用来合并为两个独立架构构建的结果。
在这个阶段完成后,编译过程被划分为了一组简单的Actions,需要执行它们来产生中间(例如:-fsyntax-only选项,不会有最终输出)或者最终输出。这些阶段实际上就是所说的编译步骤,例如”预处理”、”编译”、”汇编”、”链接”等。这样也说明了”编译”过程的输入与输出,输入为预处理后的c/c++文件,输出为汇编代码。
Bind阶段(连接阶段):选择合适的工具(库)给Actions
这个阶段(配合上下一个阶段)将Pipeline中的Actions工作树变成了一系列实际的子进程去跑。从概念上说,驱动程序driver执行自顶向下的匹配,从而将Actions分配给Tools(工具)。工具链(ToolChain)负责选择特定的工具来执行指定的操作。一旦选择后,驱动程序与工具进行交互,来查看它是否能用于处理Actions(例如clang、gcc中都集成了预处理器工具)。
一旦所有Actions都选择了工具,驱动程序会确定actions如何和工具进行”连接”(例如:使用进程内模块(inproccess module),管道pipe,临时文件或者用户提供的文件名)。并且如果需要输出文件,驱动程序也需要去计算合适的文件名(后缀和文件位置取决于输入文件以及类似于-save-temps这样的选项)。 驱动程序与工具链交互,从而执行工具绑定(Bind)。每个 ToolChain 中都包含了特定架构、平台和操作系统所需的全部工具和相关信息。在一次编译期间,单个驱动程序的调用可能查询多个工具链,从而用于不同体系结构工具交互。
此阶段的结果并没有直接计算,但是driver可以通过-ccc-print-bindings选项打印这个阶段的结果,例如:下面代码展示了命令行这个编译序列对应的工具链、工具、输入、输出(每个Actions)。具体的,Clang在这里被用于在i386体系上编译t0.c,而Darwin工具用于汇编和链接过程。gcc工具对应于PowerPC相关Actions的处理过程。
1
2
3
4
5
6
7
8clang -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阶段():
一旦选择了某个工具来执行特定的Action,该工具必须构建具体的命令(Commands),用于编译期间被执行。翻译阶段的主要工作是将GCC样式的命令行选项翻译成子进程需要的选项。
某些工具,例如汇编器,只和少量参数交互(对于汇编器来说,它只需要知道输入的汇编代码文件的路径和输出的机器码文件的路径,以及一些必要的选项参数)。不需要了解其他的上下文信息。其他的,比如编译器和链接器可能需要的参数更多一点。
因此ArgList类提供了许多简单的方法来帮助翻译参数。例如,仅传递与某些选项相对应的最后一个参数,或者一个选项的所有参数。
这个阶段的结果是要执行的命令的列表(可执行路径以及参数字符串)。
Execute阶段():
最后,编译流水线被执行。
Frontend文件夹
Frontend库包含了可用于Clang库顶层构建工具的一些功能。例如:输出诊断的多种方法。
Compiler Invocation
编译器调用,这是Frontend库中提供的类之一,该类中包含描述Clang -cc1前端当前调用的信息。这个信息主要来自于Clang driver通过命令行构建,或者从客户端执行自定义初始化。这个数据结构被分成逻辑单元,给编译器不同部分使用,例如PreprocessorOptions,LanguageOptions,CodeGenOptions。
CompilerInvocation是将用户指定的编译选项转化为Clang编译器内部状态,为CompilerInstance的初始化做好准备。
下面是源码中相关实现:
1 | class CompilerInvocationBase { |
CompilerInstance
是Clang编译器的实例化对象,它代表了一个完成的编译器实例,包含了所有的编译器组件和状态,负责管理整个编译流程。其中包括对输入源代码的解析、词法分析、语法分析、语义分析、代码生成等过程。在Clang中,CompilerInstance的创建和初始化由FrontendAction和CompilerInvocation完成,CompilerInstance的核心作用是将输入的源代码转化为目标代码,为后续的链接、调试、优化等工作做准备。
Clang中相关类
Type类以及它的子类
Type类和它的子类是AST中重要的一部分。数据类型可以通过ASTContext类被访问到,ASTContext类会在程序需要使用的时候隐式地创建它们。
Type类有着一系列不明显的特征:
- 它们不会获取类型限定符,类似于const或者volatile(可参考QualType)
- 它们隐式获取typedef的信息。一旦创建后,Type不可改变,这一点不像(Decls)
FrontendAction类
ASTFrontendAction
CodeGenAction
ASTMergeAction
DumpCompilerOptionsAction
InitOnlyAction
PreprocessorFrontendAction
PrintDependencyDirectivesSourceMinimizerAction
PrintPreambleAction
ReadPCHAndPreprocessAction
WrapperFrontendAction
Clang中经常混淆的点
可参考文章clang是编译器前端吗
Clang前端、clang cc1、clang driver、clang编译器、clang应用程序
clang前端:这个说法相对于整个编译器,也就是clang+llvm编译器而言,clang的功能是接收源文件并且产生LLVM IR文件给LLVM使用,因此有了clang前端的说法。
clang编译器:这个说法是因为在实际使用的过程中,可以直接利用clang应用程序将类c文件生成为可执行文件,clang会自动调用llvm相关工具,因此也称clang应用程序为clang编译器。
clang应用程序:就是build/bin目录下的clang程序,可以像gcc一样直接将源文件编译为可执行文件。
clang cc1:cc1为命令行参数,它可以表示真正的clang前端,也就是可以将编译器运行到前端某一步从而停止,精准调控前端。同时也可以通过”-emit-obj”选项去“隐式”调用llc(不体现在命令行中),从而实现为clang编译器。(也有人说clang应用程序通过-cc1从而以编译器的身份执行编译任务)
clang driver:clang驱动程序(实际上就是clang应用程序),用来调用编译器运行过程中需要的工具,自己不会编译源码。在前端调用cc1来将源文件生成为llvmIR文件,在后端调用llc将LLVMIR文件生成目标文件。不仅如此,还通过调用系统的链接器链接目标文件成可执行文件。例如:./clang test.c -###
1 | "/home/linguoxiong/summer-ospp/improve-compiler/llvm_install/bin/clang-12" "- |