一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
主要内容:
- LLVM的认识
- 编译流程概述
- 编译流程的详细分析
1、LLVM的认识
LLVM是架构编译器的框架系统,以C++编写而成,用于优化任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time)。对开发者保持开放,并兼容已有脚本。
1.1 LLVM架构认识
传统编译器设计: 源码 Source Code + 前端 Frontend + 优化器 Optimizer + 后端 Backend(代码生成器 CodeGenerator)+ 机器码 Machine Code,如下图所示
前端 Frontend: 编译器前端的任务是解析源代码(编译阶段),它会进行 词法分析、语法分析、语义分析、检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree AST),LLVM的前端还会生成中间代码(intermediate representation,简称IR),可以理解为llvm是编译器 + 优化器, 接收的是IR中间代码,输出的还是IR,给后端,经过后端翻译成目标指令集
优化器 Optimizer: 优化器负责进行各种优化,改善代码的运行时间,例如消除冗余计算等
后端 Backend(代码生成器 Code Generator): 将代码映射到目标指令集,生成机器代码,并且进行机器代码相关的代码优化
iOS的编译器架构: OC/C/C++使用的编译器前端为Clang,Swift为Swift,后端都是LLVM。
LLVM的设计理念 LVM设计的最重要方面是,使用通用的代码表示形式(IR),它是用来在编译器中表示代码的形式,所有LLVM可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端,如下所示
1.2 Clang认识
clang是LLVM项目中的一个子项目,它是基于LLVM架构图的轻量级编译器,诞生之初是为了替代GCC,提供更快的编译速度,它是负责C、C++、OC语言的编译器,属于整个LLVM架构中的编译器前端。
开发者经常会使用到Clang,这里就不再赘述了。
2、编译过程概述
平常我们无需关注编译和链接的过程,因为XCode的开发环境已经集成了编译链接过程。比如我们在Cmd+B进行构建(Build)时就包含了编译和链接的过程。 因此这里就需要详细描述下编译的过程
- 源文件:
- 载入.h、.m、.cpp 等文件
- 预处理:
- 也叫预编译,主要处理源文件中的以"#"开头的预编译指令
- 删除所有的"#define",展开宏定义;
- 处理条件预编译指令,比如 #if;
- 处于"#include"预编译指令,将被包含的文件插入到该预编译指令的位置
- 删除所有注释;展开头文件;
- 添加行号和文件名标识,以便于编译时能够使用
- 保留所有的#pragma编译器指令,因为编译器需要使用它们
- 产生.i 文件
- 编译
- 词法分析、语法分析、语义分析
- 还会进行一些优化,比如真值判断,假值判断
- 将.i 文件转换为汇编语言,产生.s 文件
- 汇编
- 将汇编文件转换为机器可以执行的指令,产生.o 文件
- 每一条汇编语句几乎都对应一条机器指令,这里其实得到的就是二进制文件
- 链接
- 对.o 文件中引用其他库的地方进行引用,生成最后的可执行文件MachO文件
- dyld 就在此处起作用,包括动态链接和静态链接。
3、编译流程的详细分析
本章通过命令具象的查看编译过程
编译流程:
命令:
clang -ccc-print-phases main.m
命令:
clang -ccc-print-phases main.m
结果:
说明: 可以看到总共有7步,去掉第1个和第7个,编译流程总共是5个阶段
- 查找文件
- 预处理
- 编译,生成中间代码
- 进入后端,生成汇编代码
- 汇编后生成目标文件
- 链接成镜像文件,将多个镜像文件进行连接整个Macho(此时是静态的,动态库是绑定)
- 转换成不同架构的镜像文件
3.1 预处理
命令:
//在终端直接查看替换结果
clang -E main.m
//内容很大,最好是生成对应的文件查看替换后的源码
clang -E main.m >> main2.m
说明:
- 执行完毕在main2.m文件中可以看到宏定义已经扩展,头文件也已经导入进来
- 注释也被删掉了
注意:
- typedef 在给数据类型取别名时,在预处理阶段不会被替换掉
- define则在预处理阶段会被替换,所以经常被是用来进行代码混淆,目的是为了app安全,实现逻辑是:将app中核心类、核心方法等用系统相似的名称进行取别名了,然后在预处理阶段就被替换了,来达到代码混淆的目的
3.2 编译
编译阶段有词法分析、语法分析、语义分析,接下来分别查看
3.2.1 词法分析
命令:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
结果:
说明:
- 词法分析会把代码切成一个个token,比如大小括号、等于号还有字符串等,
- 也就是把代码中的每一个词进行截取分析
3.2.2 语法分析
命令:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
结果:
说明:
- 语法分析程序判断程序在结构上是否正确,它的任务是验证语法是否正确
- 在词法分析的基础上将单词序列组合成各类此法短语,如程序、语句、表达式 等等
- 然后将所有节点组成抽象语法树AST(Abstract Syntax Tree)
- 后续在修改语法检查时经常会分析这里的检查项
3.2.3 得到中间代码IR
命令:
clang -S -fobjc-arc -emit-llvm main.m
结果:
说明:
- 执行命令会生成一个main.ll文件,这里面就是IR源码
- 代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR,
- 源码的阅读和汇编类似,如果懂汇编就很容易阅读了,这里就不再赘述
//以下是IR基本语法
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32bit,4个字节
store 写入内存
load 读取数据
call 调用函数
ret 返回
IR的优化:
命令:
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
结果:
说明:
- LLVM的优化级别分别是-O0 -O1 -O2 -O3 -Os(第一个是大写英文字母O)
- IR文件在OC中是可以进行优化的,一般设置是在target - Build Setting - Optimization Level(优化器等级)中设置
3.3 生成汇编代码
命令:
clang -S -fobjc-arc main.ll -o main.s
结果:
说明:
- 生成汇编代码也可以进行优化
clang -Os -S -fobjc-arc main.m -o main.s
3.4 汇编(生成目标文件)
命令:
clang -fmodules -c main.s -o main.o
结果:
说明:
- 对汇编文件进行汇编操作就得到了目标文件(.o文件)
- 汇编器将汇编代码转换为机器代码,最后输出目标文件(object file)
3.5 链接
命令:
clang main.o -o main
结果:
说明:
- 还是解释一下,我这里前面的自动释放池删掉了。(加自动释放池编译不过,我即使加了SDK路径还是没编译过,索性就先删掉处理)
- 链接就是将其他库文件链接到本文件中,有静态库和动态库,详细的可以看这篇文章04-OC类的加载过程