Mach-O 文件网上介绍的比较多,可是大多数都只是介绍了文件内的结构,并无说明为何会以这样的结构排布。经过阅读《程序员的自我修养》一书,结合 MachOView
工具,从新梳理一下 Mach-O 文件。程序员
除了 iOS 系统的 Mach-O,与之对应的还有 Windows 下的 PE
和 Linux 下的 ELF
。它们都是基于一种叫作 COFF
文件的变种,它的主要贡献是引入了“段”的机制,咱们编写的应用程序正是被以这种段的形式存储在 Mach-O 中。Mach-O 中除了包含机器代码指令和数据,还包括符号表、调试信息、字符串表等等,它们都被以段的形式存储。苹果官方描述 Mach-O 的结构以下:缓存
下面会经过这个图逐步展开分析 Mach-O 的内部细节,Mach-O 总体分为三部分:markdown
那么 Mach-O 为何要以这种“段”的形式存储呢?其实这种分段的好处有不少:架构
下面以蚂蚁财富
的可执行文件为例,经过 MachOView 来看下内部具体细节: ide
因为 Data 中段比较多这里只截取了部分。工具
Mach-O 文件头结构及相关常数被定义在 “/usr/include/mach-o/loader.h”
文件中,由于 5s 以后的版本 cpu 架构都是 64 位,这里以 64 位版本为例来看一下它的结构体定义:post
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 */
};
复制代码
单纯的看这个结构体不太好理解,下面再经过 MachOView 看一下蚂蚁财富 App 的 Header 构成: ui
Header 部分主要是对 Mach-O 文件的描述,从上图中能够看出,Header 的最后一个字段相对于文件开始的偏移量为 0x1c
,reserved 字段虽然没有值,但它仍然占用了 4 个字节,这样整个 Header 一共占用了 32
个字节。Mach-O 文件被系统装载的时候,会先读出 Header 部分,经过 Header 就能够找到 Load Commands 加载指令部分,读取到加载指令就能够加载到咱们编写的代码了。在 Header 中有个重要的字段 sizeofcmds 用来表示 Load Commands 的大小,经过它就能够找到 Load Commands 的位置。this
Load Command 是 Mach-O 文件中除了文件头之外最重要的结构,它描述了 Data 中的各个段信息,好比每一个段的段名、段的长度、在文件中的偏移、读写权限以及段的其余属性,它的位置要由 Header 的大小决定,下面是 Load Command 的起始位置和结束位置:spa
Load Command 的起始偏移和结束偏移分别为 0x20
和 0x2380
。0x20 换算成 10 进制是 32
,正好是 Header 的大小,0x2380
和 0x20
差值正好是 Header 中 Size of Load Commands 的值 0x2368
,由此也验证了 Load Command 的位置是紧随 Header 后面的。
在 MachOView 中 Load Command 的主要结构以下:
Load Command 由多个 Segment 构成,一个 Segment 包含一个或多个属性相似的 Section。关于 Segment 结构的定义也能够在 “/usr/include/mach-o/loader.h”
中找到:
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 */
};
复制代码
vmaddr
和 vmsize
是在应用程序被加载进虚拟内存用到的,在将 Mach-O 加载到虚拟内存的时候,会在虚拟内存上的 vmaddr 位置开始,取出 vmsize 大小的空间来存放这个段。然而在 Section 内就不存在这两个字段,由于这两个字段是给装载用的,这也是 Segment 和 Section 最主要的区别。
Data 中存放的全部的 Section,例如机器指令,全局变量和局部静态变量,符号表,调试信息等都会被存储到对应的 Section 中:
Mach-O 被装载的时候会经过 Segment 寻找对应的 Section,在 Load Commands 中 经过 Segment 能够直接找到每一个 Section 的位置和大小,例如上图 _text
段在文件中的偏移为 0x40E0
,这与它在 Segment 中的 offset
是一致的,以下图红框内所示:
在 loader.h
中也能找到 Section 的结构体的定义:
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 */
};
复制代码
关于 sectname
特别说明一下,它表示了这个 Section 存放了哪些信息,下面列举一些:
上面关于 Section 和 Segment 的主要区别没有细说,既然有了 Section,为何还要有 Segment ?
这块涉及到内存分页加载的概念,以前很火的抖音的二进制重排也是利用这种分页加载的机制,Mach-O 被虚拟内存加载的时候是以页为单位,在 iOS 上,一页的大小被划定为 16kb
,每一个 Section 在映射时都是系统页长度的整数倍,不足一个页的部分也会占用一个页,这样在 Section 增多后会带来大量内存碎片。Segment 是在装载的角度从新划分了 Mach-O 的各个段,对于相同权限的 Section,把它们合并到一块儿做为一个 Segment 进行映射。从目标文件连接的角度看,Mach-O 文件是按照 Section 存储的,但从装载的角度看,它是按照 Segment 划分的,到这里就应该很容易的理解上面苹果官方给出的 Mach-O 结构图了。
《程序员的自我修养》