LLVM编译器流程
LLVM的编译器主体和gcc一样,分为前端和后端。当然,现在流行的说法将基于IR中间表示层的优化流程称为“中端”,和前端后端并列。在编译器中,前端负责将高级语言编译为中间表示层形态(如LLVM IR),后端负责将中间表示层再编译为目标相关的机器码。LLVM编译器突出的特性就是其中间表示层LLVM IR兼容LLVM目标列表中的所有架构,也即LLVM的IR和基于IR的优化流程是公共的。所以,LLVM编译器的流程可表示为图1-2:

图1-2 LLVM编译流程中的前后端
除了图1-2中的X86、ARM、Sparc,LLVM还支持其他十多种目标架构。LLVM所支持的完整架构列表,可以在脚本llvm/CMakeLists.txt中查看,包括:
set(LLVM_ALL_TARGETS
AArch64 AMDGPU ARM AVR BPF Hexagon Lanai
Mips MSP430 NVPTX PowerPC RISCV Sparc
SystemZ VE WebAssembly X86 XCore
)
LLVM的单一IR表示形态能够在兼容这么多结构和指令集各异的硬件架构,主要得益于其后发信息优势,同时也得益于LLVM主语言C++的强大表达能力和这些年硬件算力的长足进步。
在项目架构和代码分布上,LLVM的后端实现和IR优化都实现在LLVM的核心项目中,位于代码树顶层目录llvm。LLVM的后端流程被实现为庞大的公共代码部分,各个硬件架构只需实现自己区别于其他架构的的特有部分,位于llvm/lib/Target/下的各个架构目录中。至于前端,由于各高级语言的目标应用领域分布广泛,导致它们之间语法语义差异较大,所以各个高级语言的前端往往呈现为基于LLVM的独立代码项目。Fortran和Go语言都有自己的独立前端项目;C、C++、Objective C由于书出同门,共性大于差异,所以后来在苹果公司的主导下,它们被整合在clang前端中兼容实现。此外,并行编程模型CUDA和OpenCL也在clang中作了实现。
1.1.1. 编译器流程与中间形态
上图对LLVM的编译流程是一个粗略的描述。事实上,代码程序在LLVM流程中从高级语言文本到最后的可执行形态,中间还要经历除IR以外的一些其他形态。以最终生成ELF可执行文件为例:在前端阶段,代码经历了词素流Token Stream、抽象语法树AST形态,再变换为LLVM IR代码形态;到了后端阶段,代码还要经历SelectionDAG指令选择图、MachineInstr机器代码、MC代码和ELF Relocatable可重定位文件这些形态。后端输出的ELF可重定位文件Relocatable(后缀为.o),不满足执行条件,需经过链接生成ELF EXE/DSO才可运行或被动态链接。代码编译经历的这些中间形态和流程如图1-3所示。

