22 - 编译流程认识

282 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

主要内容:

  1. LLVM的认识
  2. 编译流程概述
  3. 编译流程的详细分析

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。

iOS编译器架构.webp

LLVM的设计理念 LVM设计的最重要方面是,使用通用的代码表示形式(IR),它是用来在编译器中表示代码的形式,所有LLVM可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端,如下所示

LLVM设计理念.webp

1.2 Clang认识

clang是LLVM项目中的一个子项目,它是基于LLVM架构图的轻量级编译器,诞生之初是为了替代GCC,提供更快的编译速度,它是负责C、C++、OC语言的编译器,属于整个LLVM架构中的编译器前端。

开发者经常会使用到Clang,这里就不再赘述了。

2、编译过程概述

平常我们无需关注编译和链接的过程,因为XCode的开发环境已经集成了编译链接过程。比如我们在Cmd+B进行构建(Build)时就包含了编译和链接的过程。 因此这里就需要详细描述下编译的过程

文件加载过程.webp

  1. 源文件:
    1. 载入.h、.m、.cpp 等文件
  2. 预处理:
    1. 也叫预编译,主要处理源文件中的以"#"开头的预编译指令
    2. 删除所有的"#define",展开宏定义;
    3. 处理条件预编译指令,比如 #if;
    4. 处于"#include"预编译指令,将被包含的文件插入到该预编译指令的位置
    5. 删除所有注释;展开头文件;
    6. 添加行号和文件名标识,以便于编译时能够使用
    7. 保留所有的#pragma编译器指令,因为编译器需要使用它们
    8. 产生.i 文件
  3. 编译
    1. 词法分析、语法分析、语义分析
    2. 还会进行一些优化,比如真值判断,假值判断
    3. 将.i 文件转换为汇编语言,产生.s 文件
  4. 汇编
    1. 将汇编文件转换为机器可以执行的指令,产生.o 文件
    2. 每一条汇编语句几乎都对应一条机器指令,这里其实得到的就是二进制文件
  5. 链接
    1. 对.o 文件中引用其他库的地方进行引用,生成最后的可执行文件MachO文件
    2. dyld 就在此处起作用,包括动态链接和静态链接。

3、编译流程的详细分析

本章通过命令具象的查看编译过程

编译流程:

命令:

clang -ccc-print-phases main.m

命令:

clang -ccc-print-phases main.m

结果:

查看LLVM编译流.webp

说明: 可以看到总共有7步,去掉第1个和第7个,编译流程总共是5个阶段

  1. 查找文件
  2. 预处理
  3. 编译,生成中间代码
  4. 进入后端,生成汇编代码
  5. 汇编后生成目标文件
  6. 链接成镜像文件,将多个镜像文件进行连接整个Macho(此时是静态的,动态库是绑定)
  7. 转换成不同架构的镜像文件

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

结果: 词法分析.webp 说明:

  • 词法分析会把代码切成一个个token,比如大小括号、等于号还有字符串等,
  • 也就是把代码中的每一个词进行截取分析

3.2.2 语法分析

命令:

clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

结果:

语法分析.webp

说明:

  • 语法分析程序判断程序在结构上是否正确,它的任务是验证语法是否正确
  • 在词法分析的基础上将单词序列组合成各类此法短语,如程序、语句、表达式 等等
  • 然后将所有节点组成抽象语法树AST(Abstract Syntax Tree)
  • 后续在修改语法检查时经常会分析这里的检查项

3.2.3 得到中间代码IR

命令:

clang -S -fobjc-arc -emit-llvm main.m

结果:

IR源码.webp

说明:

  • 执行命令会生成一个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

结果:

优化后的IR源码.webp 说明:

  • 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

结果:

汇编文件.webp

说明:

  • 生成汇编代码也可以进行优化clang -Os -S -fobjc-arc main.m -o main.s

3.4 汇编(生成目标文件)

命令:

clang -fmodules -c main.s -o main.o

结果: 目标文件.webp

说明:

  • 对汇编文件进行汇编操作就得到了目标文件(.o文件)
  • 汇编器将汇编代码转换为机器代码,最后输出目标文件(object file)

3.5 链接

命令:

clang main.o -o main

结果:

MachO文件.webp

说明:

  • 还是解释一下,我这里前面的自动释放池删掉了。(加自动释放池编译不过,我即使加了SDK路径还是没编译过,索性就先删掉处理)
  • 链接就是将其他库文件链接到本文件中,有静态库和动态库,详细的可以看这篇文章04-OC类的加载过程

最终得到的文件.webp

4、总结

LLVM编译流程.webp