原文链接:http://www.cocoachina.com/special/20161130/18243.html
我看见原文是用markdown写的,但是cocoachina没有用markdown处理,所以复制在简书,方便自己阅读。
实例
实例分析当然选用最经典的helloworld了,我们可以通过终端,输出helloworld。
- 创建一个目录,同时创建文件,打开。
mkdir mach-o-helloworld
touch helloworld.c
open -e helloworld.c
- 在helloworld.c中写入代码,并保存。
#include
int main(int argc, char *argv[])
{
printf("Hello World! ");
return 0;
}
- 终端执行下面代码
xcrun clang helloworld.c -o helloworld.out
./helloworld.out
[图片上传失败...(image-be52b3-1557459281771)]
编译器
编译器主要有以下两个任务:
把OC代码转换成低级的代码
分析代码,确保其没有任何错误
XCode搭载了clang作为编译器,clang可以获取OC代码,分析,并将其转化成类似于汇编语言的低级语言的代码,LLVM Intermediate Representation, LLVM IR无系统依赖,LLVM负责构建并将其编译成指定平台的本地bytecode。这样做的好处之一就是这些构建可以在任何LLVM支持的平台上面运行。比如你写了一个iOS App,它可以自动的在Intel和ARM平台上运行,LLVM会将IR代码转成不同这些平台的bytecode。
LLVM
LLVM有三层体系
支持大量的输入语言,C, C++,等等
共享优化器,用于优化LLVM IR
-
不同的平台比如ARM,PowerPC等
[图片上传失败...(image-7a8bc8-1557459281771)]
当编译一个源文件的时候,如上图所示,其流程细化总结下来一共有这几个步骤:
输入文件
预处理
-
include的展开
宏的展开
符号化
词法分析与语义分析
将预处理的符号翻译入一个解析树
将语义分析应用到解析树
输出一个抽象语法树(AST)
代码生成和优化
将抽象语法树(AST)翻译成LLVM IR
优化生成的代码
生成目标代码
输出目标代码
汇编
将目标代码输入到一个目标文件
链接
将多个目标文件合并成一个可执行文件
预处理
当源文件进入到了预处理阶段,首先的第一件事就是处理宏,比如你写了#import
,当预处理器分析到这行的时候,会将其替换成为这个文件的内容,如果.h文件里包含其他的宏定义,也会进行替换。这就解释了为什么你的.h文件需要尽可能少的import其他头文件,而你应该采用@class的方式。
我们再弄一个例子,创建一个OC文件,helloworld.m,对其执行预处理,其命令为clang -E helloworld.m | less
,可以看到其内容为下图,代码行数为8:
[图片上传失败...(image-771bd6-1557459281771)]
如果我们在helloworld.m中引入Foundation头文件,然后用上述命令进行预处理,可以看到文件行数为
[图片上传失败...(image-5e32a5-1557459281771)]
我们可以打开预编译后的文件看下其结构,xcrun clang -E helloworld.m | open -f
,类似这样。
[图片上传失败...(image-b7e1a7-1557459281771)]
- include展开我们都用过#include和#import。它们就是告诉预处理器在#include语句的地方插入引入的.h的内容。在刚刚的文件里就是插入了一个以#开头的行标记。跟在#后面的数字是在源文件中的行号。每一行最后的数字是在新文件中的行号。接下来是系统头文件,或者extern “C”的文件。在Xcode中,你可以通过使用Product->Perform Action-> Preprocess来查看任何一个文件的预处理输出。
[图片上传失败...(image-8f3a7b-1557459281771)]
-
宏展开宏展开会直接将指定变量代码替换为宏指定的代码段,但是宏容易引发错误,很难定位,因此可以采用
static inline
,它与宏有相同的性能。
符号化
经历过预处理之后,每个.m文件都会从字符串转化成一系列的标记,我们可以使用clang生成这些标签查看,具体命令为clang -Xclang -dump-tokens helloworld.m
[图片上传失败...(image-8d2896-1557459281771)]
分析
AST生成
这步操作会将之前的符号流转化成一个AST, ObjC的复杂性就决定了分析的过程不会特别快,分析结束之后这步就会生成一个抽象语法树(AST),类似下图,抽象语法树的一个好处是能极大方便编译,分析和优化。
[图片上传失败...(image-994ac2-1557459281771)]
我们采用以下命令clang -Xclang -ast-dump -fsyntax-only hello.m
,我们会得到以下
[图片上传失败...(image-321eb9-1557459281771)]
每个AST上面节点都对应有源码的位置,所以一旦有问题,就会帮我们定位到具体的位置。
静态分析
生成语法树之后,编译器就可以通过分析语法树来进行类型检查,或者其它分析来帮助你检查错误,比如某个对象是否实现了某个方法,等等。
- 类型检查
类型检查会检查一个对象是否支持某个方法,如果不支持则会报出error,某个对象类型是否正确,编译器会爆出警告,等等。
动态类型检查:runtime中进行类型检查,用于决定是否某个对象可以响应某个方法。
静态类型检查:编译过程中进行类型检查,工程设置为ARC的时候,编译器会进行大量的类型检查。
- 其他分析
编译器还会做很多其他的分析,比如检查使用了某个变量,或者初始化方法检查等。
代码生成
经过符号化,代码分析等,编译器就会将代码生成LLVM IR。比如可以使用下面的命令来查看,clang -O3 -S -emit-llvm helloworld.m -o helloworld.ll
,生成的代码如下:
[图片上传失败...(image-e7e3-1557459281771)]
代码优化
代码生成之后就会进行代码优化,我们用菲波那切数列的递归实现为例来看这个比较,可以使用如下命令。
clang -O0 -S -emit-llvm helloworld.m -o helloworld.ll
clang -O3 -S -emit-llvm helloworld.m -o helloworld.ll
[图片上传失败...(image-9e1654-1557459281771)]
[图片上传失败...(image-1ab7a6-1557459281771)]
可以看一下递归函数已经被展开优化,这样进行代码优化之后就可以提升编译速度。
生成目标代码
可以采用xcrun clang -S -o - helloworld.c | open -f
,这样我们就可以得到汇编代码,类似这样
[图片上传失败...(image-ddb726-1557459281771)]
其中带.的为汇编指令,其它的则为汇编程序。具体内容我们不做分析。XCode通过 Product->Perform Action -> Assemble可以查看任何文件的汇编代码。
汇编
就是简单的将汇编代码转化成机器码,其创建了一个目标文件。XCode里面的.o文件都存在derived data目录下的Objects-normal文件夹。
链接
链接器主要是确定.o与libraries中间的符号问题。比如当你调用一个方法的时候,最后的执行需要知道这个方法对应的符号地址,以及这个方法在内存中的地址。连接器将会读取所有的目标文件,然后将它们编码进最后的可执行文件,然后输出最后的可执行文件。
至此我们就完成了可执行文件的生成。
可执行文件分析
Section
一个可执行文件有多个Section,每个Section都会被包含进一个Segment。当我们执行xcrun size -x -l -m helloworld.out
时候可以看到输出以下内容:
[图片上传失败...(image-7a613a-1557459281771)]
当我们执行这个可执行文件的时候,虚拟内存会映射segments到程序的内存地址,虚拟内存会通过分段或者分页等方式将可执行文件载入内存,不会一次性将所有文件全部载入。当虚拟内存做好映射后,segmengs和sections会根据权限等进行不同的映射。
__PAGEZERO,不可写,不可读,不可执行,一共4G,用于指明程序的地址空间。
__TEXT代码段,可读,可执行的,不可修改,因此它始终是干净的。
__DATA数据段,可读,可写,不可执行,我们会随时更新里面的内容。
每个Segment包含了一些Sections,不同部分具有不同的含义。
__TEXT:可以看到__text包含了复杂的机器码,__stubs和__stub_helper用于动态链接,__const用于静态变量,__cstring用于字符串变量。
__DATA:包含了读写数据,__nl_symbol_ptr以及__la_symbol_ptr,_const在__DATA段内是通用的,主要为不可变数据。_bss片段包含了没有被初始化的静态变量例如static int a;_common片段包含了被动态链接器使用的占位符片段。
Section
我们可以通过如下命令xcrun otool -v -t helloworld.out
来查看反汇编的代码,如下。
[图片上传失败...(image-2335a5-1557459281771)]
当然我们也可以查看其它片段,比如
[图片上传失败...(image-e52e61-1557459281771)]
Mach-O
OSX和iOS指明了可执行文件的格式为Mach-O
[图片上传失败...(image-a55d6f-1557459281771)]
接下来我们可以对Mach-O进行分析,使用otool可以查看Mach-o的头部信息。
[图片上传失败...(image-3bf6bd-1557459281771)]
cputype,cpusubtype指明了可执行文件的目标架构
ncmds,sizeofcmds用于命令加载,其指明了文件的逻辑结构和在虚拟内存中的布局。
我们可以看到每个segment以及每个section的位置,xcrun otool -v -l helloworld.out | open -f
[图片上传失败...(image-eaff3d-1557459281771)]
dyld
在程序中,每个函数,变量等都是通过符号的方式来展现的,当我们的可执行文件链接到目标文件的时候,链接器会寻找目标文件和动态库之间的所有符号,因此在可执行文件和目标文件中都存在符号表用来记录和保存这些符号,类似这样
[图片上传失败...(image-50c2f1-1557459281771)]
一些动态库和运行时的符号有可能是不确定的,但是符号表中会记录它们去哪里寻找动态库,而这些未定义的符号将会在运行时被dyld指定。由于一个程序会依赖大量的动态链接库,因此会有无数的符号需要指定,iOS上面使用了一个共享缓存的概念,存在着一个文件里面包含了大多数的动态链接库,其互相连接且符号已经指定,当一个Mach-O被加载的时候,首先检查共享缓存,这大大的优化了程序的加载时间。
Fish Hook
fishhook是facebook开源的用于动态方法替换的一个开源库,在iOS开发过程中,它通过动态重绑定Mach-O二进制可执行文件内的标识,完成方法替换。
工作原理
依据上文中的介绍,Mach-O的二进制文件中存在着__DATA segment,而dyld通过更新sections中的指针来绑定lazy和non-lazy的标识。fishhook通过rebind_symbols的方法来重新绑定这些标识的名字和实现。
[图片上传失败...(image-7f6db6-1557459281771)]
__DATA segment包含两个动态标识绑定相关的sections,__nl_symbol_ptr和__la_symbol_ptr,__nl_symbol_ptr是一个non-lazily绑定数据的指针数组,其在库被加载的时候就被绑定,__la_symbol_ptr也是一个指针数组,它在第一次用到标识的时候,调用dyld_stub_binder来导入方法
[图片上传失败...(image-2d3b5b-1557459281771)]
如上图所示中提供了reserved1字段用作间接符号表的偏移,间接符号表,存在于__LINKEDIT segment,它是一个符号表的索引数组,符号表的顺序与non-lazy和lazy符号section中的指针相同,所以nl_symbol_ptr在符号表中的首地址的相应位置就是间接符号表中的nl_symbol_ptr->reserved1,符号表本身就是一个结构体,如下图
[图片上传失败...(image-9d919f-1557459281771)]
每一个nlist包含一个索引,指向__LINKEDIT上的字符表,__LINKEDIT则是真正的符号名称存在的地方,所以对于每一个__la_symbol_ptr,__nl_symbol_ptr,我们都可以找到相应的标识以及通过比对标识名,我们就能找到其相应的字符串,进而可以替代section中的指针。
具体用法
具体用法如下。
[图片上传失败...(image-d62002-1557459281771)]
首先orig_free = dlsym(RTLD_DEFAULT, "free");
会将系统的free方法,指定给orig_free,当orig_free的时候就会执行原来的系统free方法。
之后rebind_symbols((struct rebinding[1]){{"free", myfree, (void *)&orig_free}}, 1);
会用myfree这个方法动态hook free方法,进而完成方法替换。