- 什么是编译器
- LLVM概述
- LLVM案例体验
- LLVM源码 & 编译流程
1 什么是编译器?
1.1 Python案例
- 创建
python
文件夹,新建helloDemo.py
文件,内容如下:
print("hello")
-
调用
python helloDemo.py
执行文件,打印出python
image
1.2 C 案例
-
vim
创建helloDemo.c
文件:
#include <stdio.h>
int main(int a, char * argv[]) {
printf("hello \n");
return 0;
}
-
clang helloDemo.c
编译,生成a.out
文件。file a.out
查看文件:image
发现.out
文件是:64位的Mach-O
可执行文件,当前clang
出来的是x86_64
架构, mac
电脑可读。 所以可以./a.out
直接执行:
Q:
解释型
语言与编译型
语言
python
是解释型语言
,一边翻译
一边执行
。和js
一样,机器可直接执行。C
语言是编译型语言
,不能直接执行,需要编译器
将其转换
成机器识别语言
。
编译型语言
:编译后
输出的是指令
(0、1组合),cpu可直接执行指令
解释性语言
:生成的是数据
,不是0、1组合
,机器也能直接识别
编译器
的作用,就是将高级语言
转化为机器
能够识别
的语言
(可执行文件
)。
Q:汇编有指令吗?
- 早期科学家,使用
0、1编码
。 比如00001111
对应call
,00000111
对应bl
。有了对应关系
后。 再手敲
0和1就有点难受
了。于是写个中间解释器
,我们只用输入call
、bl
这样的标记指令
,经过解释器
,变成0和1的组合,再交给机器去执行。 这就是汇编的由来
。
- 而基于汇编往上,再
映射
和封装
相关对应关系
。就跨时代性
的c
语言,再往上
层封装,就出现了高级语言oc
、swift
等语言。所以汇编执行快
,因为它是直接转换
为机器语言
的。
- 但
汇编
的指令集
,是针对同一操作系统
而言,它不
支持跨平台
。机器指令
是cpu
的在识别
。早期的计算机厂家
非常多
,虽然都用0
和1
的组合
,但相同组合背后却是相应不同
的指令
。所以汇编无法跨平台
,不同操作系统
下,汇编指令
是不同
的。
2. LLVM概述
-
LLVM
是架构编译器
(compiler
)的框架系统
,以c++
编写而成,用于优化
以任意程序语言
编写的程序的编译时间
(compile-time
)、链接时间
(link-time
)、运行时间
(run-time
)以及空闲时间
(idle-time
),对开发者保持开放,并兼任已有脚本。 - 2006年
Chris Lattner
加盟Apple Inc.
并致力于LLVM
在Apple开发体系
中的应用。Apple
也是LLVM计划
的主要资助者
。
目前LLVM
已经被苹果iOS开发工具
、Xilinx Vivado
、Facebook
、Google
等各大公司采用。
2.1 传统编译器的设计
- 编译器前端(Frontend):
编译器的前端任务
是解析源代码
。 会进行词法分析
、语法分析
、语义分析
。检查源代码
是否存在错误
,然后构建抽象语法树
(Abstract Syntax Tree AST),LLVM前端
还会生成中间代码
(intermediate representation, IR)
- 优化器(Optimizer)
优化器负责各种优化
。改善
代码的运行时间
,如消除冗余计算
等
- 后端(Backkend)/ 代码生成器(CodeGenerator)
将代码映射
到目标指令集
,生成机器语言
,并进行机器相关
的代码优化
(目标指不同操作系统
)
iOS的编译器架构:
Objective C
/C
/C++
使用的编译器前端
是Clang
,Swift
是swift
,后端都是LLVM
。image
2.2 LLVM的设计
GCC
是一个非常成功
的编译器
,但由于它作为整体应用程序
设计的,用途
受到了限制
。LLVM
最重要的地方:支持多种语言
或多种硬件架构
。使用通用代码
表示形式:IR
(用来在编译器中表示代码的形式)LLVM
可以为任何编程语言
独立编写前端
,也可以为任何硬件架构
独立编写后端
.所以LLVM
不是
一个简单的编译器
,而是架构编译器
,可以兼容
所有前端
和后端
。
2.3 Clang
Clang
是LLVM项目
的一个子项目
。基于LLVM架构
的轻量级编辑器
,诞生之初
就是为了替代GCC
,提供更快
的编译速度
。 他是负责编译C
、C++
、Objecte-C
语言的编译器
,它属于
整个LLVM架构
中的编译器前端
。
- 对于开发者而言,
研究Clang
可以给我们带来很多好处
。
3. LLVM案例体验
-
新建一个
Mac OS
的命令行
工程:image -
没有改动代码
image
3.1 编译流程
- cd到
main.m
的文件夹。使用下面命令查看main.m
的编译步骤:
clang -ccc-print-phases main.m
编译流程
分为以下7步
:
-
0: input, "main.m", objective-c
:
输入文件:找到源文件 -
1: preprocessor, {0}, objective-c-cpp-output
:
预处理:宏的展开,头文件的导入 -
2: compiler, {1}, ir
:
编译:词法、语法、语义分析,最终生成IR -
3: backend, {2}, assembler ()
:
汇编: LLVM通过一个个的Pass去优化,每个Pass做一些事,最后生成汇编代码 -
4: assembler, {3}, object
:
目标文件 -
5: linker, {4}, image
:
链接: 链接需要的动态库和静态库,生成可执行文件 -
6: bind-arch, "x86_64", {5}, image
:
架构可执行文件:通过不同架构,生成对应的可执行文件
optimizer优化
并没有
作为一个独立阶段
,在编译阶段
内部完成
的
3.2 预处理阶段
-
main.m
中准备测试代码
:
#import <stdio.h>
#define C 30
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
int b = 20;
printf("%d", a + b + C);
}
return 0;
}
-
clang
预编译输出main2.m
文件:
clang -E main.m >> main2.m
-
打开
main2.m
,有575行
。其中大部分是stdio
库的代码:image 我们发现测试代码中的
宏C
,在预编译阶段
完成了替换
,变成了30
预编译阶段: 1.
导入头文件
2.替换宏
- 修改测试代码,给
int类型
取个别名HT_INT_64
,再次预编译处理
:
#define C 30
typedef int HT_INT_64;
int main(int argc, const char * argv[]) {
@autoreleasepool {
HT_INT_64 a = 10;
HT_INT_64 b = 20;
printf("%d", a + b + C);
}
return 0;
}
-
发现
typedef
不会被替换image
安全拓展:
- 使用
define
将重要方法
名称进行替换
。比如#define Pay XXXTest
这样开发者使用宏Pay
开发舒服,但是被hank
时,实际代码是XXXTest
,不容易被察觉。
(#define
的真实内容
,不应该
写成乱码
,会让人有此地无银三百两
的感觉,最好
弄成系统类似名称
或其他不经意
的名称
。这样才容易
被忽视
,安全级别
才更高
😃)
typedef
没有这个偷梁换柱的效果。define
只影响预处理期。
3.3 编译阶段
3.3.1 词法分析
- 编译
main.m
文件:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
-词法分析
,就是根据空格
和括号
这些将代码拆分
成一个个Token
。标注了位置
是第几行
的第几个字符
开始的。
3.3.2 语法分析
-
语法分析
是验证语法
是否正确
。
在词法分析的基础上,将单词
序列组合
成各类语法短语
,如“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树
(Abstract Syntax Tree,AST)。语法分析程序
判断源程序
在结构
上是否正确
。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
作用域
、类型
、运算方式
都十分清晰
。( 语法树一次只能处理一次计算。两次运算,就得多分一层级。)image 语法分析
,就是在生成语法树
时完成检测
的。
- 头文件找不到时,可以指定SDK:
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m
3.4 生成中间代码IR(Intermediate representation)
3.4.1 生成中间代码
完成以上步骤后,就开始生成
中间代码IR
,代码生成器(Code Generation)会将语法树自顶向下
遍历逐步翻译成LLVM
的IR
。便于理解,我们简化代码:
#import <stdio.h>
int test(int a, int b) {
return a + b + 3;
}
int main(int argc, const char * argv[]) {
int a = test(1,2);
printf("%d",a);
return 0;
}
通过下面命令生成.ll
文本文件,查看IR代码:
clang -S -fobjc-arc -emit-llvm main.m
- IR基本语法
@
全局标识
%
局部标识
alloca
开辟空间
align
内存对齐
i32
32个bit,4个字节
store
写入内存
load
读取数据
call
调用数据
ret
返回
- 使用
VSCode
或Sublime Text
可以打开代码:(可以指定文件
的语言
,让代码
有高亮色
)
- Q:图中为何
多创建
那么多局部变量
?(如test函数内的a5、a6)- 因为在上一阶段(
编译阶段
),我们将代码
编译成了语法树结构
。而此时,我们只是沿
着语法树
进行读取
。 语法树每一个层级
,都需要
一个临时变量
来承接
。再返回上一层级处理
。- 所以会
产生
那么多局部变量
。
3.4.2 IR优化
- 我们可以在
Xcode
的Build Settings
中搜索Optimization
,可以看到优化级别。
(Debug模式
默认None [O0]
无优化,Release模式
默认Fastest,Smallest [Os]
最快最小)
LLVM的优化级别分为
-O0
、-O1
、-O2
、-O3
、-Os
(第一个字母是Optimization的O)。分别选择
O0
和Os
两个优化等级进行中间代码的生成比较:
clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll // O0 无优化
clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll // Os 最快最小
-
优化后
的代码,舒服
多了。之前那些冗余
的临时局部变量
,也都被优化
,代码量减少
很多。
3.4.3 bitCode再优化
-
Xcode7之后
,开启bitCode
苹果会再进一步优化
,生成.bc
的中间代码
。
优化体现
:上传APPstore的包
,针对不同型号手机
做了区分
,不同型号手机下载
时,包
的大小不同
。
clang -emit-llvm -c main.ll -o main.bc
3.5 生成汇编代码
完成
中间代码
的生成后,可以将代码转变
为汇编代码
了。-
此刻我们有
4种
不同程度的代码(源代码
->无优化IR代码
->Os优化IR代码
->bitcode优化代码
):image 分别对
4种程度
的代码输出汇编
文件:
clang -S -fobjc-arc main.m -o main.s
clang -S -fobjc-arc main.ll -o mainO0.s
clang -S -fobjc-arc mainOs.ll -o mainOs.s
clang -S -fobjc-arc main.bc -o mainbc.s
可以看到在生成汇编代码
时,只有选择
了优化等级
,才能减少
汇编代码量
。
【拓展】在
生成中间代码
的前后
,都可以
进行优化
。
- [尝试一] 将
main.m
直接选择Os级别
优化生成.s
汇编文件clang -Os -S -fobjc-arc main.m -o mainOs.s
- [尝试二] 将
main.m
生成无优化
的main.s
,再main.s
选择Os级别
优化生成.s
汇编文件clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll clang -Os -S -fobjc-arc mainO0.ll -o mainOoOs.s
- [尝试三] 将
main.m
选择Os级别
优化生成main.s
,再main.s
选择无优化
级别生成.s
汇编文件clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll clang -S -fobjc-arc mainOs.ll -o mainOsOo.s
- [尝试四] 将
main.m
选择Os级别
优化生成main.s
,再main.s
选择Os级别
优化生成.s
汇编文件clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll clang -Os -S -fobjc-arc mainOs.ll -o mainOsOs.s
- 内容比较:
image
3.6 生成目标文件(机器代码)
-
生成汇编文件
后,汇编器
以汇编代码
作为输入
,将汇编代码转换
为机器代码
,输出
目标文件(object file
)
clang -fmodules -c main.s -o main.o
file
对比一下main.s
汇编代码和main.o
机器代码:file main3.m file main.o
image
-
xcrun
执行nm
命令查看main.o
文件中的符号
:
xcrun nm -nm main.o
- 此时只是把
当前文件
编译为了机器码
,外部符号
(如printf
)无法识别。
undefined:
表示当前文件
暂时找不到符号
。
external:
表示这个符号
是外部可以访问
的。(实现
不在我这,在外部
的某个地方
)
所以当前虽转换
成了机器代码
。但是只是目标文件
,并不能
直接执行
,需要将
所有资源链接
起来,才可以执行
。
3.7 生成可执行文件(链接)
- 通过
链接器
把编译产生的.o
文件和.dylib
、.a
文件链接关联
起来,生成真正的mach-o可执行文件
clang main.o -o main // 将目标文件转成可执行文件
file main // 查看文件
xcrun nm -nm main // 查看main的符号
- 对比
main.o
目标文件,此时生成的main
文件:
- 从
object
文件变成了executable
可执行文件- 虽然都有
undefined
,但是可执行文件
中指定了该符号
的来源库
。机器在运行时
,会从相应的库
中取读取
该符号
(printf
)
至此,我们已完整分析:源代码
到可执行文件
的整个流程
:
4. LLVM源码 & 编译流程
⚠️ ⚠️ ⚠️ 【注意】
LLVM
源码2.29G
,编译后
文件30G
,请确保
电脑硬盘空间足够
;编译时
,电脑温度会飙升90多度
,请用空调伺候
着,可能
会黑屏
;编译时间
长达1个多小时
,请合理安排时间。
如果以上3点
,你确定能接受
,那我们就开始
吧。
4.1 LLVM下载
-
Gitee
上有已配置
好关联库
的源码
,可直接下载: https://gitee.com/mirrors/LLVM
拓展:
- github上的官方源码:https://github.com/llvm/llvm-project(国内网络限制)
- 需要注意的是,官方源码不能直接编译,需要下载
clang
、compiler-rt
、libcxx
、libcxxabi
这4个库。- 建议使用上面Gitee源。
4.2 LLVM编译
-
30G
,90°C
,1hour
之旅,begin~
- 最新的LLVM只支持
cmake
编译,需要使用Homebrew
安装cmake
:
4.2.1 安装cmake
- 查看
brew列表
,检查是否安装过cmake
,如果有,就跳过此步骤
brew list
- 如果没有,就使用
brew安装
:
brew install cmake
如果报权限错误,可
sudo chown -R
whoami:admin /usr/local/share
放开权限image
4.2.2 编译llvm
-
cmake
编译成Xcode
项目
cd llvm-project
mkdir build
cd build
cmake -G Xcode ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="Release" ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="debug" ../llvm
-
成功之后,可以看到生成的
Xcode
文件:image -
打开
LLVM.xcodeproj
,选择自动创建Schemes
。image -
自动创建完成
后,选择ALL_BUILD
进行编译(耗时0.5-1小时,CPU满负荷运转)image 编译完成。