重拾iOS-编译原理

image

关键词:LLVM,Clang,Swiftc,IR,preprocessor,Mach-O,dyld

编译器

把一种编程语言(原始语言)转换为另一种编程语言(目标语言)的程序叫做编译器.

大多数编译器由两部分组成: 前端和后端.

前端负责词法分析,语法分析,生成中间代码;

后端以中间代码作为输入,进行行架构无关的代码优化,接着针对不同架构生成不同的机器码。

前后端依赖统一格式的中间代码(IR), 使得前后端可以独立的变化. 新增一门语言只需要修改前端, 而新增一个CPU架构只需要修改后端即可. Objective C/C/C++使用的编译器前端是clang, swift是swift, 后端都是LLVM.

一、LLVM

LLVM的核心库提供了现代化的source-target-independent优化器和支持诸多流行CPU架构的代码生成器. Clang 和 LLDB都是基于LLVM衍生的子项目.

二、Clang

Clang是C语言家族的编译器前端,诞生之初是为了替代GCC,提供更快的编译速度。一张图了解clang编译的大致流程:

image

大致看来, Clang可以分为一下几个步骤:

预处理 -> 词法分析 -> 语法分析 -> 静态分析 -> 生成中间代码和优化 -> 汇编 -> 链接

1、预处理(preprocessor)

预处理会进行如下操作:

1)头文件引入, 递归将头文件引用替换为头文件中的实际内容, 所以尽量减少头文件中的#import, 使用@class替代, 把#import放到.m文件中.

2)宏替换, 在源码中使用的宏定义会被替换为对应#define的内容, 不要在需要预处理的代码中加入太多的内联代码逻辑.

3)注释处理, 在预处理的时候, 注释被删除

4)条件编译, (#if, #else, #endif)

2、词法分析(lexical anaysis)

这一步把源文件中的代码转化为特殊的标记流. 词法分析器读入源文件的字符流, 将他们组织称有意义的词素(lexeme)序列,对于每个词素,此法分析器产生词法单元(token)作为输出.

源码被分割成一个一个的字符和单词, 在行尾Loc中都标记出了源码所在的对应源文件和具体行数, 方便在报错时定位问题. 类似于下面:

int 'int'     [StartOfLine]    Loc=<main.m:14:1>
identifier 'main'     [LeadingSpace]    Loc=<main.m:14:5>
l_paren '('        Loc=<main.m:14:9>
int 'int'        Loc=<main.m:14:10>
identifier 'argc'     [LeadingSpace]    Loc=<main.m:14:14>
comma ','        Loc=<main.m:14:18>
char 'char'     [LeadingSpace]    Loc=<main.m:14:20>
star '*'     [LeadingSpace]    Loc=<main.m:14:25>
3、语法分析(semantic analysis)

词法分析的Token流会被解析成一颗抽象语法树(abstract syntax tree - AST). 在这里面每一节点也都标记了其在源码中的位置.

有了抽象语法树,clang就可以对这个树进行分析,找出代码中的错误。比如类型不匹配,亦或Objective C中向target发送了一个未实现的消息.

AST是开发者编写clang插件主要交互的数据结构,clang也提供很多API去读取AST.

4、静态分析(CodeGen)

把源码转化为抽象语法树之后,编译器就可以对这个树进行分析处理。静态分析会对代码进行错误检查,如出现方法被调用但是未定义、定义但是未使用的变量等,以此提高代码质量. 也可以使用 Xcode 自带的静态分析工具(Product -> Analyze).

常见的操作有:

1)当在代码中使用 ARC 时,编译器在编译期间,会做许多的类型检查. 最常见的是检查程序是否发送正确的消息给正确的对象,是否在正确的值上调用了正常函数。如果你给一个单纯的 NSObject* 对象发送了一个 hello 消息,那么 clang 就会报错,同样,给属性设置一个与其自身类型不相符的对象,编译器会给出一个可能使用不正确的警告.

一般会把类型分为两类:动态的和静态的。动态的在运行时做检查,静态的在编译时做检查。以往,编写代码时可以向任意对象发送任何消息,在运行时,才会检查对象是否能够响应这些消息。由于只是在运行时做此类检查,所以叫做动态类型。

至于静态类型,是在编译时做检查。当在代码中使用 ARC 时,编译器在编译期间,会做许多的类型检查:因为编译器需要知道哪个对象该如何使用。

2)检查是否有定义了,但是从未使用过的变量.

3)检查在 你的初始化方法中中调用 self 之前, 是否已经调用 [self initWith…] 或 [super init] 了.