图1-3 LLVM编译、链接、加载流程
LLVM核心代码和基于LLVM开发的前端clang、链接器lld等子项目的核心代码,要么在定义图中某一层的形态,要么就在描述图中相邻形态间变换的算法和接口。这里的变换,可以是从一个形态变换到另一个形态,比如从LLVM IR构建SelectionDAG,比如从高级语言源码解析出Token Stream词素流;也可以是在某个形态内部的变换,比如opt工具以LLVM IR同时作为输入和输出,但输出的IR是经过某种优化的,和输入IR已有不同,再比如后端llc工具以MachineInstr形态同时作为输入和输出实现了很多优化Pass。读者在分析LLVM代码时,可以时常留意当前所分析的代码模块是在描述形态本身,还是变换算法?
接下来本节对图中各个代码形态作初步介绍,后续各章节将依次展开介绍中间表示层IR、前端clang、后端llc、链接器lld这些代码形态和代码变换流程。读者在了解各层形态的表示方式,尤其是相邻两层的表示方式时,可以考虑这样两个问题:
-
每相邻两层的变换,是否有信息量损失?是否可逆?
-
相邻两层相比,各自的优劣势是什么?是形式上对人类更友好?对机器执行加载更友好?还是更利于某种优化的开展?更有利于为必须经历的某种变换(例如目标指令选择、二进制发射)服务?
1.1.1.1. 输入源码形态
clang编译器的最初输入,是软件工程师的直接设计产物或是某些代码生成器的输出产物,其形态是文本源代码。目前,clang编译器支持C系列的C、C++、ObjectiveC三种语言。源码形态对人类友好可读,但对机器执行却不够直接。这也是编译器和解释器这类工具存在的桥梁意义——将人类设计输出的代码形态变换为便于机器执行的ELF二进制等代码形态。
clang所支持的几个编程语言,其标准文档都有公开版本。C语言的标准可在https://www.iso.org/standard/82075.html下载,C++的历代标准发布在https://isocpp.org/std/the-standard。
clang支持的这几个C系列语言本身也一直处于发展中,即每隔数年会基于原有语言标准增量定义新的语法和功能特性。尤其是C++,每三到四年就有新版本推出。截止到clang 19版本,其对C++历代标准的支持情况,是能够完全支持C++ 17及之前的各个版本,但对C++ 20、23和2c这些最新版本的支持状态是部分(Partial)支持。具体到对C++个版本中各语法特性的支持,clang官网作了介绍:
https://clang.llvm.org/cxx_status.html
clang源码树中,对输入源代码和输入文件管理的功能,分布在clang/lib/Basic目录下的FileEntry.cpp、FileManager.cpp、SourceManager.cpp等源文件,以及clang/include/clang/Basic/目录下的FileEntry.h、FileManager.h、SourceManager.h等头文件。
接下来几个小节对编译中间代码形态的介绍,以一个简单的C函数源程序iop.c为例:
$ cat build/iop.c
int my_iop(int a, int b) {
if (a) {
return a+b;
} else {
*(int*)0x100 = b;
return 1;
}
}
1.1.1.2. 词素流Token Stream形态
clang前端获得文本形态的源码输入后,就对其进行预处理和词法分析,将输入源代码变换为词素流(Token Stream)。clang进行预处理和词法处理的依据,是C等语言标准中对预处理指示符(Preprocessing Directives)、关键字(Keywords)、标点符号(Punctuator)和标识符(Identifier)生成式的定义。
在clang中,用cc1工具的-dump-tokens选项可以看到源码被词法分析切割后的结果,如图1-4所示。可以看到,词法分析不仅将源码切分为一个个的词素,还记录了各个词素的StartOfLine(是否位于一行的开头)、LeadingSpace(词素前是否有空格)和Loc(在源码中的行列位置)等属性。

图1-4 C代码词法分析切割后的结果
clang对预处理和词法分析的实现,位于clang/lib/Lex源文件目录,以及clang/include/clang/Lex头文件目录。
1.1.1.3. 抽象语法树AST形态
clang以词法分析输出的词素流Token Stream为输入,继续进行前端语法分析和语义分析,从而生成代码的抽象语法树(Abstract Syntax Tree,AST)形态。AST是gcc和clang编译器前端的核心形态,它是从一维的源码字符流立体化出的的树状程序结构,可视作高级语言源码和中间表示层间的桥梁。AST的顶层元素是各个全局符号,包括全局变量和函数的声明定义。函数在AST中会被按照它原先的各个语句块、语句、表达式进行层次化和细粒度的切分。还是以iop.c为例,在clang中用cc1工具的-ast-view选项加上GraphViz工具可以对源程序的AST进行可视化,如图1-5所示。

