Xcode 构建一个程序的过程中,会把源文件 (.m 和 .h) 文件转换为一个可执行文件(Mach-O executable)。这个可执行文件中包含的字节码将会被 CPU (iOS 设备中的 ARM 处理器或 Mac 上的 Intel 处理器) 执行。当然,我们也可以不使用 Xcode,而是通过苹果提供的命令行工具(command line tools)来构建一个程序。
#1 不使用 Xcode 的 Hello World 通过终端 (Terminal),创建一个包含一个 C 文件的文件夹:
1 2 3 $ mkdir command-line$ cd !$$ touch helloworld.c
使用文本编辑器来编辑这个文件:
输入如下代码:
1 2 3 4 5 6 #include <stdio.h> int main (int argc, char *argv[]) { printf ("Hello World!\n" ); return 0 ; }
保存并返回到终端,然后运行如下命令编译 helloworld.c 文件:
1 $ xcrun clang helloworld.c
这里会产生一个名为 a.out 的文件,使用file命令查看其信息:
1 2 $ file a .out a .out : Mach-O 64 -bit executable x86_64
这个 a.out 文件就是使用 clang 编译器将 helloworld.c 编译后产生的一个 Mach-O 格式的二进制文件。注意,如果没有指定名字,那么编译器会默认的将其指定为 a.out。
然后我们执行此二进制文件,终端中的输出结果如下:
#2 Mach-O(可执行文件格式) Mach-O 是 Mach object 文件格式的缩写,类似于 windows 环境下的 PE 格式、Linux 环境下的 ELF 格式。我们平时见到的可执行文件、dSYM符号文件、Dylib动态库、Framework 等使用的都是这种格式。
Mach-O提供更多的可扩展性和更快的符号表信息存取。
Mach-O应用在基于Mach核心的系统上,目前NeXTSTEP、Darwin、Mac OS X(iPhone)都是使用这种可执行文件格式。
Mach-O 文件的结构由 Header、Load commands、Data(包含Segement的具体数据)组成。
上面我们编译出的可执行文件 a.out 使用的也是这种格式。
#示例1:使用 otool 命令查看 a.out 文件的内部结构
1 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 $ otool -l a.out a.out: Mach header magic cputype cpusubtype caps filetype ncmds sizeofcmds flags 0 xfeedfacf 16777223 3 0 x80 2 15 1200 0x00200085 Load command 0 cmd LC_SEGMENT_64 cmdsize 72 segname __PAGEZERO vmaddr 0 x0000000000000000 vmsize 0 x0000000100000000 fileoff 0 filesize 0 maxprot 0x00000000 initprot 0x00000000 nsects 0 flags 0 x0 Load command 1 cmd LC_SEGMENT_64 cmdsize 472 segname __TEXT vmaddr 0 x0000000100000000 vmsize 0 x0000000000001000 fileoff 0 filesize 4096 maxprot 0x00000007 initprot 0x00000005 nsects 5 flags 0 x0 Section sectname __text segname __TEXT addr 0 x0000000100000f50 size 0 x0000000000000034 offset 3920 align 2 ^4 (16 ) reloff 0 nreloc 0 flags 0x80000400 reserved1 0 reserved2 0 Section sectname __stubs segname __TEXT addr 0 x0000000100000f84 size 0 x0000000000000006 offset 3972 align 2 ^1 (2 ) reloff 0 nreloc 0 flags 0x80000408 reserved1 0 (index into indirect symbol table) reserved2 6 (size of stubs) Section sectname __stub_helper segname __TEXT addr 0 x0000000100000f8c size 0 x000000000000001a offset 3980 align 2 ^2 (4 ) reloff 0 nreloc 0 flags 0x80000400 reserved1 0 reserved2 0 Section sectname __cstring segname __TEXT addr 0 x0000000100000fa6 size 0 x000000000000000e offset 4006 align 2 ^0 (1 ) reloff 0 nreloc 0 flags 0x00000002 reserved1 0 reserved2 0 Section sectname __unwind_info segname __TEXT addr 0 x0000000100000fb4 size 0 x0000000000000048 offset 4020 align 2 ^2 (4 ) reloff 0 nreloc 0 flags 0x00000000 reserved1 0 reserved2 0 Load command 2 cmd LC_SEGMENT_64 cmdsize 232 segname __DATA vmaddr 0 x0000000100001000 vmsize 0 x0000000000001000 fileoff 4096 filesize 4096 maxprot 0x00000007 initprot 0x00000003 nsects 2 flags 0 x0 Section sectname __nl_symbol_ptr segname __DATA addr 0 x0000000100001000 size 0 x0000000000000010 offset 4096 align 2 ^3 (8 ) reloff 0 nreloc 0 flags 0x00000006 reserved1 1 (index into indirect symbol table) reserved2 0 Section sectname __la_symbol_ptr segname __DATA addr 0 x0000000100001010 size 0 x0000000000000008 offset 4112 align 2 ^3 (8 ) reloff 0 nreloc 0 flags 0x00000007 reserved1 3 (index into indirect symbol table) reserved2 0 Load command 3 cmd LC_SEGMENT_64 cmdsize 72 segname __LINKEDIT vmaddr 0 x0000000100002000 vmsize 0 x0000000000001000 fileoff 8192 filesize 240 maxprot 0x00000007 initprot 0x00000001 nsects 0 flags 0 x0 Load command 4 cmd LC_DYLD_INFO_ONLY cmdsize 48 rebase_off 8192 rebase_size 8 bind_off 8200 bind_size 24 weak_bind_off 0 weak_bind_size 0 lazy_bind_off 8224 lazy_bind_size 16 export_off 8240 export_size 48 Load command 5 cmd LC_SYMTAB cmdsize 24 symoff 8296 nsyms 4 stroff 8376 strsize 56 Load command 6 cmd LC_DYSYMTAB cmdsize 80 ilocalsym 0 nlocalsym 0 iextdefsym 0 nextdefsym 2 iundefsym 2 nundefsym 2 tocoff 0 ntoc 0 modtaboff 0 nmodtab 0 extrefsymoff 0 nextrefsyms 0 indirectsymoff 8360 nindirectsyms 4 extreloff 0 nextrel 0 locreloff 0 nlocrel 0 Load command 7 cmd LC_LOAD_DYLINKER cmdsize 32 name /usr/lib/dyld (offset 12 ) Load command 8 cmd LC_UUID cmdsize 24 uuid 6122E2B2 -9651-3991 -B2BA-15 B45ADC0C6B Load command 9 cmd LC_VERSION_MIN_MACOSX cmdsize 16 version 10 .13 sdk 10 .13 Load command 10 cmd LC_SOURCE_VERSION cmdsize 16 version 0 .0 Load command 11 cmd LC_MAIN cmdsize 24 entryoff 3920 stacksize 0 Load command 12 cmd LC_LOAD_DYLIB cmdsize 56 name /usr/lib/libSystem.B.dylib (offset 24 ) time stamp 2 Thu Jan 1 08:00:02 1970 current version 1252.50.4 compatibility version 1 .0 .0 Load command 13 cmd LC_FUNCTION_STARTS cmdsize 16 dataoff 8288 datasize 8 Load command 14 cmd LC_DATA_IN_CODE cmdsize 16 dataoff 8296 datasize 0
头部主要用来规定这个文件是什么,以及文件是如何被加载的(通过 otool -h 可以打印出头信息)。
1 2 3 4 $ otool -h a.out Mach header magic cputype cpusubtype caps filetype ncmds sizeofcmds flags 0xfeedfacf 16777223 3 0x80 2 15 1200 0x00200085
1 2 3 4 5 6 7 8 9 struct mach_header { uint32_t magic; cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t filetype; uint32_t ncmds; uint32_t sizeofcmds; uint32_t flags; };
1 2 3 4 5 6 7 8 9 10 struct mach_header { uint32_t magic; cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t filetype; uint32_t ncmds; uint32_t sizeofcmds; uint32_t flags; uint32_t reserved; };
64 位架构只比 32 位架构多了一个 reserved 字段。
表示当前二进制文件的类型,不同类型的二进制文件有自己独特的”魔数值”。加载器通过这个魔数值来判断这是什么样的文件,例如32位 Mach-O 文件的魔术值是 0xfeedface、64位 Mach-O 文件的魔术值是 0xfeedfacf、胖二进制文件的魔数值是 0xcafebabe。所以这个字段也可以用于快速确认该文件用于64位还是32位。
cup的类型(如 iOS设备的ARM、Mac上的Intel)。
cup的子类型(如armv7、arm64等)。
文件类型(如可执行文件、库文件、dSYM文件等),具体枚举如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 * Constants for the filetype field of the mach_header */#define MH_OBJECT 0x1 #define MH_EXECUTE 0x2 #define MH_FVMLIB 0x3 #define MH_CORE 0x4 #define MH_PRELOAD 0x5 #define MH_DYLIB 0x6 #define MH_DYLINKER 0x7 #define MH_BUNDLE 0x8 #define MH_DYLIB_STUB 0x9 #define MH_DSYM 0xa #define MH_KEXT_BUNDLE 0xb
指的是加载命令(Load Commands)的数量,如上面#示例1中 a.out 文件的 ncmds 是 15 个,编号为 0-14。
表示 N 个 Load Commands 的总字节大小, Load Commands 区域是紧接着 Header 区域的。
标志位,具体枚举值定义如下:
1 2 3 4 5 #define MH_NOUNDEFS 0x1 #define MH_DYLDLINK 0x4 #define MH_PIE 0x200000 #define MH_TWOLEVEL 0x80 ...
##2.2. Load commands Load commands 跟在 Header 部分的后面,这些加载指令负责在 Mach-O 文件加载解析时告诉加载器如何处理二进制数据:有些命令是由内核处理的,有些是由动态链接器处理的,最终会根据 cmd 字段的不同类型,使用不同的函数来加载。
Load commands 的结构定义如下:
1 2 3 4 struct load_command { uint32_t cmd; uint32_t cmdsize; };
代表 Load commands 的类型,如 LC_SEGMENT_64、LC_UUID、LC-CODE-SIGNATURE 等等。
代表 Load commands 的大小。
下面列举几个看上去比较熟悉的 Load commands…
1 2 3 4 5 6 7 8 9 10 11 12 13 #define LC_SEGMENT 0x1 #define LC_SEGMENT_64 0x19 #define LC_UUID 0x1b #define LC_LOAD_DYLINKER 0xe #define LC_CODE_SIGNATURE 0x1d #define LC_ENCRYPTION_INFO 0x21
以 LC_SEGMENT_64 类型为例,它代表将文件中64位的段映射到进程的地址空间。它的结构定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct segment_command_64 { uint32_t cmd; uint32_t cmdsize; char segname[16 ]; uint64_t vmaddr; uint64_t vmsize; uint64_t fileoff; uint64_t filesize; vm_prot_t maxprot; vm_prot_t initprot; uint32_t nsects; uint32_t flags; };
下面解释一下各参数的作用。。
段的名称,下面的##2.3小结中有更详细的介绍。
段的虚拟内存地址。
段的虚拟内存大小
段在文件中偏移量。
段在文件中的大小。
段中有多少 secetion。
以上面 #示例1 中的 Load command 0 为例:
1 2 3 4 5 6 7 8 9 10 11 12 Load command 0 cmd LC_SEGMENT_64 cmdsize 72 segname __PAGEZERO vmaddr 0 x0000000000000000 vmsize 0 x0000000100000000 fileoff 0 filesize 0 maxprot 0 x00000000 initprot 0 x00000000 nsects 0 flags 0 x0
它就是告诉加载器将该段文件的内容加载到内存中:从 fileoff 处加载 filesize 大小到虚拟内存 vmaddr 处。由于这里在内存地址空间中是 _PAGEZERO 段(这个段不具有访问权限,用来处理空指针)所以各参数都是零。
##2.3. Segments(段数据) Segments 包含了很多 segment,每一个 segment 定义了一些 Mach-O 文件的数据、地址和内存保护属性,这些数据在动态链接器加载程序时被映射到了虚拟内存中。每个段都有不同的功能,一般包括:
空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用;
包含了执行代码以及其他只读数据。 为了让内核将它直接从可执行文件映射到共享内存, 静态连接器设置该段的虚拟内存权限为不允许写。当这个段被映射到内存后可以被所有进程共享(这主要用在 Frameworks、 Bundles和共享库等程序中,也可以为同一个可执行文件的多个进程拷贝使用)。
包含了程序数据,该段可写;
Objective-C运行时支持库;
含有为动态链接库使用的原始数据,比如符号,字符串,重定位表条目等等。
##2.4. section(区数据) 一般段又会按不同的功能划分为几个区(section)。区的字母为小写,同样加两个下划线作为前缀。下面列出段中可能包含的 section:
1 __text , __cstring , __picsymbol_stub , __symbol_stub , __const , __litera14 , __litera18 ;
1 2 __data , __la_symbol_ptr , __nl_symbol_ptr , __dyld , __const , __mod_init_func , __mod_term_func , __bss , __commom ;
1 __jump_table , __pointers ;
其中 __TEXT 段中的 __text 是实际上的代码部分;__DATA 段的 __data 是实际的初始数据。
section 的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct section { char sectname[16 ]; char segname[16 ]; uint32_t addr; uint32_t size; uint32_t offset; uint32_t align; uint32_t reloff; uint32_t nreloc; uint32_t flags; uint32_t reserved1; uint32_t reserved2; };
其中部分参数的介绍:
section 的名字,如上面提到的 __text、__cstring等。
section 所属的 segment,比如 __TEXT。
section 在内存的起始位置。
section 的大小。
section 的文件偏移量。
#示例2:通过 otool –s查看某个 section 的信息
1 2 3 4 5 6 7 $ otool -s __TEXT __text a.out a.out: Contents of (__TEXT,__text) section 0000000100000f50 55 48 89 e5 48 83 ec 20 48 8d 05 47 00 00 00 c7 0000000100000f60 45 fc 00 00 00 00 89 7d f8 48 89 75 f0 48 89 c7 0000000100000f70 b0 00 e8 0d 00 00 00 31 c9 89 45 ec 89 c8 48 83 0000000100000f80 c4 20 5d c3
由于-s __TEXT __text 很常见,otool 对其设置了一个缩写 -t 。我们还可以通过添加 -v 来查看反汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $ otool -t -v a.out a.out: (__TEXT, __text) section _main:0000000100000 f50 pushq %rbp 0000000100000 f51 movq %rsp , %rbp 0000000100000 f54 subq $0x20 , %rsp 0000000100000 f58 leaq 0x47 (%rip ), %rax 0000000100000 f5 f movl $0x0 , -0 x 4 (%rbp )0000000100000 f66 movl %edi , -0 x 8 (%rbp )0000000100000 f69 movq %rsi , -0 x 10 (%rbp )0000000100000 f6 d movq %rax , %rdi 0000000100000 f70 movb $0x0 , %al 0000000100000 f72 callq 0x100000f84 0000000100000 f77 xorl %ecx , %ecx 0000000100000 f79 movl %eax , -0 x 14 (%rbp )0000000100000 f7 c movl %ecx , %eax 0000000100000 f7 e addq $0x20 , %rsp 0000000100000 f82 popq %rbp 0000000100000 f83 retq
#3 Mach-O & 胖文件 OS X 有两种类型的目标文件:Mach-O 和 universal binary ,后者就是通用二进制文件,也叫胖文件。区别在于:
实际上 universal binary 文件只不过是将支持不同架构的 Mach-O 文件打包在一起,再在文件起始位置加上 fat_header 来说明所包含的 Mach-O 文件支持的架构和偏移地址信息。
胖文件的结构︰
fat_header
fat_arch
fat_arch
。。。
其中 fat_header 的数据结构在 mach-o/fat.h 头文件上定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define FAT_MAGIC 0xcafebabe #define FAT_CIGAM 0xbebafeca struct fat_header { uint32_t magic; uint32_t nfat_arch; }; struct fat_arch { cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t offset; uint32_t size; uint32_t align; };
参数说明:
就是上面说的魔数,加载器通过这个魔数值来判断这是什么样的文件,胖二进制文件的魔数值是0xcafebabe;
是指当前的胖二进制文件包含了多少个不同架构的 Mach-O 文件。有多少个不同架构的 Mach-O 文件,就有多少个 fat_arch,用于说明对应 Mach-O 文件大小、支持的CPU架构、偏移地址等;
大部分情况下,xxx.app/xxx 文件并不是 Mach-O 格式文件,由于现在需要支持不同 CPU 架构的 iOS 设备,我们编译打包出来的执行文件一般都是胖文件。当然,支持的架构越多,最终打出来的包就会越大,所以项目里的 Architectures 和 Valid Architectures 选项要根据需求来配置哟。
#示例3:WeChat.app/WeChat 文件的信息
1 2 3 4 5 $ file WeChat WeChat: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64] WeChat (for architecture armv7): Mach-O executable arm_v7 WeChat (for architecture arm64): Mach-O 64 -bit executable arm64
最后,了解 Mach-O 文件对于使用 dSYM 解析崩溃日志、逆向工程、bitcode、应用瘦身等会有很大的帮助!更多相关的内容之后继续慢慢研究。。
相关参考:
#©WWDC 2016
#©极客学院
#©简书1
#©简书2