前言:进阶篇目的在于剖分Clang和LLVM的基本原理,因此依照常规编译器的架构从前端(clang)到后端(LLVM);因此,进阶篇被依次分成两个部分:前端和后端;当前章节主要是LLVM前端的分析,这里使用Clang;如果需要未来会增加其他前端的实现分析,如Julia等;这一章节会比较偏重前端的理论研究,我们依照传统编译器分为以下几个部分:词法分析、语法分析、语义分析、IR代码生成
1 概述:
Clang并不包含在llvm原始程序中,需要专门下载源码,见之前的llvm安装;clang的源码在:tools/clang之下。功能:
- GCC相兼容
- 灵活的
- 低开销
- 简单
Clang是由一系列的库构成
2 Clang的执行过程
Clang是LLVM的C/C++前端,从原理上他会产生用于后端的IR指令。但实际上Clang会有两种执行方式:
- 以Driver的方式执行
- 作为cc1前端方式运行
在大多数情况下,以驱动程序方式运行Clang会自动调用相关后端程序,并生成可执行文件,这也是之所以Clang虽然只是前端,却可以直接产生目标代码的原因,我们可以使用”-###”观察Clang的执行过程;
在驱动模式下,clang实质只是一个调度管理程序,其即完成以下的工作:
- 为clang补齐各参数,如库函数的地址等,然后通过系统调用执行clang -cc1
- 执行其他必要的外部进程,如link;
注意:Driver方式的clang只是一个“调度程序”,他不会执行有关前端需要的程序。其只是补齐参数,并很“偷懒的”调用系统函数,并以clang -cc1执行真正的前端操作和之后的link操作。如下:
如上的简单示例,我们在对一个程序执行clang时,除其执行了带-cc1参数的Clang外,还会执行系统的ld命令。因此,我们简单跟踪一下Clang的执行过程。我们使用GDB快速跟踪一下Clang的执行过程(这也是之前为什么要生成Debug版本的LLVM的原因)。其执行结果如下:
因为执行过程耗时过长,我们在这里就不详细描述跟踪过程。只是将初步的跟踪结果做个叙述。
- Clang执行初期是作为driver执行的,因此,程序的入口是:tools/driver/driver.cpp;
- 如果程序第一个参数为-cc1则直接执行函数”ExecuteCC1Tool”此时为cc1前端模式,直接执行cc1_main或cc1as_main;执行完毕后程序退出
- 如果不是-cc1,则进行相关命令解释,生成相容的命令行
- 通过Driver建立与GCC相容的编译过程,并由TheDriver.ExecuteCompilation执行该相容的
- 错误讯息输出
注意因为clang两种工作模式下,驱动模式实际是在补足参数后再通过-cc1的方式执行,因此,我们只讨论驱动模式。
2.1 Driver方式运行
关键类:Driver
下图显示了驱动程序体系的重要组件及他们之间的相互关系。橙色组件表示了由驱动程序构建的具体数据结构,绿色组件表示了在概念上不同阶段操作这些数据结构,蓝色表示重要的有帮助的类:
在概念上,驱动模式依照上图分成5个阶段:
- Parse:Option Parsing
命令行参数被分解为各参数(Arg实例);每个参数恰好对应一个抽象的选项定义,他描述了通过附加Metadata如何解析参数。参数示例本身是轻量、仅包含了客户端确定他们对应选项和数值(如果有附加参数)的足够信息。
例如,命令行类似“-lfoo -l foo”会被解析为两个参数实例(连接在一起参数和分离参数实例),但是每个都对应到同一个选项。
为了避免在加载驱动时填充所有的Option类,Options被慵懒的创建。多数驱动程序代码仅需要处理他们自己唯一ID的Options(options::OPT_I)
Arg实例本身不存储参数值,在很多情况下,这只会导致创建没有必要的字符串参数副本。相反,Arg实例总是被嵌入到ArgList结构中,这里包含原始的参数字串向量。每个Arg他自己仅仅需要包含指向该向量的索引而不是直接保存他们。
Clang驱动可以转储这一阶段的通过”-###”标志(必须在任何命令之前)----- 注意,但我自己实测之后没有Option输出
在这个阶段完成后,命令行将被分解为具有适当参数定义好的选项。接下来阶段很少需要字串处理
- Pipeline:编译动作构造(Compilation Action Construction)
在完成了参数解析后,子进程作业树需要确认构造编译序列。这包含确认输入文件及其类型、对他们进行什么样的工作(预处理、编译、汇编、链接等)并为每个人物构造动作实例链表。这样的结构是一个或更多的顶层动作列表,每个通常对应一个单一的输出(例如,对象或链接的可执行文件)
多数的动作(Actions)对应一个实际的任务,但是这里有两个特殊的任务(Actions),第一个是InputActions,他只是简单将输入参数匹配到另一个Actions的输入。第二个是BindArchAction,从概念上给所有使用输入的动作替换架构
Clang驱动可以使用命令“-ccc-print-phases”转印这一阶段的结果
- Bind:Tool & Filename Selection
这一阶段(连同翻译阶段)转换Action树为实际运行的子进程列表。从概念上,驱动器由顶向下将Action分配给工具。工具链负责选择特定动作的工具,一旦选择了,驱动与工具进行交互确认匹配的其他操作(如,通过整合一个预处理)
一旦所有的操作(action)选择了工具,驱动程序确定如何连接这些工具(例如:使用一个Inprocess模块、管道、临时文件或用户私有文件名)。如果需要一个输出文件,驱动器可以产生一个适当的文件名(文件后缀和文件位置依赖于输入类型和类似-save-temps的选项)
驱动与工具链互动去执行工具绑定(The driver interacts with a ToolChain to perform the Tool bindings)。每个工具链包含特定架构、平台和操作系统等编译需要的所有信息;一个单一的工具在编译期间需要提取很多工具链,为了与不同体系架构的工具进行交互。
这样,这一阶段不会直接计算,但是驱动器可以使用”-ccc-print-bindings”参数打印这一结果,如下:
这显示了已经绑定到编译序列的工具链,工具,输入和输出;在本例中可以看到是使用gnu的linker工具
- Translate:Tool Specific Argument Translation
当工具被选择来执行特定的动作,工具必须为之后运行的编译过程构造具体的命令。主要的工作是翻译gcc格式的命令到子进程所期望的格式。
某些工具,类似汇编器,只与少数的参数和确定的执行路径及传入他们的输入和输出参数进行交互。其他的类似编译、链接也许需要翻译大量的额外参数。
ArgList提供了一些简单的有用的方法协助参数的翻译,例如:仅传递与某个选项想对应最后的参数或全部的参数
这一阶段的结果是一个命令列表被执行(可执行路径和参数字串)