现代操作系统通常提供了多种运行时环境,Mac OS X也不例外。然而所有的环境基础都是Mach-O,全称为Mach Object File Format。但这里的Mach并不代表是Mach内核的一部分。事实上这个格式是Mac OS X内核独有的,通过execve加载的二进制格式。这个syscall实现在BSD部分。
Mach-O用于实现各种各样的系统组件,例如
- bundle(可通过编程加载的代码)
- 动态链接库
- 框架
- umbrella framework(包含多个其他框架的框架)
- 内核扩展
- 可链接对象文件
- 静态链接库
- 可执行文件
Mac OS X也提供了一些程序用来创建,分析和修改Mach-O文件
- as – GNU汇编器前端
- clang,clang++ – clang编译器前端
- dyld – 动态链接器
- ld – 静态链接器
- libtool – 一个可以从Mach-O对象文件创建动态链接库和静态链接库的工具,通常被编译器调用用来创函数库
- nm – 可以现实符号表的工具
- otool – 一个可以读取mach-O内部构造的多功能工具,同时也可以反编译
Mach-O文件包含一个固定大小的头部,并跟随着多个大小可变的load命令,紧接着的是更多的segment。每个segment又可以分成一个或多个section。头部定义如下

Mach-O文件头包含了文件的特征,布局和链接特征。filetype有以下几种
- MH_BUNDLE – bundle类型文件,一段可以通过编程在运行时加载进程序的代码
- MH_CORE – 内核转储文件
- MH_DYLIB – 动态链接库
- MH_DYLINKER – 动态链接器,一个特殊的动态链接库
- MH_EXECUTE – 课程性文件
- MH_OBJECT – 对象文件,通常以.o结尾。也被用来做内核拓展
对于可执行程序来说,在Mach-O的文件头中有一个LC_LOAD_DYNLINKER指令用于指定动态链接器位置。目前唯一可用的动态链接器为苹果自己的dyld,位于/usr/lib/dyld。这个文件本身是一个MH_DYLINKER类型文件。内核和dyld一起执行一系列步骤启动一个可执行文件,粗略步骤如下:
- 内核首先检查Mach-O文件头,确认文件类型
- 内核执行load指令,例如LC_SEGMENT指令会将程序加载进内存
- 内核执行LC_LOAD_DYLINKER启动动态链接器
- 内核调用动态链接器,动态链接器是第一个在程序的地址空间被执行的程序(先于程序本身的代码执行)。内核将Mach-O文件头和命令行参数传递给动态链接器
- 动态链接器继续执行Mach-O文件头的load指令,根据程序要求将动态链接库加载进内存并将Mach-O的导入符号和动态链接库的定义相连接
- 动态链接器调用LC_UNIXTHREAD或LC_THREAD load指令指定的程序入口。这通常是语言运行时的初始化函数,这个函数将调用程序真正的main函数。
下面是一个简单的例子,我们有一个C语言程序

编译后通过otool可以看到这个程序的文件头

LC_UNIXTHREAD指向了一个寄存器。这个例子中指向的是PowerPC的srr0寄存器。srr0中包含了一个地址0x000023cc,这就是程序开始的地址。这个地址是一个名为start的函数,由start函数最终调用程序的main函数执行。这个start函数来自C语言运行时库crt1.o,编译器在编译时会链接这个库。
如果在x86计算机上编译这个程序,LC_UNIXTHREAD会指向x86的eip寄存器,其中保存了同样的start函数地址。
在更新的Mac OS X版本中,LC_MAIN取代LC_UNIXTHREAD,但LC_UNIXTHREAD依然可用,因为LC_MAIN需要dyld支持,dyld本身不能使用LC_MAIN。因此dyld依然在使用LC_UNIXTHREAD。
由于Mac OS X支持多个平台(在本书中是PowerPC和x86,现在是x86和ARM),Mac OS X使用胖二进制文件让同一个程序可以在不同平台上运行。这种文件被苹果称作Apple Universal Binary

胖二进制文件本质上只是打包了不同平台的二进制文件,并有一个特殊的fat_header头,这个文件头中记录了在这个文件中包含了几个不同平台的二进制文件。在fat_header后是一些fat_arch头,每一个fat_arch头记录了对应的平台信息和偏移量。内核可以根据偏移量找到需要的二进制文件的位置并加载。
在Mac OS X中,动态链接是默认的,所有正常用户空间的程序都是动态链接的。事实上苹果不支持静态链接用户空间的程序(Mac OS X没有带静态链接库)。动态链接其中一个原因是C库和系统之间的ABI是私有的,也就是说syscall的trap指令不应该被用户空间程序直接调用。但是内核拓展却不能动态链接,因为内核拓展是对象文件而不是可执行文件。
otool可以显示一个文件需要的动态链接库是哪些。比如下面这个例子显示了launchd使用的动态链接库。有趣的是launchd是用户空间的第一个程序(pid 1),在正常的Unix系统中通常是静态链接的

Mach-O的运行时还有其他特性,第一个是多种动态库的连接方式。分别是懒绑定 lazy binding,也叫Just-In-Time binding和加载时绑定load-time binding。此外还有一个pre binding,在10.4时被废弃。lazy binding只会在一个动态链接库被第一次调用时才会被加载,并且符号不会立刻和动态链接库绑定,只有在第一次使用时才会被绑定。load-time binding会在加载时一次性绑定所有符号。pre binding稍有不同,是在程序编译时就解析所有用到的符号并为动态链接库预留出空间,动态链接器只需要在运行时根据编译生成的符号加载函数即可。这会加快动态链接的速度,但也需要程序保证预留的空间不会被其他内存覆盖。在10.4之后dylib已经经过优化,使pre binding带来的性能优势变得十分微弱,因此在之后被废弃。
第二个特性是双层命名空间。使用哪一个函数不仅由符号名决定,也由这个函数所在的动态链接库决定。因此动态链接库默认情况下不能在程序运行前使用DYLD_INSERT_LIBRARIES环境变量加载(例如如果我想用自己写的my_malloc代替系统的malloc,由于双层命名空间的存在我的函数命名类似my_malloc::malloc,而系统的则是malloc::malloc,程序还是会使用系统的malloc,但dyld也提供了解决方法__interpose)。可以使用DYLD_FORCE_FLAT_NAMESPACE关闭双层命名空间。然而很多动态链接库会有重名的函数,这样做可能会导致程序不能正常运行。
第三个特性是弱引用符号。通常情况下当一个符号不存在时程序会崩溃,然而如果一个符号是弱引用的,动态链接器如果发现这个符号不存在只会返回一个NULL。因此调用的程序需要在调用前检查函数是否存在。下面是一个弱引用的例子

上文提到函数替换的问题,dyld支持一个特殊的__interpose指令可以替换一个特定的函数,例如我想使用my_open替代open函数,可以在代码里这样写,此时动态库函数可以被替换
