Mac-O
# 一、概述
运行时架构(runtime architecture)是针对软件运行环境定义的一系列规则,包括但不限于:
如何为代码和数据(code and data)排位; 在内存中怎样去加载或者追踪程序的部分代码; 告诉编译器应该如何组装代码; 如何调用系统服务,如加载插件; Mac 系统支持多种运行时架构,但是内核可以直接读取的可执行文件只有一种:Mach-O。因此,mac 的运行时架构也被命名为:Mach-O Runtime Architecture;因此,Mach-O 是一种存储标准,用于 Mach-O runtime architecture 架构中对程序的磁盘存储;
Mach-O 是 mach object 的缩写,在 -objc解决分类不加载的问题的官方文档中,明确指出所有的源文件都会被转化成一个 objcet,只不过最后经过链接操作,工程或被转化成静态库、动态库或者是可执行文件(类型不同的 mach-O);
Mach-O 文件分为三大部分:
文件头:
Header(头部),指明了 cpu 架构、大小端序、文件类型、Load Commands 个数等一些基本信息
命令区域:
Load Commands(加载命令),正如官方的图所示,描述了怎样加载每个 Segment 的信息。在 Mach-O 文件中可以有多个 Segment,每个 Segment 可能包含一个或多个 Section,segment 的名字都是大写的,且空间大小为页的整数。页的大小跟硬件有关,在 arm64 架构一页是 16KB,其余为 4KB。
数据区域(包括数据, 代码等等):
Data(数据区),Segment 的具体数据,包含了代码和数据等。
# 二、mach_header
header 位于 Mach-O 文件的头部,其作用是:
识别 Mach-O 的格式; 文件类型; CPU 架构信息; 64 位 header 结构体如下:
struct mach_header_64 {
uint32_t magic; /* 主要用来区分当前Mach-O所支持的CPU架构(当前只有32bit和64bit)。*/
cpu_type_t cputype; /* 主要的CPU类型(32/64bit), 以及其他的属性*/
cpu_subtype_t cpusubtype; /* arm 架构下有 arm_v7、arm_all 之类的区别,而 subtype 就是表示这个,部分定义如下: */
uint32_t filetype; /* filetype 就是我们熟知的 Mach-O 文件的类型,比如动态库、主工程生成可执行文件、bundle 等等 */
uint32_t ncmds; /* 也就是下一个segment中得segment的数量。number of load commands */
uint32_t sizeofcmds; /* 表示 header 之后的 Load Command 的段数和大小 */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
2
3
4
5
6
7
8
9
10
# Load Command 源码解读
Load Command 由多个 command 组成; command 主要有两种类型:指向具体数据、不指向具体数据; 代码层面上 load_command 结构体相当于基类,很少被使用;
# Load Command 和 segment/section 的关系
上文中讲到 Load Command 主要分为指向数据实体和不指向数据实体两种类型。
不指向数据实体的 command 主要作用是为 dyld 提供信息,而指向数据实体的 command 才是 command 和 segment/section 关系的体现;
如 LC_SEGMENT 指向具体的 segment,这个 segment 的实体部分就是 Mach-O 文件的第三部分,主要内容是代码和数据;
Mac-O加载流程
execve // 用户点击了app,用户态会发送一个系统调用 execve 到内核
▼ __mac_execve // 主要是为加载镜像进行数据的初始化,以及资源相关的操作,以及创建线程
▼ exec_activate_image // 拷贝可执行文件到内存中,并根据不同的可执行文件类型选择不同的加载函数,所有的镜像的加载要么终止在一个错误上,要么最终完成加载镜像。
// 在 encapsulated_binary 这一步会根据image的类型选择imgact的方法
/*
* 该方法为Mach-o Binary对应的执行方法;
* 如果image类型为Fat Binary,对应方法为exec_fat_imgact;
* 如果image类型为Interpreter Script,对应方法为exec_shell_imgact
*/
▼ exec_mach_imgact
▶︎ // 首先对Mach-O做检测,会检测Mach-O头部,解析其架构、检查imgp等内容,判断魔数、cputype、cpusubtype等信息。如果image无效,会直接触发assert(exec_failure_reason == OS_REASON_NULL); 退出。
// 拒绝接受Dylib和Bundle这样的文件,这些文件会由dyld负责加载。然后把Mach-O映射到内存中去,调用load_machfile()
▼ load_machfile
▶︎ // load_machfile会加载Mach-O中的各种load command命令。在其内部会禁止数据段执行,防止溢出漏洞攻击,还会设置地址空间布局随机化(ASLR),还有一些映射的调整。
// 真正负责对加载命令解析的是parse_machfile()
▼ parse_machfile //解析主二进制macho
▶︎ /*
* 首先,对image头中的filetype进行分析,可执行文件MH_EXECUTE不允许被二次加载(depth = 1);动态链接编辑器MH_DYLINKER必须是被可执行文件加载的(depth = 2)
* 然后,循环遍历所有的load command,分别调用对应的内核函数进行处理
* LC_SEGMET:load_segment函数:对于每一个段,将文件中相应的内容加载到内存中:从偏移量为 fileoff 处加载 filesize 字节到虚拟内存地址 vmaddr 处的 vmsize 字节。每一个段的页面都根据 initprot 进行初始化,initprot 指定了如何通过读/写/执行位初始化页面的保护级别。
* LC_UNIXTHREAD:load_unixthread函数,见下文
* LC_MAIN:load_main函数
* LC_LOAD_DYLINKER:获取动态链接器相关的信息,下面load_dylinker会根据信息,启动动态链接器
* LC_CODE_SIGNATURE:load_code_signature函数,进行验证,如果无效会退出。理论部分,回见第二节load_command `LC_CODE_SIGNATURE `部分。
* 其他的不再多说,有兴趣可以自己看源码
*/
▼ load_dylinker // 解析完 macho后,根据macho中的 LC_LOAD_DYLINKER 这个LoadCommand来启动这个二进制的加载器,即 /usr/bin/dyld
▼ parse_machfile // 开始解析 dyld 这个mach-o文件
▼ load_unixthread // 解析 dyld 的 LC_UNIXTHREAD 命令,这个过程中会解析出entry_point
▼ load_threadentry // 获取入口地址
▶︎ thread_entrypoint // 里面只有i386和x86架构的,没有arm的,但是原理是一样的
▶︎ //上一步获取到地址后,会再加上slide,ASLR偏移,到此,就获取到了dyld的入口地址,也就是 _dyld_start 函数的地址
▼ activate_exec_state
▶︎ thread_setentrypoint // 设置entry_point。直接把entry_point地址写入到用户态的寄存器里面了。
//这一步开始,_dyld_start就真正开始执行了。
▼ dyld
▼ __dyld_start // 源码在dyldStartup.s这个文件,用汇编实现
▼ dyldbootstrap::start()
▼ dyld::_main()
▼ //函数的最后,调用 getEntryFromLC_MAIN,从 Load Command 读取LC_MAIN入口,如果没有LC_MAIN入口,就读取LC_UNIXTHREAD,然后跳到主程序的入口处执行
▼ 这是下篇内容
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42