LLVM概述
LLVM是构架编译器的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)、以及空闲时间(idle-time),对开发保持开发,并且兼容已有的脚步。
LLVM计划启动于2000年,最初由美国UIUC大学的Chris Lattner博主主持研发的。2006年Chris Lattner加盟Apple Inc. 并致力于LLVM在Apple开发体系中的应用。Apple也是LLVM计划的主要资助者。
目前LLVM已经被Apple、Facebook、Google等公司采用。
1.传统的编译器设计
1.1 编译器前端(Frontend)
编译器前端的任务是解析源代码,进行词法分析、语法分析、语义分析
,检查源代码是否存在错误,然后会构建为抽象语法树(Abstract Syntax Tree,AST)
,LLVM的前端还会生成中间代码(intermediate representation,IR)
1.2 优化器
优化器负责进行各种优化。改善代码的大小与运行时间,例如消除冗余的计算。
1.3 后端(Backend)
将代码映射到目标指令集.生成机器语言,并且进行机器相关的代码优化
2.苹果的编译器设计
Ojective C/C/C++使用的编译器前端是Clang,Swift是Swift编译器,后端都是LLVM。
3.LLVM的设计
LLVM也分三个阶段,但是设计上略微的有些区别, LLVM不同的就是对于不同的语言它都提供了同一种中间表示(IR):
前端可以使用不同的工具对代码进行词法分析、语法分析、语义分析,生成抽象语法树。然后转为LLVM的中间表示,中间部分的优化器通过一系列的pass对IR做优化,后端负责将优化好的IR解释成对应不同架构的机器码。所以LLVM可以为任何编程语言编写独立的前端以及后端。
3.1 Clang与LLVM的关系
Clang是一个C++编写的,基于LLVM架构的轻量级编译器,发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器。诞生之初是为了替代GCC编译器,相比GCC而言,它的编译速度快、占用内存小、更加方便进行二次开发。
它属于整个LLVM架构中的编译器前端。
4.LLVM编译流程
LLVM编译一个源文件的过程:预处理 -> 词法分析 -> Token -> 语法分析 -> AST -> 代码生成 -> LLVM IR -> 优化 -> 生成汇编代码 -> Link -> 目标文件
我们可以通过命令行工具打印出源码的编译主要流程
clang -ccc-print-phases main.m
-
输入文件(input)
:找到源文件 -
预处理阶段(preprocessor)
:这个过程处理宏定义的替换、头文件的导入 -
编译阶段(compiler)
:进行词法分析(生成Token)、语法分析(生成AST),最终生成中间代码IR
4.后端(backend)
:这里LLVM会通过一个一个的Pass去优化,每一个Pass做一些事情,最终生成汇编代码
5.汇编(assembler)
:生成目标文件.o
6.链接(linker)
:链接需要的动态库和静态库(系统的动态库与静态库在共享缓存中),生成可执行文件
7.绑定不同的架构(bind-arch)
:,生成对应的可执行文件
4.1 预处理阶段
通过执行如下命令,可以看到头文件的导入与宏定义的替换
clang -E main.m
替换前
替换后
4.2 编译阶段
4.2.1 词法分析
预处理完成之后,就会源代码进行词法分析,这里会把代码分割一个个的Token,比如大小括号,字符串以及等号等。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
通过命令执行之后的代码
annot_module_include '#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
int a = 10;
int b' Loc=<main.m:9:1>
int 'int' Loc=<main.m:10:1>
identifier 'main' [LeadingSpace] Loc=<main.m:10:5>
l_paren '(' Loc=<main.m:10:9>
int 'int' Loc=<main.m:10:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:10:14>
comma ',' Loc=<main.m:10:18>
const 'const' [LeadingSpace] Loc=<main.m:10:20>
char 'char' [LeadingSpace] Loc=<main.m:10:26>
star '*' [LeadingSpace] Loc=<main.m:10:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:10:33>
l_square '[' Loc=<main.m:10:37>
r_square ']' Loc=<main.m:10:38>
r_paren ')' Loc=<main.m:10:39>
l_brace '{' [LeadingSpace] Loc=<main.m:10:41>
at '@' [StartOfLine] [LeadingSpace] Loc=<main.m:11:5>
identifier 'autoreleasepool' Loc=<main.m:11:6>
l_brace '{' [LeadingSpace] Loc=<main.m:11:22>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:13:9>
identifier 'a' [LeadingSpace] Loc=<main.m:13:13>
equal '=' [LeadingSpace] Loc=<main.m:13:15>
numeric_constant '10' [LeadingSpace] Loc=<main.m:13:17>
semi ';' Loc=<main.m:13:19>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:14:9>
identifier 'b' [LeadingSpace] Loc=<main.m:14:13>
equal '=' [LeadingSpace] Loc=<main.m:14:15>
identifier 'a' [LeadingSpace] Loc=<main.m:14:17>
plus '+' [LeadingSpace] Loc=<main.m:14:19>
identifier 'COUNT' [LeadingSpace] Loc=<main.m:14:21>
semi ';' Loc=<main.m:14:26>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:15:9>
l_paren '(' Loc=<main.m:15:15>
string_literal '"%d"' Loc=<main.m:15:16>
comma ',' Loc=<main.m:15:20>
identifier 'b' Loc=<main.m:15:21>
r_paren ')' Loc=<main.m:15:22>
semi ';' Loc=<main.m:15:23>
r_brace '}' [StartOfLine] [LeadingSpace] Loc=<main.m:16:5>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:17:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:17:12>
semi ';' Loc=<main.m:17:13>
r_brace '}' [StartOfLine] Loc=<main.m:18:1>
eof '' Loc=<main.m:18:2>
4.2.2 语法分析
它的任务就是验证程序的语法是否正确,在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,然后将所有节点组成抽象语法树(AST),
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
命令执行的main函数代码如下
-ImportDecl 0x7fd3c7820378 <main.m:9:1> col:1 implicit Foundation
`-FunctionDecl 0x7fd3c7820640 <line:10:1, line:17:1> line:10:5 main 'int (int, const char **)'//main函数
|-ParmVarDecl 0x7fd3c78203d0 <col:10, col:14> col:14 argc 'int'//第一个参数argc
|-ParmVarDecl 0x7fd3c78204f0 <col:20, col:38> col:33 argv 'const char **':'const char **' //第二个参数 argv
`-CompoundStmt 0x7fd3c7820de0 <col:41, line:17:1> //复合声明 {}
|-ObjCAutoreleasePoolStmt 0x7fd3c7820d98 <line:11:5, line:15:5>//自动释放池声明
| `-CompoundStmt 0x7fd3c7820d78 <line:11:22, line:15:5>//复合声明{}
| |-DeclStmt 0x7fd3c7820bf8 <line:13:9, col:19> //倾斜声明
| | `-VarDecl 0x7fd3c7820790 <col:9, col:17> col:13 used a 'int' cinit//变量int a
| | `-IntegerLiteral 0x7fd3c78207f8 <col:17> 'int' 10//整形字面量10
| `-CallExpr 0x7fd3c7820d00 <line:14:9, col:22> 'int'//调用printf(<#const char *restrict, ...#>)
| |-ImplicitCastExpr 0x7fd3c7820ce8 <col:9> 'int (*)(const char *, ...)' <FunctionToPointerDecay>//print 隐式函数表达式int (*)(const char *, ...)
| | `-DeclRefExpr 0x7fd3c7820c10 <col:9> 'int (const char *, ...)' Function 0x7fd3c7820820 'printf' 'int (const char *, ...)'//printf函数描述,函数地址,名称,返回值类型,参数类型
| |-ImplicitCastExpr 0x7fd3c7820d48 <col:16> 'const char *' <NoOp>//printf函数第一个参数
| | `-ImplicitCastExpr 0x7fd3c7820d30 <col:16> 'char *' <ArrayToPointerDecay>//printf函数第二个参数
| | `-StringLiteral 0x7fd3c7820c68 <col:16> 'char [3]' lvalue "%d"//第三个变量 %d
| `-ImplicitCastExpr 0x7fd3c7820d60 <col:21> 'int' <LValueToRValue>
| `-DeclRefExpr 0x7fd3c7820c88 <col:21> 'int' lvalue Var 0x7fd3c7820790 'a' 'int'//int a
`-ReturnStmt 0x7fd3c7820dd0 <line:16:5, col:12>//返回声明
`-IntegerLiteral 0x7fd3c7820db0 <col:12> 'int' 0//返回0
4.3 生成中间代码IR
完成以上步骤后就开始生成中间代码IR,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR,可以通过以下命令生成.ll文件,查看IR代码。注意:没有经过编译器优化
clang -S -fobjc-arc -emit-llvm main.m
4.3.1 IR的基本语法
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit,4个字节
store 写入内存
load 读取数据
call 调用函数
ret 返回
执行命令之后的main函数代码如下:
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.6"
@.str = private unnamed_addr constant [3 x i8] c"%d\00", align 1
; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32, i8**) #0 {
%3 = alloca i32, align 4 // 开辟一个4字节内存 %3
%4 = alloca i32, align 4 // 开辟一个4字节内存 %4
%5 = alloca i8**, align 8 // 开辟一个8字节内存 %5
%6 = alloca i32, align 4 // 开辟一个4字节内存 %6
store i32 0, i32* %3, align 4 // 将%3写入内存
store i32 %0, i32* %4, align 4 // 将%4写入内存
store i8** %1, i8*** %5, align 8 // 将%5写入内存
%7 = call i8* @llvm.objc.autoreleasePoolPush() #1 //调用autoreleasePoolPush
store i32 10, i32* %6, align 4 //将10写入 %6,即 %6 = 10
%8 = load i32, i32* %6, align 4 //读取 %6 赋%8 = 10
%9 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 %8)//调用printf函数
call void @llvm.objc.autoreleasePoolPop(i8* %7)//调用 autoreleasePoolPop
ret i32 0 //返回0
}
4.3.2 IR的优化
LLVM的优化级别分别是-O0 -O1 -O3 -Os -Ofast -Oz
-O3:最快,但是包最大
-Os:是xcode默认的优化级别,它平衡了包体积大小与速度
-Oz:加载慢,但是包体积小,它内部是将多个函数汇编代码相同的指令放到一个新的函数
我们可以通过以下命令行去查看优化之后的IR
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
//未开启优化之前的main函数
define i32 @main(i32, i8**) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
%6 = alloca i32, align 4
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%7 = call i8* @llvm.objc.autoreleasePoolPush() #1
store i32 10, i32* %6, align 4
%8 = load i32, i32* %6, align 4
%9 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 %8)
call void @llvm.objc.autoreleasePoolPop(i8* %7)
ret i32 0
}
//开启-Os优化之后的main函数
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i8* @llvm.objc.autoreleasePoolPush() #1
%4 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 10) #3, !clang.arc.no_objc_arc_exceptions !9
tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #1
ret i32 0
}
从上面我们可以很明显的看到了编译器做了很大的优化。
4.4 bitCode
xcode以后开启bitcode苹果会进一步的优化,生成.bc的中间代码,我们可以优化后的IR代码生成.bc代码,
clang -emit-llvm -c main.ll -o main.bc
4.5 生成汇编代码
我们可以通过最终的.bc或者.ll代码生成汇编代码
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15, 6 sdk_version 10, 15, 6
.globl _main ## -- Begin function main
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movl %edi, -4(%rbp) ## 4-byte Spill
movq %rsi, -16(%rbp) ## 8-byte Spill
callq _objc_autoreleasePoolPush
leaq L_.str(%rip), %rdi
movl $10, %esi
movq %rax, -24(%rbp) ## 8-byte Spill
movb $0, %al
callq _printf
movq -24(%rbp), %rdi ## 8-byte Reload
movl %eax, -28(%rbp) ## 4-byte Spill
callq _objc_autoreleasePoolPop
xorl %eax, %eax
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "%d"
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
生成汇编代码也可以进行优化
clang -Os -S -fobjc-arc main.m -o main.s
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15, 6 sdk_version 10, 15, 6
.globl _main ## -- Begin function main
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
pushq %rbx
pushq %rax
.cfi_offset %rbx, -24
callq _objc_autoreleasePoolPush
movq %rax, %rbx
leaq L_.str(%rip), %rdi
movl $10, %esi
xorl %eax, %eax
callq _printf
movq %rbx, %rdi
callq _objc_autoreleasePoolPop
xorl %eax, %eax
addq $8, %rsp
popq %rbx
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "%d"
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
4.6 生成目标文件(汇编器)
目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器语言,最后输出目标文件(object file)
clang -fmodules -c main.s -o main.o
通过nm命令
,可以查看main.o中的符号
xcrun nm -nm main.o
_printf
是一个undefined external的undefined
:表示在当前文件暂时找不到符号_printfexternal
:表示这个符号是外部可以访问的
4.7 生成可执行文件(链接)
链接器把编译生成的.o文件和(.dylib .a)文件链接之后,生成一个mach_o文件
clang main.o -o main
查看链接之后的符号
执行main
./main
输出 10