Basic Structure
一个Mach-O文件包含三个主要的区域(region):
在每一个Mach-O文件的开始都有一个header结构(header structure)来标示这是一个Mach-O文件。这个header同时还包含着其他一些信息:文件类型信息,目标架构信息和一写影响文件其他部分内容的flags标示。
紧跟着header的是一系列大小可变的加载命令(load commands),用于标示文件的布局和联动特征。两个加载命令之间的信息一般有一下几种含义:
文件在虚拟内存中的初始布局
用于动态链接的符号列表(symbol talbe)的位置
程序主线程初始的执行状态
共享库的名称,这些库包括主执行文件导入的符号定义
- load commands下面是一个或多个的数据段(segments)。每一个segment都包含零个或更多的sections。segemnt的每一个section包含特定类型的代码和数据。每一个segment定义了一片虚拟内存区域,也就是动态链接器将映射到的进程地址空间。segment和section的数量及其布局是由load commands和文件类型决定的。
最后一个segment是链接编辑段(link edit segment),它与用户级挂钩。该segment包含链接编辑信息表(tables of link edit information),如:符号表(symbol table),字符表(string table)等等,动态加载器会根据这些表将可执行文件或Mach-O包和与他依赖的库进行链接。

当使用DWARF进行调试的时候,调试信息存储在image对应的dSYM文件中,并且被uuid_command 结构所定义。
每个Mach-O文件都包含着与一个架构相对应的代码和数据。头结构指定了被内核所确认的特定的结构,例如:在基于PowerPC架构的Mac计算机中的代码就不能在基于Intel架构的Mac计算机上运行。但是你可以利用Universal Binaries and 32-bit/64-bit PowerPC Binaries.中所定义的格式将多个Mack-O文件组合进一个二进制文件中。
注意:由包含一个以上架构的对象文件组成的二进制文件并不是Mach-O文件,它是指对多个Mach-O文件进行了压缩。
segment和section通常是通过名称进行访问的,segment的名称由两个底杠加上大写字母组成(如:TEXT);section的名称由两个底杠加上小写字母组成(如:text)。这些命名规则都是约定俗成的。
Header Structure
通过otool -h hw.out命令可以显示出Mach-O文件的Mach header信息,其结构如下:
1 | localhost:binspect ronglei$ xcrun otool -h hw.out |
1 | struct mach_header { |
- magic是一个常量,详见:/usr/include/mach-o/loader.h
1 | / Constant for the magic field of the mach_header (32-bit architectures) / |
- cputypeCPU标示,代码执行所需的CPU的类型
- cpusubtypemachine标示,详见:/usr/include/mach/machine.h
1 |
|
- filetype标示文件类型,参见:/usr/include/mach-o/loader.h
1 |
|
- ncmds和sizeofcmds分别标示load commands的数量和所有load commands命令的size
Load Commands
Load Commands不仅定义了文件的逻辑结构,还定义了文件在虚拟内存中的布局。他们是Mach-O文件的核心从上面输出的mac header信息中我看看到我们的Mach-O文件总共有16个load commads:
在终端通过otool -l hw.out 命令可以查看Mach-O文件中所有Load Commands。
1 | localhost:binspect ronglei$ xcrun otool -l hw.out |
- LC_SEGMENT_64:(Load command 0-3)定义了一个(64-bit)的segments及其中的sections在文件加载时如何映射到内存中。
- LC_SYMTAB:(Load command 5)定义了文件的符号表(stabs style)和字符表。链接器在链接文件的时候会用到,调试器也可以将它们将符号表映射到与它们相对应的源码文件中,以便于调试问题所在。
- LC_DYSYMTAB:(Load command 6)为动态链接器提供了额外的信息,用以如何将符号写进符号表中。
- LC_DYLD_INFO_ONLY:(Load command 4)定义了一些额外的与动态链接压缩信息,包含信号的元数据和动态绑定其它事情的操作码。“_ONLY”后缀意味着这条load command需要在程序中运行,因此那些不懂这条load command的旧的链接器将无法运行。
- LC_LOAD_DYLINKER:(Load command 7)加载动态链接器,(name /usr/lib/dyld)
- LC_LOAD_DYLIB:(Load command 12)加载动态链接共享库。如:“/usr/lib/libSystem.B.dylib”一个用标准C语言函数库。每一个动态库都会被动态链接器加载进内存,它们内部包含一张符号名称与地址链接在一起的符号表。
- LC_MAIN:(Load command 11)指定了程序main()函数的入口地址。
- LC_UUID:(Load command 8)提供了一个随机的唯一标示符(UUID),通常由静态连接器生成。
- LC_VERSION_MIN_MAXOSX:(Load command 9)该二进制文件所支持的最低OS X系统版本。
- LC_SOURCE_VERSION:(Load command 10)用于build二进制文件的源码版本。
- LC_FUNTION_STARTS:(Load command 13)定义了一张包含所有函数开始位置的表。
- LC_DATA_IN_CODE:(Load command 14)定义了code segemnt中的一张非指令表。
- LC_DYLIB_CODE_SIGN_DRS:(Load command 15)为动态链接库定义了代码签名。
Segments and Sections
segment 在Mach-O文件中定义了一定范围的字节数,在动态链接器加载程序的时候这一特定范围的字节数将被映射到对应的虚拟内存中。
出于内存分页的考虑,头结构和加载命令通常会成为第一个segment 的一部分。在一个可执行文件中,这通常意味着头结构和加载命令会被放置到__TEXT segment 的开头,因为它才是第一个包含数据的segment。
注意:内容为空的section需要被放置在segment的末尾,否则标准工具将无法对Mach-O文件正确地进行操作。
我们可以通过size指令打印出Mach-O文件在虚拟内存中的布局:
1 | localhost:binspect ronglei$ xcrun size -x -l -m hw_clang.out |
在上一节的otool -l hw.out 命令输出中我们看到,segment/section结构被定义在’LC_SEGMENT_64’加载命令中。下面然我们来看一下有关它们的一下详细信息:
PAGEZERO :作为可执行文件的第一个segment,该segment被放置在虚拟内存开始的位置权限为可读写。PAGEZERO segment在被映射入内存后(在当前的架构对于基于Intel和PowerPC的Mac计算机而言,该空间大小为4096字节用十六进制表示为0x1000)占据虚拟内存一页的空间,而在存储在磁盘上市,__PAGESIZE segment不不占据任何空间,所以他加载进内存中所需要的空间要比存储在磁盘上的空间更大。顺便说一句该segment是隐藏恶意代码的好地方。
TEXT:包含可执行代码和只读数据。为了使得内核方便的将该段直接从可执行文件中映射到共享内存中,静态链接器设置该段在虚拟内存的权限为只读。当该段被映射到内存的时候,它被所有对它感兴趣的进程所共享(这正是framework,bundle和shared library的主要用途,但是它可以在同一个执行程序中运行多个拷贝),只读属性同时意味着由该segment组成的内存页不需要再被写回到磁盘中。当内核需要释放物理内存时,它只需简单地释放一页或更多页的TEXT,然后在需要的时候重新从磁盘将他们读取到内存。
__text:存放可执行机械代码的section;
__stubs:间接地符号存根。代表着lazy引用和no-lazy引用做要跳转的地址值。
__stubs_helper:为lazy加载符号提供帮助。
__cstring:存储只读的C语言格式字符串(如:“hello, wold!0”)的section,链接器会对该section进行去重的处理。
unwind_info:栈展开信息的压缩格式。该section是链接器对‘eh_frame’section里面的信息进行压缩产生的。
__eh_frame:一个标准的用于异常处理的
section,它负责为DWARF格式提供栈展开信息。
DATA:一个包含可读写数据的segment。静态链接器设置这段虚拟内存的属性为可读写。因为它是可写读写的,共享库的DATA段会被拷贝到每一个与这个库链接的进程中。当可读写__DATA段被映射到虚拟内存页的时候,内核将他们标记为写时拷贝(copy-on-write);因此当进程向某页写入数据时,该进程将会接受一个该页的私有拷贝。
__nl_symbol_ptr:指向非延迟导入符号(non-lazy imported symbols)的表。
__la_symbol_ptr:指向延迟导入符号(lazy imported symbols)的表。
- __OBJ:一个包含用于Ojbectiv-C语言动态运行时库数据的segment。
- __IMPORT:一个只有在IA-32架构的计算机中才会产生的segment。
- __LINKEDIT:段包含会被动态链接器使用的原生数据,如:符号,字符串和重定位表入口。
Reference
OS X ABI Mach-O File Format Reference
THE NITTY GRITTY OF “HELLO WORLD” ON OS X
Mach-O Executables
Dynamic Linking On OS X
otool
iOS程序main函数之前发生了什么