Mach-O文件结构是什么?

摘要:Mach-O文件是Apple各种编译中间产物(比如.o)和最终编译产物(比如可执行二进制)使用的文件格式。 熟悉Mach-O格式,对于我们进行一些底层调试有帮助。 1 整体结构 Mach-O的整体结构如下: 2 Header Mach-O最
Mach-O文件是Apple各种编译中间产物(比如.o)和最终编译产物(比如可执行二进制)使用的文件格式。 熟悉Mach-O格式,对于我们进行一些底层调试有帮助。 1 整体结构 Mach-O的整体结构如下: 2 Header Mach-O最顶部是头信息。 头信息定义了这个Mach-O的基本信息,比如这个Mach-O使用的CPU架构、文件类型等。 头信息定义在XNU源码macho/loader.h中: 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 */ }; magic magic使用的常量为MH_MAGIC_64和MH_CIGAM_64。 对这两个值常见的一个误解是,以为MH_MAGIC_64代表Mach-O使用大端字节序,MH_CIGAM_64使用小端字节序。 而实际上是,如果Mach-O使用的字节序和后面cputype指定的CPU使用的字节序一样,那么就是MH_MAGIC_64。 反之,如果Mach-O使用的字节序和后面cputype指定的CPU使用的字节序相反,那么就是MH_CIGAM_64。 cputype cputype表示这个Mach-O文件要运行的CPU类型,比如是ARM64还是X86_64。 注意到这个字段的类型是cpu_type_t,它的定义如下: // mach/machine.h typedef integer_t cpu_type_t; typedef integer_t cpu_subtype_t; typedef integer_t cpu_threadtype_t; // mach/arm/vm_types.h typedef int integer_t; 从上面代码可以看到,cpu_type_t实际就是一个int类型。 cpusubtype 定义了CPU的子类型,比如CPU_SUBTYPE_ARM64_ALL。 filetype 常见的值如下: MH_OBJECT表明当前是一个.o文件。 MH_EXECUTE表明当前是一个可执行文件。 MH_DYLIB表明当前是一个动态链接库。 MH_PRELOAD这个类型已经废弃了。 MH_CORE表明当前是一个core文件。 程序崩溃后产生core文件,后续可以直接调试core文件定位问题,但是iOS应用不会产生这个文件类型。 MH_DYLINKER表明当前是一个动态链接器,dyld就是这个类型。 MH_DSYM表明当前是一个.dsym符号文件。 ncmds 文件后后面LC_Command的个数 sizeofcmds 所有LC_Command占用的字节数大小。 flags 常见的flags值如下: MH_NOUNDEFS表明当前Mach-O内没有未定义的引用。 MH_DYLDLINK表明当前Mach-O只能作为动态连接器的输入,不能再进行静态链接,可执行文件有这个标志。 MH_TOWLEVEL表明当前Mach-O使用二级命名空间,也就是说每一个外部符号都会记录它来自哪个库,避免名称冲突。 MH_PIE表明当前Mach-O会使用ASLR。 3 Load Command 紧接着Mach-O头信息的是一系列Load Command。 Load Command的种类很多,所有的Load Command开头的结构都是一样的: // mach-o/loader.h struct load_command { uint32_t cmd; /* type of load command */ uint32_t cmdsize; /* total size of command in bytes */ }; cmd 表明当前Load Command的类型。 cmdsize 不同的Load Command大小的计算方式不一样。 但是无论如何,对于64bit机器上的Load Command,需要8bytes对齐。 下面就来看下常见的Load Command。 3.1 segment_command_64 segment_command_64定义了一个段Segment在Mach-O中的偏移,以及这个段加载到虚拟内存后的地址: // 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 */ }; cmd 设置为LC_SEGMENT_64。 cmdsize 当前这个Load Command的大小,计算方式为: cmdsize = sizeof(segment_command_64) + sizeof(section_64) * nsects 从计算公式上可以看到,LC_SEGMENT_64的大小除了包含自身结构体,还包含它下面的节section_64结构体的大小。 segname 段名,按照约定,段名都得是大写,比如__TEXT。 vmaddr 当前段加载到虚拟内存后的地址,由于ASLR的存在,实际地址为vmaddr + ASLR。 vmsize 段Segment所占用的虚拟内存的大小,必须对齐内存页。 对于iOS,内存页的大小为16KB。 也就是说,段Segment的vmaddr最低4bit必须是0。 fileoff LC_SEGMENT_64对应的段Segment在Mach-O文件中的偏移量。 filesize LC_SEGMENT_64对应的段Segment在Mach-O文件中占用的磁盘大小。 maxprot LC_SEGMENT_64对应的段Segment在内存中最大的保护设置,比如只读VM_PROT_READ。 initprot LC_SEGMENT_64对应的段Segment在内存中的初始保护设置。 nesects LC_SEGMENT_64对应的段Segment中包含的节section的数量。 flags 通常为0。 一个Mach-O文件中,包含的常见Segment如下: __TEXT段Segment包含程序代码或者一些只读数据,比如C字符串。 __DATA段Segment包含程序数据。 __LINKDIT段Segment包含符号表、字符串表、间表等。 在一个.o文件中,所有的section_64都位于一个匿名LC_SEGMENT_64下面: 静态连接器最后会将不同的section放到对应的Segment。 3.1.1 section_64 一个LC_SEGMENT_64结构后面,可能会跟着0个或者多个section_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 */ }; sectname 节名,按照约定,节名都是小写字母,比如__text。 segname 节所在的段名。 addr 节被加载到虚拟内存后的地址,实际地址为addr + ASLR。 size 节所占用的虚拟内存大小。 对于一些特殊的节比如__bss,可能磁盘占用大小为0,但是内存大小size不为0。 offset 当前节在Mach-O文件中的偏移量。 align 节的内存对齐要求,如果值为3,代表是2^3,也就是8字节对齐。 reloff 与重定位Relocation有关。 在.o中会有一个重定位表,refloff表示重定向表中,第一个属于这个节需要定位的项在Mach-O中的偏移。 也就是说,通过这个字段,可以在重定位表中找到这个节所有需要重定位的项。 我们可能会经常遇到重定位(Relocation)、重基址(Rebase)和绑定(Bind)。 这三者的区别是: 重定位(Relocation)发生在静态链接期间,由静态连接器ld完成。 一个.o文件会调用另一个.o文件中函数,编译期间前者并不知道后者的正确地址,只会使用一个占位地址。 静态链接器ld在合并多个.o文件成为可执行文件时,将上面的占位地址替换成正确的地址。 重基址(Rebase)发生在可执行文件加载时,由动态链接器dyld完成。 其实就是因为ASLR,dyld需要对可执行文件中的地址进行重新修复。 绑定(Bind)由动态链接器dyld完成,它将可执行文件中指向外部动态库中函数的占位地址替换成真正的地址。 nreloc 表示这个节需要重定向的个数。 flags 这个字段被分成了2部分。 低8bit定义了这个节的类型,高24bit定义了这个节的属性。 常见 Section 类型 S_REGULAR 表示这节是一个普通的节,比如__TEXT,__text。 S_ZEROFILL 表示这个节初始时会被填入0,比如__DATA,__bss。 S_CSTRING_LITERAL 表示这个节只包含C字符串。 S_LITERAL_POINTERS 表示这个节只包含常量指针,比如__DATA,__objc_selrefs。 S_LAZY_SYMBOL_POINTERS 表示这个节包含的都是延迟绑定的指针,也就是只有第一次访问时才由动态连接器dyld绑定真正的地址。 之所以需要动态连接器dyld绑定,是因为动态库的存在。 如果一个可执行文件调用了某个动态库中的外部函数,比如系统C库中的print函数,这个外部函数print的地址在可执行文件编译链接期间是无法知道的。 这是因为动态库在每次操作系统加载它时,位于虚拟内存中的地址是不固定的。 因此,静态连接器ld在链接期间只能给print函数一个占位地址,并且标记它需要在运行时由动态连接器dyld绑定。 由于动态连接器dyld绑定的过程需要进行符号查找,为了加快可执行App启动速度,就产生了延迟绑定技术。 但是在iOS >= 15上由于dyld使用Chained Fixup技术,已经取消了延迟绑定,都是非延迟绑定。 非延迟绑定,就是在App启动时,所有外部地址都已经由dyld绑定好了,不用等到第一次访问这个外部地址。 S_NON_LAZY_SYMBOL_POINTERS 表示这个节包含的都是非延迟绑定的指针,这些指针的地址在启动时就已经由动态链接器dyld绑定好了。 S_SYMBOL_STUBS 表示这个节只包含Stubs函数,比如__TEXT,__stubs。 每一个Stubs函数都是一段很简单的汇编代码,与延迟绑定有关。 每个Stubs函数都会获取一个S_LAZY_SYMBOL_POINTERS节中需要延迟绑定的指针,然后跳转到__TEXT,__stub_helper节中的汇编函数,调用dyld来进行符号绑定。 但是,在iOS >= 15上由于已经取消了延迟绑定,已经没有__TEXT,__stub_helper节了。 因此,在iOS >= 15上,Stubs函数都是非延迟绑定的,直接跳转到对应的外部函数。 S_COALESED 用于处理重复符号的定义,确保在静态链接后,多个.o合并之后,只有一份代码。 举个例子,在C++中不同源文件可能对同一个模版进行相同的实例化,这样导致编译后不同.o包含多个重复的代码。 将它们标记为S_COALESED后,静态连接器就会进行合并(Coalesed),只会保留一份代码。 常见 Section 属性 S_ATTR_PRUE_INSTRUCTIONS 表示本节包含的只有可执行代码,比如__TEXT,__text。 S_ATTR_SOME_INSTRUCTIONS 表示本节包含有可执行代码。 S_ATTR_NO_DEAD_STRIP告诉静态链接器ld,不管本节内容有没有被引用到,都不能被删除。 S_ATTR_LIVE_SUPPORT 告诉静态链接器ld,只有当本节引用的某些代码是"活"的,它自己才存活,否则就会被删除掉。 S_ATTR_STRIP_STATIC_SYMS 告诉静态连接器ld,移除由static定义的静态符号。因为它们只在当前文件中可见,移除后可以减少符号表体积。 reserved1 这个字段通常是0,只在某些特别的节有用: S_SYMBOL_STUBS节,比如__TEXT,__stubs; S_LAZY_SYMBOL_POINTERS节,比如__DATA,__la_symbol_ptr; S_NON_LAZY_SYMBOL_POINTERS节,比如__DATA_CONST,__got。 在这些节中,reserve1表示当前节中的第一个stub或者pointer在间接符号表中的索引。 间接符号表存储的是符号表中的索引。 通过符号表就能找到符号的相关信息。 上面3种类型的stub和pointer都是在间接符号表中是连续存在的。 比如__TEXT,__stubs中有3个stub,第一个sub在间接符号表中的索引为25,那么接下来2个stub在间接符号表中的索引就是26 27。 由于全局符号标包含所有的符号信息,而间接符号表可以方便让动态链接器dyld知道哪些符号需要绑定。 reserved2 这个字段通常都是0,只在某些特别的节有用: S_SYMBOL_STUBS节,比如__TEXT,__stubs。 在这个节中,reserved2表示一个stub所占用的字节大小。 那么这个节的stub数就等于当前节的size除以reserved2。 而对于S_LAZY_SYMBOL_POINTERS和S_NON_LAZY_SYMBOL_POINTERS节存储的都是指针。 指针的大小都是8字节,因此S_LAZY_SYMBOL_POINTERS和S_NON_LAZY_SYMBOL_POINTERS节中指针的数量就是当前节的size除以8。 reserved3 保留未使用为0。 Section 序号 Mach-O中的节Section都有序号。 Section的序号从1开始,跨越了段Segment。 也就是说如果第一个Segment中有10个Section,那么第二个Segment中的Section就从11开始。 3.1.2 __PAGEZERO __PAGEZERO是一个特殊的Segment段。 在Mach-O的Load Command中对__PAGEZERO段的描述如下图所示: 当它被加载到虚拟内存中时,它的起始地址为0x0,占用的虚拟内存大小为0x100000000,刚好为4G。 也就是说__PAGEZERO的地址空间为0x0~0xffffffff。 程序中如果访问到这个地址空间的地址,会发生空指针崩溃。 之所以是4G大小,是因为4G刚好是32bit,避免64bit程序使用32bit的指针。 同时还可以发现,这个段在Mach-O中不占据磁盘空间,它的fileSize为0。 因为这个段里面不包含任何数据,这样可以节省磁盘空间。 那对于iOS可执行文件,由于有ASLR机制,__PAGEZERO段的空间还是0x0~0xffffffff吗? 答案是在ASLR机制下,__PAGEZERO的起始虚拟地址还是0x0,但是结束地址会在0xffffffff上加上对应的偏移量。 3.1.3 __TEXT 有一个问题是,Mach-O中的Header和Load Command会被加载到虚拟内存中吗? 答案是会。 那Header和Load Command会被加载到虚拟内存的什么位置呢? 答案是Header和Load Command会被放到__TEXT段。 作为第一个有数据的Segment,__TEXT段紧挨着__PAGEZERO段。 但是__TEXT段在虚拟内存中的起始位置并不是代码,而是Header和Load Command数据。 在Header和Load Command之后,才是真正可以执行的代码。 从图上可以看到,__TEXT段的虚拟起始地址为0x100000000。 但是真正的代码在虚拟内存中的地址为0x1000011B8: 两者相减的值0x11B8正好是Header和Load Command占据的大小。 3.1.4 __LINKEDIT 静态链接器会对各个段排序。 __TEXT段会被排在第一位。 __LINKEDIT段总是Mach-O里最后一个段。 __LINKEDIT段包含的内容比较多,通常包括: Chained_Fixups Exports_Tire Function Starts 符号表 Data In Code Entries 间接符号表 字符串表 Code Signature 除了字符串表之外,其余部分都有对应的Load Command来描述自己的属性。 字符串表的位置,由符号表的Load Command LC_SYMTAB描述。 3.2 uuid_command uuid_command存储Mach-O对应的128bit UUID。 // mach-o/loader.h struct uuid_command { uint32_t cmd; /* LC_UUID */ uint32_t cmdsize; /* sizeof(struct uuid_command) */ uint8_t uuid[16]; /* the 128-bit uuid */ }; cmd 设置为LC_UUID。 cmdsize sizeof(uuid_command)。 uuid 128bit的UUID。 3.3 dylib_command dylib_command存储Mach-O使用的动态库信息。 // mach-o/loader.h struct dylib_command { uint32_t cmd; uint32_t cmdsize; struct dylib dylib; }; cmd cmd可以设置的值为: 1 LC_ID_DYLIB 2 LC_LOAD_DYLIB 3 LC_LOAD_WEAK_DYLIB 4 LC_REEXPORT_DYLIB 如果当前Mach-O是一个动态库,那么cmd会被设置为LC_ID_DYLIB。 此时,LC_ID_DYLIB标识了这个动态库的install name,告诉别人可以在哪里找到它。 当一个App链接到这个动态库时,动态库的LC_ID_DYLIB就会拷贝到App的LC_LOAD_DYLIB里,这样App在运行时就可以找到这个动态库。 LC_LOAD_WEAK_DYLIB表示弱链接。 弱链接意味着即使App在运行时找不到对应的动态库,或者库存在,但是库里的某个接口被删除了,也不会发生崩溃。 LC_REEXPORT_DYLIB的作用是把一个动态库的接口,伪装成另一个动态库的接口。 比如,现有一个巨大的动态库A,想把它拆成2个比较小的动态库B和C。 由于还有其他App依赖动态库A,为了不影响App的运行,那么动态库A可以使用LC_REEXPORT_DYLIB导出动态库B和C的接口。 换句话说,动态库A就成了一个空壳子。 iOS中的Umbrella Framework就是运用了LC_REEXPORT_DYLIB。 cmdsize dylib_command的大小除了sizeof(dylib_command)之外,还要加上struct dylib包含的字符串长度。 同时,整体的大小还有是8bytes的整数倍。 dylib 结构体dylib定义如下: // mach-o/loader.h struct dylib { union lc_str name; uint32_t timestamp; uint32_t current_version; uint32_t compatibility_version; }; name name是一个联合体lc_str,它的定义如下: // mach-o/loader.h union lc_str { uint32_t offset; #ifndef __LP64__ char *ptr; #endif }; 在64bit环境下,这个联合体实际上是: union lc_str { uint32_t offset; }; lc_str的offset存储字符串距离当前Load Command顶部的偏移量。 实际的字符串直接存储在当前Load Command的后面。 timestamp 动态库构建的时间。 current_version 当前动态库的版本号。 compatibility_version 动态库向后兼容的最低版本。 在App运行时,动态链接器使用struct dylib中的name字段指向的字符串作为路径,来加载动态库。 要想成功启动App,动态库自身的compatibility_version必须大于或者等于App记录的compatibility_version。 如果不是这样,动态链接器在程序启动时,会直接让程序崩溃。 因为对于苹果来说,如果App的compatibility_version更大,苹果就假定App肯定会用了更新的动态库接口,否则App也没有必要链接新版动态库。 所以如果提供了低版本的动态库,为了安全,直接不让App启动。 但是,动态库的compatibility_version更新,只代表App可以正常启动,不不代表运行时不会崩溃。 可能App使用的一个接口,在新版动态库被删除了,此时App运行到这个接口时就会崩溃。 但是让动态链接器去确认App有没有使用动态库里已经删除的接口,开销太大了,所以苹果没有做相关检查让App启动时就崩溃。 虽然有运行崩溃的风险,但是大多数情况下,动态库都还是可以平滑升级,并且不影响现有App的运行的。 3.4 dylinker_command dylinker_command存储动态链接器有关的信息。 struct dylinker_command { uint32_t cmd; uint32_t cmdsize; union lc_str name; cmd cmd的值可以设置为: 1 LC_LOAD_DYLINKER 2 LC_ID_DYLINKER 3 LC_DYLD_ENVIRONMENT 如果Mach-O文件是App,那么cmd会被设置成LC_LOAD_DYLINER。 此时这个命令存储的是动态链接器所在的磁盘路径。 它在内核加载这个App时,告诉内核去哪里找动态链接器。 如果Mach-O文件是动态链接器本身,那么cmd会被设置成LC_ID_DYLINKER。 此时这个命令存储的也是动态链接器自己在磁盘上的路径。 动态库既没有LC_LOAD_DYLINKER命令,也没有LC_ID_DYLINKER命令。 因为动态库都是可执行文件启动时,由动态链接器加载的。 此时,内核已经从可执行文件里读取了动态链接器的位置了。 有时我们运行一个可执行文件,想给动态链接器传递一些环境变量。 比如我们可以在Xcode -> Scheme -> Run -> Arguments -> Environment Variables传递DYLD_PRINT_LIBRARIES=1,让动态链接器打印当前可执行程序加载的动态库。 或者使用命令行: DYLD_PRINT_LIBRARIES=1 ./可执行程序 但是我们有时希望可执行程序加载的时候,能自动的将环境变量传递给动态链接器,那么LC_DYLD_ENVIRONMENT就起作用了。 动态链接器加载可执行程序时,会搜寻LC_DYLD_ENVIRONMENT,读取里面的环境变量。 想要在Mach-O里嵌入LC_DYLD_ENVIRONMENT,有2种方式。 第1种是在Xcode -> Build Settings -> other Link Flags里添加: -Wl,-dyld_env,DYLD_PRINT_LIBRARIES=1 -Wl是clang的参数,作用是把后面逗号分割的参数传递给静态连接器。 -dyld_env是静态链接器的参数,作用是让静态链接器将对应的环境变量写入Mach-O。 第2种是直接在控制台添加: clang main.m -o MyApp -Wl,-dyld_env,DYLD_PRINT_STATISTICS=1 cmdsize sizeof(dylinker_command)加上name字符串的长度。 name 要么是动态链接器的路径,要么是环境变量的值。 3.5 sub_framework_command 一个framework可能是一个umbrella framework的成员。 比如一个A.framework是umbrella framework的成员,那么A.framework中的sub_framework_command就用来记录umbrella framework的名字。 struct sub_framework_command { uint32_t cmd; uint32_t cmdsize; union lc_str umbrella; }; cmd 设置成LC_SUB_FRAMEWORK。 cmdsize sizeof(sub_framework_command)加上umbrella framework的名字长度。 unbrella 记录umbrella framework的名字。 如果一个framework里有sub_framework_command,那么它就只能被umbrella framework或者是其他umbrella framework成员链接。 3.6 sub_umbrella_command 一个umbrella framework可能会包含其他子umbrella framework。 这时,umbrella framework就会使用sub_umbrella_command记录子umbrella fraemwork的名字。 struct sub_umbrella_command { uint32_t cmd; uint32_t cmdsize; union lc_str sub_umbrella; }; cmd 设置为LC_SUB_UMBRELLA。 cmdsize sizeof(sub_umbrella_command)加上子umbrella framework名字长度。 sub_umbrella 子umbrella framework的名字 如果使用了sub_umbrella_command,那么从子umbrella framework中导出的符号,都会被当成从主umbrella framework导出一样,这个和LC_REEXPORT_DYLIB很像。 可能正是因为这样,sub_umbrella_command用的很少了,至少搜索了几个iOS系统库没有找到使用这个命令的。 3.7 sub_library_command sub_library_command和sub_umbrella_command对应。 只是一个应用于framework,一个直接应用于动态库.dylib。 struct sub_library_command { uint32_t cmd; uint32_t cmdsize; union lc_str sub_library; }; cmd 设置为LC_SUB_LIBRARY。 cmdsize sizeof(sub_library_command)加上子库的名字长度。 sub_library 子库的名字。 3.8 sub_client_command 有时一个framework只想给特定的程序链接,就需要sbu_client_command。 sub_client_command相当于一个白名单。 struct sub_client_command { uint32_t cmd; uint32_t cmdsize; union lc_str client; }; cmd 设置成LC_SUB_CLIENT。 cmdsize sizeof(sub_client_command)加上被允许链接的程序的名字。 client 被允许链接到这个framework的程序的名字。 3.9 build_version_command build_version_command用来记录当前程序的构建信息。 struct build_version_command { uint32_t cmd; uint32_t cmdsize; uint32_t platform; uint32_t minos; uint32_t sdk; uint32_t ntools; }; cmd 设置为LC_BUILD_VERSION。 cmdsize build_version_command后面会跟一个或者多个结构体,每个结构体对应使用的一个工具。 所以cmdsize等于sizeof(build_version_command)加上n * sizeof(build_tool_version)。 build_tool_version就是定义工具的结构体。 platform 定义了这个程序运行的平台: #define PLATFORM_MACOS 1 #define PLATFORM_IOS 2 #define PLATFORM_TVOS 3 #define PLATFORM_WATCHOS 4 #define PLATFORM_BRIDGEOS 5 #define PLATFORM_MACCATALYST 6 #define PLATFORM_IOSSIMULATOR 7 #define PLATFORM_TVOSSIMULATOR 8 #define PLATFORM_WATCHOSSIMULATOR 9 #define PLATFORM_DRIVERKIT 10 #define PLATFORM_MAX PLATFORM_DRIVERKIT minos 定义了这个程序能运行的最低系统版本。 sdk 定义了构建这个程序使用的sdk版本。 ntools 定义了构建这个程序使用的工具数量。 使用的工具信息由结构体build_tool_version定义,跟在build_version_command后面。 如果使用了多个工具,就会有多个build_tool_version结构体。 struct build_tool_version { uint32_t tool; uint32_t version; }; tool 定义工具类型: #define TOOL_CLANG 1 #define TOOL_SWIFT 2 #define TOOL_LD 3 version 定义工具的版本信息。 版本信息的格式为XXXX.YY.ZZ。 高16bit定义XXXX。 中间8bit定义YY。 最后8bit定义ZZ。 3.10 source_version_command source_version_command包含了用来构建当前二进制程序使用的源码版本信息。 struct source_version_command { uint32_t cmd; uint32_t cmdsize; uint64_t version; }; cmd 设置为LC_SOURCE_VERSION。 cmdsize sizeof(source_version_command)。 version 源码版本信息。 版本格式为a.b.c.d.e。 其中a占用24bit。 b占用10bit。 c占用10bit。 d占用10bit。 e占用10bit。 3.11 entry_point_command entry_point_command定义了可执行程序main函数的入口位置。 struct entry_point_command { uint32_t cmd; uint32_t cmdsize; uint64_t entryoff; uint64_t stacksize; }; cmd 设置为LC_MAIN。 cmdsize sizeof(entry_point_command)。 entryoff main函数入口的偏移量。 注意,这个偏移量是相对于__TEXT段起始地址的偏移量。 stacksize 主线程初始栈大小。 3.12 encryption_info_command_64 encryption_info_command_64定义了程序中的加密范围。 encryption_info_command_64在真机程序上才有,模拟器程序上没有。 通常是对__TEXT段进行加密。 struct encryption_info_command_64 { uint32_t cmd; uint32_t cmdsize; uint32_t cryptoff; uint32_t cryptsize; uint32_t cryptid; uint32_t pad; }; cmd 设置成LC_ENCRYPTION_INFO_64。 cmdsize sizeof(encryption_info_command_64)。 cryptoff 加密范围的起始偏移。 cryptsize 加密范围的大小。 cryptid 是否加密。 如果是0,表示未加密。 pad 填充字段。 因为command的大小要是8bytes的倍数。 3.13 rpath_command 有时不想让可执行程序记录的被链接的动态库地址是绝对地址,那么rpath_command就发挥作用了。 比如我们写了一个动态库A.dylib,想和可执行程序一起打包发布。 如果在链接可执行程序时使用A.dylib的绝对地址,那么由于用户安装可执行程序地址不一样,会造成程序运行崩溃。 此时可以使用install_name_tool工具,先将A.dylib的install name改成@rpath: install_name_tool -id @rpath/A.dylib A.dylib 然后重新将A.dylib链接到可执行程序。 最后使用install_name_tool工具给可执行程序添加rpath_command: install_name_tool -add_rpath @executable_path/Frameworks MyApp 当动态链接器dyld根据可执行程序中的LC_LOAD_DYLIB加载一个动态库,发现LC_LOAD_DYLIB记录的地址是@rpath/aaa/bbb,那么dyld就会查看rpath_command,用来解析@rpath。 struct rpath_command { uint32_t cmd; uint32_t cmdsize; union lc_str path; }; cmd 设置成LC_RPATH。 cmdsize sizeof(rpath_command)加上path字符串的长度。 path @rpath的值,包含2种情形: 1 @executable_path表示当前可执行程序所在目录。 2 @loader_path表示当前被加载的可执行程序或者库所在的目录。 如果当前加载的是可执行程序,那么@loader_path就是可执行程序所在的目录。 如果当前加载的是另一个动态库B.dylib,那么@loader_path就是B.dylib所在的目录。 3.14 linkedit_data_command 前面在__LINKEDIT段提到,__LINKEDIT段的内容除了字符串表之外,都有自己对应的Load Command。 其中,间接符号表和符号表的Load Command会单独写。 剩余部分的Load Command都由linkedit_data_command定义: struct linkedit_data_command { uint32_t cmd; /* LC_CODE_SIGNATURE, LC_SEGMENT_SPLIT_INFO, LC_FUNCTION_STARTS, LC_DATA_IN_CODE, LC_DYLIB_CODE_SIGN_DRS, LC_LINKER_OPTIMIZATION_HINT, LC_DYLD_EXPORTS_TRIE, or LC_DYLD_CHAINED_FIXUPS. */ uint32_t cmdsize; uint32_t dataoff; uint32_t datasize; }; cmd 命令包括 1 LC_DYLD_CHAINED_FIXUPS 2 LC_DYLD_EXPORTS_TRIE 3 LC_FUNCTION_STARTS 4 LC_LINKER_OPTIMIZATION_HINT 5 LC_DYLIB_CODE_SIGN_DRS 6 LC_SEGMENT_SPLIT_INFO 7 LC_DATA_IN_CODE 8 LC_CODE_SIGNATURE LC_DYLD_CHAINED_FIXUPS与动态链接器绑定符号地址有关,会单独写。 LC_DYLD_EXPORTS_TRIE与动态链接器查找导出符号有关,会单独写。 LC_FUNCTION_STARTS记录了二进制Mach-O中所有函数的分别是从哪里开始的,会单独写。 LC_LINKER_OPTIMIZATION_HINT存在于目标.o文件中,它告诉静态链接器ld,哪些汇编指令,可以用更加高效的指令替代。 LC_DYLIB_CODE_SIGN_DRS它记录了一个二进制Mach-O依赖的所有动态库的签名规则,以确保这些动态库被加载时都是根正苗红的,没有被恶意替换的。 LC_SEGMENT_SPLIT_INFO因为LC_DYLD_CHAINED_FIXUPS的出现逐渐被取代了。 LC_DATA_IN_CODE编译期有时为了查找效率或者内存对齐要求,会在__TEXT段插入一些数据,如果没有这个命令,反汇编就会把这些数据错误的解释成代码指令。 LC_CODE_SIGNATURE存放二进制Mach-O的数字签名。 cmdsize sizeof(linkedit_data_command)。 dataoff 相关数据相对于__LINKEDIT段的偏移量。 datasize 相关数据的大小。 4 Fat Mach-O Apple支持将同一个源码在不同CPU架构编译成的Mach-O二进制放到同一个Mach-O中,这样的Mach-O叫Fat Mach-O。 Fat Mach-O结构如下: Fat Header指明了包含多少种CPU架构下的Mach-O。 Fat Arch指明了当前是何种CPU架构以及这个架构下的Mach-O相对于Fat Mach-O头部的偏移量。 每种CPU架构下的Mach-O都是一个完整的Mach-O文件,即包含Header,Load Command,Segment。 当系统加载Fat Mach-O时,只会选择与当前CPU匹配的Mach-O进行加载,不会加载整个Fat Mach-O。 4.1 Fat Header Fat Header的定义如下: // mach-o/fat.h struct fat_header { uint32_t magic; uint32_t nfat_arch; }; magic 始终为FAT_MAGIC nfat_arch 表明包含有多少种CPU架构的Mach-O。 对于Fat Header,无论编译这个Fat Mach-O的机器使用大端还是小端,它所有的字段都是大端存储。 4.2 Fat Arch Fat Arch的定义如下: struct fat_arch { cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t offset; uint32_t size; uint32_t align; }; cputype 对应CPU类型。 cpusubtype 对应CPU子类型。 offset 这种CPU类型下的Mach-O相对于当前Fat Mach-O底部的偏移量。 size 这种CPU类型下的Mach-O的大小。 align 指定offset的对齐方式,应该对齐2^align。 由于系统加载Fat Mach-O,是将磁盘页与内存页一一对应映射,所以对offset有对齐要求。