图1-5 C代码语法分析生成的AST树结构
事实上,工程师在进行程序设计编码之前,对程序的构思本就是以这样一种树状结构来进行的,只是落到源码文件上借助字符流形式来表达而已。因此,AST可视作高级语言设计的本原模型之一。当然,我们的程序构思在落到AST树状形态之前,还可能经历了面向过程或面向对象这样的建模过程。
笔者接触过一些采取完全自研编译器软件的设计团队,他们的架构就有采取直接用C++或Python来直接构建类似AST中循环、跳转、顺次执行这样的程序结构的方式,也就是跳过了AST之前的文本源码形态。
clang中对本形态的实现以及语法语义分析的代码实现,位于:
-
AST形态表示,位于clang/lib/AST和clang/include/clang/AST目录
-
语法分析,位于clang/lib/Parse和clang/include/clang/Parse目录
-
语义分析,位于clang/lib/Sema和clang/include/clang/Sema目录
1.1.1.4. 中间表示层IR形态
clang进一步以AST为输入,在经过代码生成流程,生成了LLVM IR代码。LLVM IR(Intermidate Representation)是LLVM编译器框架的中间表示层,也就是LLVM的核心形态。IR代码文件的主要顶层结构,包括全局符号(函数和全局变量)、元数据(Metadata)和数据布局(data layout)方式定义等。在函数表示方面,IR的形式为模块Module、函数Function、基本块BasicBlock、指令Instruction四级结构。
IR是编译器前端和后端的衔接桥梁,也是中端优化的操作对象形态。LLVM基于IR形态和其Pass机制,实现了基本的O0-3和Os、Oz标准优化流水线,还实现了作为LLVM亮点的链接时优化(Link Time Optimization,LTO)和其低内存压力版本ThinLTO。
使用clang的-emit-llvm -S选项,可以生成IR的文本形式.ll文件。以iop.c为例,其IR代码iop.ll的核心函数部分如下:
$ cat iop.ll
define i32 @my_iop(i32 %a, i32 %b) {
***\*entry:\****
%tobool.not = icmp eq i32 %a, 0
br i1 %tobool.not, label %if.else, label %if.then
***\*if.then:\****
%add = add nsw i32 %b, %a
br label %return
***\*if.else:\****
store i32 %b, ptr inttoptr (i64 256 to ptr)
br label %return
***\*return:\****
%retval.0 = phi i32 [ %add, %if.then ], [ 1, %if.else ]
ret i32 %retval.0
}
clang生成的iop.ll代码中有entry、if.then、if.else和return这4个BasicBlock。每个BasicBlock内的指令是串行的执行顺序,BasicBlock之间会相互跳转。用opt工具可以可视化BasicBlock间的跳转关系,生成IR函数的控制流图(Control Flow Graph,CFG),如图1-6所示。

图1-6 IR代码的CFG控制流图
LLVM对IR的形态描述和构建的代码实现,位于llvm/lib/IR源文件目录,以及llvm/include/llvm/IR头文件目录。clang由AST构建生成IR代码的实现,位于clang/lib/CodeGen源文件目录,以及clang/include/clang/CodeGen头文件目录。
LLVM的在线文档对IR的语法语义作了详尽介绍,读者可以参看:
https://llvm.org/docs/LangRef.html
1.1.1.5. 指令选择图SelectionDAG形态
自IR以后,就是代码的后端形态了,默认的第一个后端代码形态是SelectionDAG。它是以IR BasicBlock为单位构建出的有向无环图,图里每个节点称为一个SDNode,并对应一个ISD指令操作码。后端在SelectionDAG形态执行的变换包括类型和指令合法化、指令图优化、指令选择、指令调度和发射。SelectionDAG可以由llc的–view-isel-dags等选项可视化,如图1-7所示。