此处遍历语法树,最终生成LLVM IR代码。LLVM IR是前端的输出,后端的输入. Objective C代码在这一步会进行runtime的桥接:property合成,ARC处理等

  • LLVM 会去做些优化工作, 在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass.
  • 如果开启了 Bitcode 苹果会做进一步的优化. 虽然Bitcode仅仅只是一个中间码不能在任何平台上运行, 但是它可以转化为任何被支持的CPU架构, 包括现在还没被发明的CPU架构. iOS Apps中Enable Bitcode 为可选项, WatchOS和tvOS, Bitcode必须开启. 如果你的App支持Bitcode, App Bundle(项目中所有的target)中的所有的 Apps 和 frameworks 都需要支持Bitcode.
5、生成汇编指令

LLVM对IR进行优化后,会对代码进行编译优化例如针对全局变量优化、循环优化、尾递归优化等, 然后会针对不同架构生成不同的目标代码,最后以汇编代码的格式输出.

6、汇编

在这一阶段,汇编器将上一步生成的可读的汇编代码转化为机器代码。最终产物就是 以 .o 结尾的目标文件。使用Xcode构建的程序会在DerivedData目录中找到这个文件.

Tips:什么是符号(Symbols)? 符号就是指向一段代码或者数据的名称。还有一种叫做WeakSymols,也就是并不一定会存在的符号,需要在运行时决定。比如iOS 12特有的API,在iOS11上就没有.

7、链接

目标文件(.o)和引用的库(dylib,a,tbd)链接起来, 最终生成可执行文件(mach-o), 链接器解决了目标文件和库之间的链接.

这时可执行文件的符号表信息已经有了, 会在运行时动态绑定.

8、Mach-O文件

Mach-O是OS X中二进制文件的原生可执行格式,是传送代码的首选格式。可执行格式决定了二进制文件中的代码和数据读入内存的顺序。代码和数据的顺序会影响内存使用和分页活动,从而直接影响程序的性能.

Mach-O是记录编译后的可执行文件,对象代码,共享库,动态加载代码和内存转储的文件格式。不同于 xml 这样的文件,它只是二进制字节流,里面有不同的包含元信息的数据块,比如字节顺序,cpu 类型,块大小等。文件内容是不可以修改的,因为在 .app 目录中有个 _CodeSignature 的目录,里面包含了程序代码的签名,这个签名的作用就是保证签名后 .app 里的文件,包括资源文件,Mach-O 文件都不能够更改.

Mach-O结构

Mach-O 文件包含三个区域:

Mach-O Header: 包含字节顺序,magic,cpu 类型,加载指令的数量等.

Load Commands: 包含很多内容的表,包括区域的位置,符号表,动态符号表等。每个加载指令包含一个元信息,比如指令类型,名称,在二进制中的位置等.

Data: 最大的部分,包含了代码,数据,比如符号表,动态符号表等.

Mach-O文件的结构如下:

image

Header

保存了Mach-O的一些基本信息,包括了平台、文件类型、LoadCommands的个数等等.

使用otool -v -h a.out查看其内容:

image

Load commands

这一段紧跟Header,加载Mach-O文件时会使用这里的数据来确定内存的分布

Data

包含 Load commands 中需要的各个 segment,每个 segment 中又包含多个 section。当运行一个可执行文件时,虚拟内存 (virtual memory) 系统将 segment 映射到进程的地址空间上.

使用xcrun size -x -l -m a.out查看segment中的内容:

image
  • Segment __PAGEZERO。
    大小为 4GB,规定进程地址空间的前 4GB 被映射为不可读不可写不可执行。

  • Segment __TEXT。
    包含可执行的代码,以只读和可执行方式映射。

  • Segment __DATA。
    包含了将会被更改的数据,以可读写和不可执行方式映射。

  • Segment __LINKEDIT。
    包含了方法和变量的元数据,代码签名等信息。

9、dyld动态链接

生成可执行文件后就是在启动时进行动态链接了, 进行符号和地址的绑定. 首先会加载所依赖的 dylibs,修正地址偏移,因为 iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类,最后执行 load 方法和 clang attribute 的 constructor 修饰函数.

10、dSYM

在每次编译后都会生成一个 dSYM 文件,程序在执行中通过地址来调用方法函数,而 dSYM 文件里存储了函数地址映射,这样调用栈里的地址可以通过 dSYM 这个映射表能够获得具体函数的位置。一般都会用来处理 crash 时获取到的调用栈 .crash 文件将其符号化.

当release的版本 crash的时候,会有一个日志文件,包含出错的内存地址, 使用symbolicatecrash工具能够把日志和dSYM文件转换成可以阅读的log信息,也就是将内存地址,转换成程序里的函数或变量和所属于的 文件名.

相关参考

  1. iOS编译原理
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343