一、概述
运行时架构(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 文件分为三大部分:
- mach-header;
- load commands;
- segment and section;
二、mach_header
header 位于 Mach-O 文件的头部,其作用是:
- 识别 Mach-O 的格式;
- 文件类型;
- CPU 架构信息;
64 位 header 结构体如下:
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
1. magic
一个整数,用于标识该文件为 Mach-O 类型。可以理解成多种类型的文件会被加载,而该 Image 如果值为特定的值,则该 Image 为 Mach-O 类型。
另外,如果该 Mach-O 的架构和编译该 Mach-O 文件的 CPU 字节序(大小端)一致,则使用 MH_MAGIC
,相反则使用 MH_CIGAM
;
32 和 64 位为固定的值:
/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */
如 dyld 源码中使用这个字段来判断是否为 Mach-O 文件:
2. cputype
一个整数,标志该文件将被使用在何种 CPU 架构上;
定义在如下文件中:
部分 type 如下:
3. subtype:
arm 架构下有 arm_v7、arm_all 之类的区别,而 subtype 就是表示这个,部分定义如下:
4. filetype
filetype 就是我们熟知的 Mach-O 文件的类型,比如动态库、主工程生成可执行文件、bundle 等等,部分 type 如下:
举个例子:
如上图,主工程生成的可执行文件就是 MH_EXECUTE、动态库则为 MH_DYLIB、ViewController.o 则为 MH_OBJECT,而 dyld 链接器 则为 MH_DYLINKER;
需要注意的是,静态库只是一个 mach-o object 的集合:
关于 fat 的格式和静态库为什么没有 header,暂时不深究???
5. ncmds && sizeofcmds
表示 header 之后的 Load Command 的段数和大小;
实例:
看看 CoreAutoLayout 动态库的 Mach-O 文件:
ps:后文会有 ncmds 在 fishhook 中的使用;
三、Load Command
1. Load Command 作用概述
其作用有:
- Mach-O 文件的布局;
这一点和 Mach-O 本身的设计有关,Load Command 本身不包含数据,Load Command 中的 segment 和section 类似于一个指针的作用,其描述(指向)的 segment 或者 section 实体才是真正存储数据或代码的地方。
- 链接信息;
这一点主要是通过几个段(LC_SYMTAB、LC_LOAD_DYNLINER __Linkedit 等) 来描述符号表相关的信息,链接器位置等。dyld 通过这些信息进行符号表的 rebase 和 bind 等操作;
- Mach-O 文件在虚拟内存中的初始化布局;
这一点应该跟 __PAGEZERO 有关,具体??待补充
- 符号表的位置;
是链接信息的一部分,主要由 LC_SYMTAB、LC_DYSYMTAB、__LINKEDIT 来描述符号表、动态符号表、字符串表的位置;
- 程序 main 线程的初始执行状态;
这里指的应该是 LC_MAIN 段描述的程序的入口函数位置;
- 主工程所导入的共享库信息;
这一点就不多说了,在 machOView 中可以直观的看到,也可以通过 otool 指令来获取;
2. Load Command 的理解
以上是官方文档对 Load Command 的表述,这里加上自己的理解。
Load Commands 由多个 command 组成,其大小由 command 的数量和 command 的 size 决定。Load Commands 更多的是一个统称的概念;
如果 Load Command 按照是否指向数据实体来分类,分为两种:
- 指向具体数据段
该种 command 存储了一些信息,且指向 Data 部分的具体数据。
如 LC_SEGMENT(segment_command) 指向存放函数代码的 __TEXT 段,程序员打交道最多的 __DATA / __DATA_CONST 段;
再比如 LC_CODE_SIGNATURE 指向 Data 中的签名数据:
再比如动态链接相关的 __LINKEDIT 对应的 command 指向 Data 区域的 __LINKEDIT 段;
- 不指向具体数据段
该种 command 一般不包含数据实体,只起到描述性作用。
不像 LC_SEGMENT 一般会指向一个 SEGMENT,比如 __TEXT。而 LC_MAIN、LC_RPATH 等这些 command 都只是告诉 dyld 一些信息,不指向具体的数据段。常见的 command 如下在会问会有列举;
3. Load Command 源码解读
接着,再说说 load_command 在代码层面上的表现。
代码层面上,command 的基本结构体为:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
这个结构体相当于一个基类。类似的结构体还有很多,比如:dysymtab_command、segment_command 等等,都包含 cmd 和 cmdsize;
因为 load_command 包含的信息太少,编码时不好用,所以在代码层面上被使用更多的是 LC_SEGMENT 对应的结构体和其他类型的结构体,如:
LC_SEGMENT:
非 LC_SEGMENT 的 command 结构体如下:
LC_SYMTAB:
其他的还有 dysymtab_command
、dylinker_command
等等,可以自行在源码中查看。
举个例子:
这里以 fishhook 源码来举个实例,看如下代码:
// 定位到 LC 其实位置
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
// 遍历LC中的所有command,找出__LINKEDIT、LC_SYMTAB、LC_DYSYMTAB
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
// 这里先直接强转成 segment_command ,因为比load_command 更好用
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
// __LINKEDIT是segment_command类型不需要再强转
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
// LC_SYMTAB是symtab_command类型需要强转
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
// LC_DYSYMTAB是dysymtab_command类型需要强转
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
上述代码是 fishhook 中寻找 __LINKEDIT 段、LC_SYMTAB、LC_DYSYMTAB 的代码,其中 __LINKEDIT 对应的类型就是 segment_command,LC_SYMTAB 对应的是 symtab_command 类型,LC_DYSYMTAB 对应的是 dysymtab_command 类型;
总结:
- Load Command 由多个 command 组成;
- command 主要有两种类型:指向具体数据、不指向具体数据;
- 代码层面上 load_command 结构体相当于基类,很少被使用;
4. Load Command 和 segment/section 的关系
上文中讲到 Load Command 主要分为指向数据实体和不指向数据实体两种类型。
不指向数据实体的 command 主要作用是为 dyld 提供信息,而指向数据实体的 command 才是 command 和 segment/section 关系的体现;
如 LC_SEGMENT 指向具体的 segment,这个 segment 的实体部分就是 Mach-O 文件的第三部分,主要内容是代码和数据;
延伸官方的图片,绘制如下:
如上图, LC_SEGMENT 类型的 command 指向具体的 section data。常见的 segment_command 一般也就几个:__TEXT、__DATA、__DATA_CONST、__LINKEDIT、__PAGEZERO;
_TEXT、__DATA、__DATA_CONST 这三个不用赘述了,指向代码、数据、常量区等;
这里其实可以很简单的理解成大数据都放在 Data 中并在 command 中添加相关的信息,使用时可以很方便的找到。小数据则直接存放在 command 中(再大你也放不下啊)。这里的设计思想和索引/目录的思想很类似,Load Command 就相当于目录;
总结:
- __LINKEDIT 指向存放 link 操作必要的数据段,是链接操作奠基石般的存在;
- 非数据类型的 command 用于未 dyld 提供简短的信息;
- 数据类型的 command 在提供信息的同时,指向了 Data 段具体的数据/代码;
- 具体的数据使用 segment 和 section 进行分段和分组;
5. __LINKEDIT是否属于段
__LINKEDIT 也属于 segment, command 指向 __LINKEDIT 这个段。只是在 machOView 软件上没有体现:
而使用 image lookup memory 是可以看到的:
另外,代码层面上也有体现:
从上图可以看到,很多 command (LC_CODE_SIGNATURE等)都是用了 linkedit_data_command 这个结构体。而其中的 dataoff 则描述了对应数据在 __LINKEDIT 中的位置;
而 __LINKEDIT 这个 command 使用了 LC_SEGMENT ,对应着 segment_command 这个结构体。所以,这个 linkedit_data_command 更像是一个补充的作用。
四、segment 和 section
1. segment
segment 命令在 64 位下的结构体:
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
解读:
其实关于 segment 和 section 在上文中基本讲的差不多了,一言以蔽之:
- Data 段的代码是一团一团(blob)的,而 segment 和 section 以段和组的维度指向 Data 区域的数据或代码的实体,利于寻址和使用;
2. segment 和 section的关系
segment 相当于一个数组,section 相当于数组中的元素;
这里需要注意的是,segment_command 中的 nsects 。该字段起到了数组的作用,用于 section 的寻址。这个数组是采用(数量 + 大小)的方式来直接获取对应的地址,从而获取到对应的 section 。
其实这种方式在 Mach-O 文件中很常见。比如 Header 后面跟的就是 Load Command, Load Command 地址 = Header 的初始地址 + Header.size ,这也是为什么 Header 结构体中包含 load commands 的个数,而 segment 结构体又包含 section 的个数的原因,fishhook 源码中有体现:
- 计算load command的初始位置
// 计算load command的初始位置
// header 是一个地址,指向这个 mach-O object 的初始位置
// 头部是一个Header(mach_header_t结构体),紧接着是Loac Command
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
- 遍历 Load Command
// ncmds 表示load command 的个数
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
// .....
}
- 遍历 segment 中的 section
// nsects为number of sections
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
// ......
}
3. section
section 以“组”的维度指向 Data 部分中的数据。在 64 位架构中的结构体:
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
上文说了 segment 相当于一个数组,section 相当于数组中的元素。但是有一点需要注意:segment 本身是会存储一些信息的,其实这一点在 Mach-O 文件中也可以看到:
- segment 初始地址 ≠ 第一个 section header 的地址;
即:
- segment 中不仅像数组一样描述了该 segment 包含的 section,还存储了 segment 段的一些信息;
4. 数据在 Data 的表现形式
再次强调一下,在 Data 中只有数据/代码,并没有描述信息,只有数据/代码。
如下可以看到 Data 部分中的某个 section 初始地址 = 第一个 section 的地址:
即:
- Data 部分的数据/代码是一团一团的纯数据按顺序排列,没有描述信息,segment 和 section 是一个统筹的概念;
来张图吧:
总结:
- 数据和代码都是一坨一坨的存储在 Data 中;
- segment 和 section 按照两个维度划分了 Data 部分,并描述了相关的信息;
5. 为什么要有 segment 和 section
从上文看,Data 中的数据都是一团一团的二进制,Mach-O 为此区分出了 section 和 segment。section 好理解,相似类型或者相同作用的数据作为一组数据嘛~~
比如懒加载符号都在 __la_symbol_ptr 这个 section 中,非懒加载符号都在 __got 这个 section 中,代码都在 __text 这个 section 中,桩函数都在 __stub 中,桩函数的包装函数都在 __stub_helper 中,这样不就得了?为什么还要个 segment???
先说结论:
- 功能细化,segment 负责内存对齐以及保持 section 相对位置不变。section 则只管数据/代码的存储;
怎么解释呢?这里其实分为两点:
- segment 和内存对齐;
- 位置相对不变;
首先说内存对齐,官方文档描述如下:
即:segment 中的数据都会被印射到虚拟内存中,所以 segment 是按页对齐的。
即:segment 中的数据在虚拟内存中占得大小要比在磁盘中所占大小更大。
比如 __PAGEZERO 段,因为没有数据,所以在磁盘中不占内存,但是在虚拟内存中占一个页的内存。
这里需要解释一下,__PAGEZERO 在 Load Command 中还是会占据少许磁盘空间的,即一个 command 结构体的大小。但是其描述的 segment 位于 Data 段,因为没有具体数据,所以在磁盘中不占空间,即为 0;当 __PAGEZERO 动态链接器加载时,因为是 segment,所以要按页对齐,最少分配一个 Page,所以虽然没有数据,但是仍然占据了一个 Page;
至此我们知道 segment 在内存中是需要按照一定规则对齐的,以此实现 I/O 或者 CPU 指令的优化;
再说说 section 的位置相对不变。
假设只有 section,那么内存对齐之后,section 如果未占满一页,那么该 section 后面的数据会留白,而在对齐之前,下一个 section 是紧跟着上一个 section 的。对齐之后,后面的 section 的位置就会发生变化。
这就是为什么 segment command 既有 vmaddr 又有 fileoff ,而 section 只有 fileoff(如symoff、stroff);
也就是说,section 只记录相对于磁盘中文件初始位置的偏移,而 segment 已经根据对齐原则,算好了在虚拟内存中位置。如果是 segment 对齐后补 0,因为是补在最末尾,所以对当前 segment 中所有的 section 完全没有影响,影响的只是下一个 segment 的位置,如下图:
即:使用 section 来记录 vmaddr 理论上也是可以实现,但是相对复杂,而且功能划分不够明确,设计感更糟糕;
dyld 和 fishhook 中计算动态链接相关表的位置的公式就是基于 segment 的 vmaddr 和 fileoff 来计算基地址,最后加上 section 中的 fileoff,详见(fishhool原理分析)[https://www.jianshu.com/p/c856f5cbbadb]
五、常见的 command
六、常见的 segment
常见的 segment 如下:
- __PAGEZERO;
- __TEXT;
- __DATA;
- __DATA_CONST;
- __LINKEDIT;
其实还有 __OBJC 、__IMPORT 等,具体定义在 loader.h 中,定义了常见的 segment 和 section:
注释中也说明了,这些 segment name 和 section name 对于链接器而言没有什么意义。但是为了支持传统的 UNIX 可执行文件,需要链接器和汇编器使用约定的名称;
所以,不需要纠结有哪些 segment,只需要关注几点:
- command 分为指向具体的数据和不指向具体数据两种类型;
- section 指向 data 中一团一团的数据,segment 整合 section,在虚拟内存的加载时,屏蔽掉分页对 section 位置的影响;