25:LLVM 简介和编译流程详解
目录
image.png传统编译器设计
image.png-
输入源代码(
Obj-C
,Swift
, ...) → 编译器处理 → 输出机器码(010101
) -
编译器处理分为以下步骤
前端 (Frontend)
负责解析源代码,进行:
-
词法分析
-
语法分析,语义分析,检查源代码是否有错误,构建 抽象语法树 (
Abstract Syntax Tree
, AST)
优化器 (Optimizer)
负责进行各种优化。例如消除冗余计算 (甚至直接将方法优化成一个固定值,而不去调用方法)等。
后端 (Backend)
将代码映射到目标指令集。生成机器语言,此过程会再次优化 (机器语言层面)。
LLVM 的设计
-
从图里看出,编译器前端输入源代码,后端输出机器码。因为传统编译器是按照整体程序设计的,所以总共需要做 n×m 个编译器。
-
image.pngLLVM
使用通用的代码表现形式 (IR
,可以理解为中间码),优化器的出入口都是IR
,所以LLVM
可以为任何编程语言独立编写前端,为任何硬件架构独立编写后端,工作量缩减为 n+m,且能集中力量不断提升优化器性能。
Clang 编译流程
Clang
是LLVM
的一个子项目。它属于整个LLVM
架构的编译器 前端,负责编译C
、C++
、Objective-C
。
运行命令,打印源码编译阶段
运行命令clang -ccc-print-phases main.m
0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
- 0:输入文件:找到源文件
- 1:预处理:替换宏,但不会替换别名
typedef
;头文件导入并展开,包括头文件的头文件,代码行数激增 - 2:编译:词法分析 (切割成一个个词,不检查语法错误)、语法分析 (组装词,检查语法错误)、最终生成
IR
- 3:后端:
LLVM
通过一个个Pass
(类似节点) 去优化,每个Pass
有自己的优化方式,最终生成汇编代码 - 4:把汇编文件变成
.o
文件 - 5:各个
.o
文件有联系,需要进行链接,生成Mach-O
文件 - 6:对应不同架构,生成对应的
Mach-O
文件
1: 预处理
-
main.m
文件#import <stdio.h> #define a 10 typedef int MD_INT_64; int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... MD_INT_64 b = 20; printf("sum = %d", a + b + 50); } return 0; }
-
运行命令
clang -E main.m >> main1.cpp
,如果不输入>> main1.cpp
,则不会新生成文件,而直接在命令行工具打印。以下省略前面549行代码 ↓typedef int MD_INT_64; int main(int argc, const char * argv[]) { @autoreleasepool { MD_INT_64 b = 20; printf("sum = %d", 10 + b + 50); } return 0; }
2.1: 编译-词法分析 (切割词)
-
运行命令
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
-
第几行,第几个字符开始,第几个字符结束,一目了然。只截取了一些 ↓
// insert' Loc=<main.m:9:1> typedef 'typedef' [StartOfLine] Loc=<main.m:13:1> int 'int' [LeadingSpace] Loc=<main.m:13:9> identifier 'MD_INT_64' [LeadingSpace] Loc=<main.m:13:13> semi ';' Loc=<main.m:13:22> int 'int' [StartOfLine] Loc=<main.m:15:1> identifier 'main' [LeadingSpace] Loc=<main.m:15:5> l_paren '(' Loc=<main.m:15:9> int 'int' Loc=<main.m:15:10> identifier 'argc' [LeadingSpace] Loc=<main.m:15:14>
2.2: 编译-语法分析 (重新组合,生成抽象语法树)
-
运行命令
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
如果导入了iOS特有的头文件,需要修改一下指令 (仅供参考,每个人电脑路径和模拟器版本不一样)
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdkSDK -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
经过重新组合,语法分析出来的代码行数通常会比词法分析短一些,譬如词法分析里的
int
、argc
,在语法分析里变成一行这是一个名叫argc的int类型参数
。最好带着栈思维去读抽象语法树。只截取了一些 ↓|-TypedefDecl 0x7fd405845368 <line:13:1, col:13> col:13 referenced MD_INT_64 'int' | `-BuiltinType 0x7fd405036700 'int' `-FunctionDecl 0x7fd405845640 <line:15:1, line:22:1> line:15:5 main 'int (int, const char **)' |-ParmVarDecl 0x7fd4058453d8 <col:10, col:14> col:14 argc 'int' |-ParmVarDecl 0x7fd4058454f0 <col:20, col:38> col:33 argv 'const char **':'const char **' `-CompoundStmt 0x7fd4050f1ad8 <col:41, line:22:1> |-ObjCAutoreleasePoolStmt 0x7fd4050f1a90 <line:16:5, line:20:5> | `-CompoundStmt 0x7fd4050f1a70 <line:16:22, line:20:5> | |-DeclStmt 0x7fd4050f1868 <line:18:9, col:25> | | `-VarDecl 0x7fd4050f1400 <col:9, col:23> col:19 used b 'MD_INT_64':'int' cinit | | `-IntegerLiteral 0x7fd4050f1468 <col:23> 'int' 20 | `-CallExpr 0x7fd4050f1a10 <line:19:9, col:38> 'int' | |-ImplicitCastExpr 0x7fd4050f19f8 <col:9> 'int (*)(const char *, ...)' <FunctionToPointerDecay> | | `-DeclRefExpr 0x7fd4050f1880 <col:9> 'int (const char *, ...)' Function 0x7fd4050f1490 'printf' 'int (const char *, ...)'
2.3 / 3.0: 生成中间码 IR (Intermediate Representation) / Pass 优化
-
代码生成器 (
Code Generation
) 会将语法树自顶向下遍历,翻译成LLVM IR
-
运行命令
clang -S -fobjc-arc -emit-llvm main.m
,获得main.ll
文件。和汇编有点像。只截取了main
函数 ↓ -
IR
基本语法@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit,共4个字节
store 写入内存
load 读内存的数据
call 调用函数
ret 返回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 20, i32* %6, align 4 %8 = load i32, i32* %6, align 4 %9 = add nsw i32 10, %8 %10 = add nsw i32 %9, 50 %11 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([9 x i8], [9 x i8]* @.str, i64 0, i64 0), i32 %10) call void @llvm.objc.autoreleasePoolPop(i8* %7) ret i32 0 }
-
刚才是没有优化的,看看优化的,
LLVM
的优化级别分别为-O0
-O1
-O2
-03
-Os
,我们试试-Os
,运行命令clang -Os -S -fobjc-arc -emit-llvm main.m
,获得main.ll
文件。print
函数的参数,直接用绝对值80
,而不像刚才用局部变量算来算去。只截取了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 ([9 x i8], [9 x i8]* @.str, i64 0, i64 0), i32 80) #3, !clang.arc.no_objc_arc_exceptions !9 tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #1 ret i32 0 }
-
这个优化级别在
image.pngXcode
可以调:Build Settings
→Code Generation
。Debug
模式下为了编译快点一般不优化,选None [-O0]
LLVM
的优化使用了叫Pass
的东西,可以理解为优化节点,每个节点负责不同的优化事项 (跳转、运算等),一个个Pass
搞下来,逻辑处理发生变化,就完成了优化。如果想玩LLVM
优化可以试试写Pass
。
Pass
能使FuncA→FuncB→FuncC
变成FuncA→FuncC
甚至FuncA(算好的值)
;也能使FuncA→FuncB
变成FuncA→FuncX→FuncY→FuncB
,变得复杂,做到混淆效果。不光是逻辑,其中的局部标识也能增加。直接混淆还能看懂些,优化完以后再混淆就真的难看懂。
2.4: Bitcode
Xcode7
以后,Enable Bitcode
苹果会在IR
的基础上做进一步的优化,生成.bc
代码。
iOS端:
Bitcode
可选
watchOS端:Bitcode
必选
macOS端:Bitcode
不可选
- 运行命令
clang -emit-llvm -c main.ll -o main.bc
。.bc
文件暂时不知道怎么打开,没有截图。
3.1: 生成汇编代码 (属于 后端Backend / 代码生成器CodeGenerator)
汇编代码可以由.ll
或.bc
代码生成。
-
运行命令
clang -S -fobjc-arc main.bc -o main.s
-
或运行命令
clang -S -fobjc-arc main.ll -o main.s
-
这里也能优化 (机器语言层面)
clang -Os -S -fobjc-arc main.m -o main.s
-
只截取部分代码 ↓
subq $48, %rsp movl $0, -4(%rbp) movl %edi, -8(%rbp) movq %rsi, -16(%rbp) callq _objc_autoreleasePoolPush movl $20, -20(%rbp)
4: 生成目标文件 .o
汇编器将汇编代码转换为机器代码,这就是.o
文件 (object file
)。
-
运行命令
clang -fmodules -c main.s -o main.o
-
运行命令
xcrun nm -nm main.o
,查看main.o
中的符号-
undefined
,当前文件暂时找不到 -
external
,这个符号在外部找 (我们自己内部没有)
(undefined) external _objc_autoreleasePoolPop (undefined) external _objc_autoreleasePoolPush (undefined) external _printf 0000000000000000 (__TEXT,__text) external _main
-
5. 生成可执行文件 Mach-O
链接器 (Linker
) 把.o
文件和.dylib
.a
文件 生成一个Mach-O
文件。
现在是编译阶段,这个
Linker
不是dyld
,dyld
是运行时的事情。
-
运行命令
clang main.o -o main
友情提示:如果是上面一路跟下来的,这里会因为找不到
@autoreleasepool
报错,请去掉源码里的@autoreleasepool
再跟一下) -
文件变大了,
main.s
1KB,main
13KB -
运行命令
xcrun nm -nm main
,查看main
中的符号。(undefined) external _printf (from libSystem) (undefined) external dyld_stub_binder (from libSystem) 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header 0000000100000f73 (__TEXT,__text) external _main 0000000100002008 (__DATA,__data) non-external __dyld_private
-
上面是编译阶段,下面要讲的是运行阶段(
dyld
相关)的事情。虽然printf
仍然是undefined
,但这只是一个标示,后面写了(from libSystem)
,意味着当程序跑起来的时候,自己没有printf
,它是个external
外部函数,找libSystem
,刚好iOS操作系统有libSystem
,在那里找到printf
的地址以后,进行符号绑定就OK了。 -
运行命令
./main
,执行程序sum = 80%
-
运行命令,
file main
,看文件类型和架构main: Mach-O 64-bit executable x86_64