编译过程
传统编译过程一般分为以下步骤。
源代码(source code)→ 预处理器(preprocessor)→ 编译器(compiler)→ 汇编程序(assembler)→ 目标代码(object code)→ 链接器(linker)→ 可执行文件(executables)
其中预处理主要工作是宏定义的替换和头文件的引入
编译器
简单而言,编译器的设计一般分为三部分
传统编译器(compiler)的设计一般如下图所示
LLVM编译器设计如图所示
- 前端 Frontend:前端的主要工作是解析源代码, 词法分析、语法分析、语义分析、检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree AST)。
- 优化器 Optimizer:负责优化代码,例如消除冗余。
- 后端 Backend:将代码映射到目标指令集,生成机器代码。
LLVM
底层虚拟机(Low Level Virtual Machine),LLVM是一套编译器基础设施项目,以C++写成,包含一系列模块化的编译器组件和工具链,用来开发编译器前端和后端。优化各种语言写的程序在编译过程中环节,如编译时期、链接时期、运行时期以及闲置时期。
LLVM前端
Clang是LLVM编译器工具集的前端(front-end),属于LLVM项目中的一个子项目,它是基于LLVM架构图的轻量级编译器,Clang的现在已经取代GCC,负责C、C++、OC语言的编译。Clang主要工作是输出代码对应的抽象语法树(Abstract Syntax Tree, AST),并将代码编译成LLVM Bitcode。接着在后端(back-end)使用LLVM编译成平台相关的机器语言。
LLVM中间端
LLVM的核心是中间端表达式(Intermediate Representation,IR),一种类似汇编的底层语言。
LLVM后端
LLVM后端的主要工作是将LLVM中间端表达式(IR)转换成特定目标机器代码(object code),机器代码一般由机器代码或接近于机器语言的代码组成。即存放目标代码的计算机文件,它常被称作二进制文件(binaries)。目前LLVM支持输出多种后端指令集,包括X86、PowerPC、ARM以及SPARC等。目标文件包含着机器代码(可直接被CPU执行)以及代码在运行时使用的数据,如重定位信息,如用于链接或调试的程序符号(变量和函数的名字),此外还包括其他调试信息。目标文件是从源代码文件产生程序文件这一过程的中间产物。
LLVM链接器
LLD是LLVM项目中的链接器,替换GNU系统链接器。LLD把目标文件(.o文件和 .dyld .a)链接在一起来生成可执行文件或库文件(mach-o文件)。
Mach-O文件格式
Mach-O 是 iOS 可执行文件的格式,典型的 Mach-O 是主二进制和动态库。Mach-O 可以分为三部分:
- Header
- Load Commands
- Data
Header 的最开始是 Magic Number,表示这是一个 Mach-O 文件,除此之外还包含一些 Flags,这些 flags 会影响 Mach-O 的解析。
Load Commands 存储 Mach-O 的布局信息,比如 Segment command 和 Data 中的 Segment/Section 是一一对应的。除了布局信息之外,还包含了依赖的动态库等启动 App 需要的信息。
Data 部分包含了实际的代码和数据,Data 被分割成很多个 Segment,每个 Segment 又被划分成很多个 Section,分别存放不同类型的数据。
标准的三个 Segment 是 TEXT,DATA,LINKEDIT,也支持自定义:
- TEXT,代码段,只读可执行,存储函数的二进制代码(__text),常量字符串(__cstring),Objective C 的类/方法名等信息
- DATA,数据段,读写,存储 Objective C 的字符串(__cfstring),以及运行时的元数据:class/protocol/method…
- LINKEDIT,启动 App 需要的信息,如 bind & rebase 的地址,代码签名,符号表…
iTunes Connect 会对上传 Mach-O 的 TEXT 段进行加密,防止 IPA 下载下来就直接可以看到代码。这也就是为什么逆向里会有个概念叫做“砸壳”,砸的就是这一层 TEXT 段加密。iOS 13 对这个过程进行了优化,在博文中的page fault过程中,需要读取Page In 的时候不需要解密了。
参考文章
http://www.aosabook.org/en/llvm.html
抖音品质建设 - iOS启动优化《原理篇》