图1-7 后端指令选择的SelectionDAG图
SelectionDAG的特点,在于它引入了Chain和Glue两种指令约束关系,配合常规的数据依赖关系来共同描述SDNode间的约束和依赖关系。图中指向ch的若干虚线,表示的就是Chain依赖关系,这种关系要求被依赖指令先于依赖指令发射。Glue关系表示两条指令在发射时绑定,即这两条指令之间不能有其他指令。
LLVM对SelectionDAG的形态描述和构建的代码实现,位于llvm/lib/CodeGen/SelectionDAG源文件目录,以及llvm/include/llvm/CodeGen头文件目录。其中SDNode操作码集合定义在llvm/include/llvm/CodeGen/ISDOpcodes.h中。
1.1.1.6. Machine-IR(MIR)形态
SelectionDAG形态的代码经指令选择和发射,就生成了Machine-IR代码(以下简称MIR代码)。MIR代码和IR代码的结构相同,具有Module、Function、BasicBlock、Instruction的4级架构,但MIR的指令操作码大多是目标架构的机器指令。LLVM后端的各类代码优化、寄存器分配、指令调度流程也是基于MIR代码形态进行,且这些流程呈现为一个个可模块化插入和执行的Pass。
llc的-print-before和-print-after选项可以打印出特定Pass运行前后的MIR,例如下面iop.ll的MIR代码:
$ llc iop.ll -print-after dead-mi-elimination
\# *** IR Dump After Remove dead machine instructions (dead-mi-elimination) ***:
\# Machine code for function my_iop: IsSSA, TracksLiveness
Function Live Ins: $edi in %2, $esi in %3
***\*bb.0.entry\****:
successors: %bb.2(0x30000000), %bb.1(0x50000000); \
%bb.2(37.50%), %bb.1(62.50%)
liveins: $edi, $esi
%3:gr32 = COPY $esi
%2:gr32 = COPY $edi
TEST32rr %2:gr32, %2:gr32, implicit-def $eflags
JCC_1 %bb.2, 4, implicit $eflags
JMP_1 %bb.1
***\*bb.1.if.then\****:
; predecessors: %bb.0
successors: %bb.3(0x80000000); %bb.3(100.00%)
%0:gr32 = nsw ADD32rr %3:gr32(tied-def 0), %2:gr32, implicit-def dead $eflags
JMP_1 %bb.3
***\*bb.2.if.else\****:
; predecessors: %bb.0
successors: %bb.3(0x80000000); %bb.3(100.00%)
MOV32mr $noreg, 1, $noreg, 256, $noreg, %3:gr32 :: \
(store (s32) into `ptr inttoptr (i64 256 to ptr)`)
%4:gr32 = MOV32ri 1
***\*bb.3.return\****:
; predecessors: %bb.1, %bb.2
%1:gr32 = PHI %0:gr32, %bb.1, %4:gr32, %bb.2
$eax = COPY %1:gr32
RET 0, $eax
\# End machine code for function my_iop.
在iop.ll的上述MIR代码中,依然留存着IR形态时的4个BasicBlock。并且,代码中每个BasicBlock下面还用predecessors和sucessors标注了它的前驱BasicBlock和后继BasicBlock,从而表征了这些BasicBlock构成的CFG控制流图关系。
LLVM对MIR的形态描述和构建的代码实现,位于llvm/lib/CodeGen源文件目录,以及llvm/include/llvm/CodeGen头文件目录。
1.1.1.7. MC形态
机器码(Machine Code,MC)形态是Machine-IR的下一代码形态。MC代码形态的主要用途,是在AsmPrinter这个后端最后一个Pass中,充当Machine-IR形态和输出文件形态(生成ELF二进制文件,或ASM Text文本文件)间的中转形态。MC代码不具有MIR的Function、BasicBlock和Global Variable Pool信息,只有Label、Symbol、Section等概念。所以,抛开各类重定位不谈,MC代码和ELF中的.text二进制代码可视为信息等价。事实上,MC代码形态可以和ELF代码互相变换,也可以和ASM Text代码相互变换。例如,llvm-objdump的实现就是将ELF的.text变换为MC代码,再以文本形式打印出来。
LLVM对MC的形态描述和构建的代码实现,位于llvm/lib/MC源文件目录,以及llvm/include/llvm/MC头文件目录。
1.1.1.8. ELF形态
ELF是可执行文件格式的一种,适用于Linux和类UNIX系统环境。如果按照加载和链接能力来区分,ELF文件格式细分为可重定位文件(Relocatable,REL)、可执行文件(Executable,EXE)和动态链接库(Dynamic Shared Object,DSO)。Relocatable是可重定位的二进制目标文件,它是编译器后端的最终输出产物。但是Relocatable形态不可执行,需要经过链接器(lld或ld)链接生成ELF的EXE或DSO后才可以。
和整个编译链接流程的输入即文本源代码相比,二进制形式的DSO/EXE可读性不高,但对机器加载执行却十分便捷。ELF文件可以用llvm-readelf进行结构展示,其.text代码部分可以用llvm-objdump反汇编,从而以汇编文本展示,如图1-8所示。

图1-8 ELF文件的反汇编输出
LLVM对ELF的形态描述和构建的代码实现,位于lib/BinaryFormat源文件目录和llvm/include/llvm/BinaryFormat头文件目录。这些目录还包含COFF、MachO和WebAssembly等其他可执行文件格式的实现。
关于ELF可执行文件格式的定义细节,可以参看:
https://elinux.org/Executable_and_Linkable_Format_(ELF)
以汇编文本展示,如图1-8所示。
[外链图片转存中…(img-Klrktu8v-1766381312420)]
图1-8 ELF文件的反汇编输出
LLVM对ELF的形态描述和构建的代码实现,位于lib/BinaryFormat源文件目录和llvm/include/llvm/BinaryFormat头文件目录。这些目录还包含COFF、MachO和WebAssembly等其他可执行文件格式的实现。
关于ELF可执行文件格式的定义细节,可以参看:
https://elinux.org/Executable_and_Linkable_Format_(ELF)
1万+

被折叠的 条评论
为什么被折叠?



