前言
- 今天来学习一下牛逼的架构编译器LLVM
学习大纲
- 简单了解编译器
- LLVM概述
- LLVM案例体验
- LLVM源码 & 编译流程
准备
-
github上的官方源码:https://github.com/llvm/llvm-project(国内网络限制) ,需要注意的是,官方源码不能直接编译,需要下载
clang
、compiler-rt
、libcxx
、libcxxabi
这4个库。建议使用上面Gitee源。
一 、简单了解编译器
-
解释型语言
与编译型语言
- 编译型语言:编译后输出的是
指令
(0、1组合),cpu可直接执行指令 - 解释性语言:生成的是数据,不是0、1组合,机器也能直接识别
- 编译型语言:编译后输出的是
python
是解释型语言
,一边翻译一边执行。和js一样,机器可直接执行
。
C语言
是编译型语言
,不能直接执行,需要编译器
将其转换成机器识别语言
。
-
编译器的
作用
,就是将高级语言转化为机器能够识别的语言(可执行文件)。 -
汇编指令
- 早期科学家,使用0、1编码。 比如 00001111 对应 call, 00000111 对应bl。有了对应关系后。 再手敲0和1就有点难受了。于是写个中间解释器,我们只用输入call、bl这样的标记指令,经过解释器,变成0和1的组合,再交给机器去执行。 这就是汇编的由来。
- 而基于汇编往上,再
映射
和封装
相关对应关系。就跨时代性的c语言,再往上层封装,就出现了高级语言oc、swift等语言。所以汇编执行快,因为它是直接转换为机器语言的。- 但
汇编的指令集
,是针对同一操作系统
而言,它不支持跨平台
。机器指令是cpu的在识别。早期的计算机厂家非常多,虽然都用0和1的组合,但相同组合背后却是相应不同的指令。所以汇编无法跨平台
,不同操作系统
下,汇编指令
是不同
的。
- 案例创建体验
- Python 案例:创建python文件夹,新建helloDemo.py文件,
内容:
print("hello")
-
可以看出 Python文件内容 可直接执行
-
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查看文件:
-
发现.out文件是:64位的
Mach-O
可执行文件,当前clang出来的是x86_64
架构, mac电脑可读。 所以可以./a.out直接执行: -
可以看出 C 文件内容 需要编译
二 、 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等各大公司采用。
1. 传统编译器的设计
- 编译器前端(Frontend):
编译器的前端任务是解析源代码。 会进行词法分析、语法分析、语义分析。
检查源代码
是否存在错误,然后构建抽象语法树
(Abstract Syntax Tree AST),LLVM前端还会生成中间代码
(intermediate representation, IR)
- 优化器(Optimizer)
优化器
负责各种优化
。改善代码的运行时间,如消除冗余计算等
- 后端(Backkend)/ 代码生成器(CodeGenerator)
将
代码
映射到目标指令集
,生成机器语言
,并进行机器相关的代码优化
(目标指不同操作系统)
2. iOS的编译器架构
Objective C / C / C++ 使用的编译器前端是Clang,Swift是swift,后端都是LLVM。
3. LLVM的设计
-
传统编译器(如
CGG
)的前端和后端没有完全分离,耦合在了一起,因而如果要支持一门新的语言或硬件平台,需要做大量的工作。 -
LLVM
最重要的地方:支持多种语言
或多种硬件架构
。使用通用代码表示形式:IR
(用来在编译器中表示代码的形式) -
LLVM
可以为任何编程语言独立编写前端
,也可以为任何硬件架构独立编写后端
. -
所以
LLVM
不是一个简单的编译器,而是架构编译器
,可以兼容所有前端和后端。 -
LLVM
同时支持AOT
预先编译和JIT
即时编译
- 不同的前端后端使用统一的中间代码LLVM Intermediate Representation (LLVM IR)
- LLVM IR格式以 .ll结尾、以 .bc 的二进制格式结尾、内存格式
- Bitcode(Xcode 7之后)就是以.bc结尾的中间代码,是LLVM-IR在磁盘上的一种二进制表示形式。例如:clang -c -emit-llvm xxxx.m 生成 xxxx. bc
- 如果要转换成文本格式查看,例如:llvm-dis xxxx.bc -o xxxx.ll
- 苹果单独对 Bitcode 进行了额外的优化.
- i) 应用上传到 AppStore时,Xcode会将程序对应的 Bitcode一起上传;
- ii) AppStore会将 Bitcode重新编译为可执行程序,供用户下载;
- iii) Bitcode被Xcode打包成 xar文档,嵌入的 MachO中。
- 如果需要支持一种新的编程语言、硬件设备,那么只需要实现一个新的前后端
4. Clang简介
Clang
是LLVM项目
的一个子项目
。基于LLVM架构的轻量级编辑器,诞生之初就是为了替代GCC,提供更快的编译速度。 他是负责编译
C、C++、Objecte-C语言的编译器,它属于整个LLVM架构中的编译器前端
。
对于开发者而言,研究Clang可以给我们带来很多好处。
三 、 LLVM案例体验
-
新建一个
Mac OS
的命令行
工程: -
没有改动代码
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
库的代码: -
我们发现测试代码中的
宏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
不会被替换
使用安全拓展:
- 使用
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
。标注了位置
是第几行
的第几个字符
开始的。
Last login: Mon Nov 16 16:43:13 on ttys000
ios@HJ ~ % cd /Users/ios/Desktop/学习资料/hu/TestDemo/TestDemo
ios@HJ TestDemo % clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
annot_module_include '#import <Foundation/Foundation.h>
#define C 30
int main(int argc, const char * argv[]) {
@autoreleasepoo' Loc=<main.m:8:1>
int 'int' [StartOfLine] Loc=<main.m:11:1>
identifier 'main' [LeadingSpace] Loc=<main.m:11:5>
l_paren '(' Loc=<main.m:11:9>
int 'int' Loc=<main.m:11:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:11:14>
comma ',' Loc=<main.m:11:18>
const 'const' [LeadingSpace] Loc=<main.m:11:20>
char 'char' [LeadingSpace] Loc=<main.m:11:26>
star '*' [LeadingSpace] Loc=<main.m:11:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:11:33>
l_square '[' Loc=<main.m:11:37>
r_square ']' Loc=<main.m:11:38>
r_paren ')' Loc=<main.m:11:39>
l_brace '{' [LeadingSpace] Loc=<main.m:11:41>
at '@' [StartOfLine] [LeadingSpace] Loc=<main.m:12:5>
identifier 'autoreleasepool' Loc=<main.m:12:6>
l_brace '{' [LeadingSpace] Loc=<main.m:12: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>
numeric_constant '20' [LeadingSpace] Loc=<main.m:14:17>
semi ';' Loc=<main.m:14:19>
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 'a' [LeadingSpace] Loc=<main.m:15:22>
plus '+' [LeadingSpace] Loc=<main.m:15:24>
identifier 'b' [LeadingSpace] Loc=<main.m:15:26>
plus '+' [LeadingSpace] Loc=<main.m:15:28>
numeric_constant '30' [LeadingSpace] Loc=<main.m:15:30 <Spelling=main.m:9:11>>
r_paren ')' Loc=<main.m:15:31>
semi ';' Loc=<main.m:15:32>
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>
ios@HJ TestDemo %
3.3.2 语法分析
-
语法分析
是验证语法
是否正确
。
在词法分析的基础上,将单词
序列组合
成各类语法短语
,如“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树
(Abstract Syntax Tree,AST)。语法分析程序
判断源程序
在结构
上是否正确
。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
作用域
、类型
、运算方式
都十分清晰
。( 语法树一次只能处理一次计算。两次运算,就得多分一层级。)
-
语法分析
,就是在生成语法树
时完成检测
的。
- 头文件找不到时,可以指定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 生成汇编代码
-
完成
中间代码
的生成后,可以将代码转变
为汇编代码
了。 -
此刻我们有
image4种
不同程度的代码(源代码
->无优化IR代码
->Os优化IR代码
->bitcode优化代码
): -
分别对
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
在生成汇编代码
时,只有选择
了优化等级
,才能减少
汇编代码量
。
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
image
- 此时只是把
当前文件
编译为了机器码
,外部符号
(如printf
)无法识别。
undefined:
表示当前文件
暂时找不到符号
。
external:
表示这个符号
是外部可以访问
的。(实现
不在我这,在外部
的某个地方
)
所以当前虽转换
成了机器代码
。但是只是目标文件
,并不能
直接执行
,需要将
所有资源链接
起来,才可以执行
。
3.7 生成可执行文件(链接)
- 通过
链接器
把编译产生的.o
文件和.dylib
、.a
文件链接关联
起来,生成真正的mach-o可执行文件
clang main.o -o main // 将目标文件转成可执行文件
file main // 查看文件
xcrun nm -nm main // 查看main的符号
image
- 对比
main.o
目标文件,此时生成的main
文件:
- 从
object
文件变成了executable
可执行文件 - 虽然都有
undefined
,但是可执行文件
中指定了该符号
的来源库
。机器在运行时
,会从相应的库
中取读取
该符号
(printf
)
总结:
源代码
到可执行文件
的整个流程
:
四. LLVM源码 & 编译流程
【注意】
LLVM
源码2.29G
,编译后
文件30G
,请确保
电脑硬盘空间足够
;编译时
,电脑温度会飙升90多度
,请用空调伺候
着,可能
会黑屏
;编译时间
长达1个多小时
,请合理安排时间。
如果以上3点
,你确定能接受
,那我们就开始
吧。
4.1 LLVM下载
-
Gitee
上有已配置
好关联库
的源码
,可直接下载: https://gitee.com/mirrors/LLVM
4.2 LLVM编译
- 最新的LLVM只支持
cmake
编译,需要使用Homebrew
安装cmake
:
4.2.1 安装cmake
- 查看
brew列表
,检查是否安装过cmake
,如果有,就跳过此步骤
brew list
- 如果没有,就使用
brew安装
:
brew install cmake
如果报权限错误,可sudo chown -R
whoami:admin /usr/local/share
放开权限
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
-
成功之后,可以看到生成的
imageXcode
文件: -
打开
imageLLVM.xcodeproj
,选择自动创建Schemes
。 -
image自动创建完成
后,选择ALL_BUILD
进行编译(耗时0.5-1小时,CPU满负荷运转) -
编译完成。接下来我们开始创建插件。
网友评论