Mach-O(一)结构

引子:可执行文件

在Unix中,任何文件都可以通过简单的chmod+x来标记为可执行文件, 但这并不能保证该文件可移植性。这个标志的作用,只是简单告诉操作系统内可以将该文件读入内存。

然后寻找一个头签名,据此可以确定精确的可执行格式。该头签名,就是魔数(magic)

可执行文件格式有如下格式:

EXECUTABLE FORMAT MAGIC USED FOR
PE32/PE32+ MZ Portable executables: The native format in Win- dows and Intel’s Extensible Firmware Interface (EFI) binaries. Although OS X does not support this format, its boot loader does and loads boot.efi.
ELF \x7FELF Executable and Library Format: Native in Linux and most UNIX flavors. ELF is not supported on OS X.
Script #! UNIX interpreters, or script: Used primarily for shell scripts, but also common for other inter- preters such as Perl, AWK, PHP, and so on. The kernel looks for the string following the #!, and executes it as a command. The rest of the file is passed to that command via standard input (stdin).
Universal (fat) binaries 0xcafebabe (Little-Endian)
0xbebafeca (Big-Endian)
Multiple-architecture binaries used exclusively in OS X.
Mach-O 0xfeedface (32-bit)
0xfeedfacf (64-bit)
OS X native binary format.

下面,我们从Mach-O格式说起。

注意:在本文涉及到的一些结构体定义或者类,出自于XNU源码。在本文末尾参考有下载地址。

一、Mach-O

Mach-O是Mach object的缩写,是macOS、iOS上用于存储程序、库的标准格式。

macOS系统基于Unix,Unix基本标准化了一个通用的可移植的二进制格式,称为Executable and Library Format(ELF)。该格式有良好的文档,还有一整套binutils工具用于维护和调试这个格式。

然而,苹果维护了自己独有的的二进制格式:Mach-O。

二、Mach-O基本结构

我们可以通过otool或者jtool,见macOS开发命令集来查看Mach-O的具体结构,但是我们通过GUI工具—MachOExplorer ,可以更直观的观察。

下面我们观察一个/usr/bin/curl

machoexploer_view

苹果对Mach-O的说明在这儿-Mach-O Programming Topics

一个Mach-O文件包含3个主要区域

区域 作用
Header 文件类型、目标架构类型等
Load commands 描述文件在虚拟内存中的逻辑结构、布局
Raw segment data 在Load commands中定义的Segment的原始数据

mach-section

2.1 Header

Mach-O结构体:

1
2
3
4
5
6
7
8
9
10
11
12
//<mach-o/loader.h>
struct mach_header {
//struct mach_header_64 64bit struct
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 only in 64bit*/
};

屏幕快照 2018-09-11 上午10.56.34

继续观察上面/usr/bin/curl在开头显示的Header。

perl_mach_header

其中第一个4字节显示的就是Magic Number,数据为FEEDFACF,表明是64位中的mach文件,并将其Value显示为MH_MAGIC_64,其中该宏就是源码中定义的:

#define MH_MAGIC_64 0xfeedfacf / the 64-bit mach magic number /

#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) /

2.1.1文件类型

<EXTERNAL_HEADERS/mach-o/loader.h>定义了下面类型,类型后面是十六进制值。

FILE TYPE USED FOR EXAMPLE
MH_OBJECT(0x1) Relocatable object files: inter- mediate compilation results, also 32-bit kernel extensions. (Generated with gcc –c)
MH_EXECUTABLE(0x2) Executable binaries. Binaries in /usr/bin, and application binary files (in Contents/MacOS)
MH_CORE(0x4) Core dumps. (Generated in a core dump)
MH_DYLIB(0x6) Dynamic Libraries. Libraries in /usr/lib, as well as frame- work binaries
MH_DYLINKER(0x7) Dynamic Linkers. /usr/lib/dyld
MH_BUNDLE(0x8) Plug-ins: Binaries that are not standalone but loaded into other binaries. These differ from DYLIB types in that they are explicitly loaded by the executable, usually by NSBundle (Objective-C) or CFBundle (C). (Generated with gcc –bundle) QuickLook plugins at /System/Library /QuickLook Spotlight Importers at /System /Library/Spotlight Automator actions at /System/Library /Automator
MH_DSYM(0xa) Companion symbol files and debug information. (Generated with gcc –g)
MH_KEXT_BUNDLE(0xb) Kernel extensions. 64-bit kernel extensions

其中在Xcode中查看target的Mach-O类型:图片 1

同样,对于/usr/bin/curl,其file type 为MH_EXECUTABLE,即可执行文件。

2.1.2 文件头其他标志

loader.h 中还可以找到 flags 的含义。

FILE TYPE USED FOR
MH_NOUNDEFS Objects with no undefined symbols. These are mostly static binaries, which have no further link dependencies
MH_SPLITSEGS Objects whose read-only segments have been separated from read- write ones.
MH_TWOLEVEL Two-level name binding (see “dyld features,” discussed later in the chapter).
MH_FORCEFLAT Flat namespace bindings (cannot occur with MH_TWOLEVEL).
MH_WEAK_DEFINES Binary uses (exports) weak symbols.
MH_BINDS_TO_WEAK Binary links with weak symbols.
MH_ALLOW_STACK_EXECUTION Allows the stack to be executable. Only valid in executables, but generally a bad idea. Executable stacks are conducive to code injec- tion in case of buffer overflows.
MH_PIE Allow Address Space Layout Randomization for executable types (see later in this chapter).
MH_NO_HEAP_EXECUTION Make the heap non-executable. Useful to prevent the “Heap spray” attack vector, wherein hackers overwrite large portions of the heap blindly with shellcode, and then jump blindly into an address therein, hoping to fall on their code and execute it.

MH_ALLOW_STACK_EXECUTIONMH_NO_HEAP_EXECUTION均用于放置某系额数据的执行,主要防止代码注入。

代码注入常见方法是使用栈变量(即自动变量),因为默认情况栈都标记为不可执行,堆则默认可执行。

两个设置都可以通过sysctl修改:vm.allow_stack_exec、vm.allow_heap_exec。

2.2 Load Command

​ Mach-O文件头的主要功能在于加载命令——load command,加载命令规定了文件的逻辑结构和文件在虚拟内存中的布局,这些指令在被调用时清晰地知道了如何设置并加载二进制数据。

​ 当运行一个可执行文件时,虚拟内存 (VM - virtual memory) 系统将 segment 映射到进程的地址空间上。映射完全不同于我们一般的认识,如果你对虚拟内存系统不熟悉,可以简单的想象虚拟内存系统将整个可执行文件加载进内存 – 虽然在实际上不是这样的。VM 使用了一些技巧来避免全部加载。

​ 当虚拟内存系统进行映射时,segment 和 section 会以不同的参数和权限被映射。

文件头的两个字段——ncmds(加载命令的总数)和sizeofncmds(加载命令的大小)——用于解析加载命令

加载命令紧跟在基本的mach_header之后,。

每条加载命令结果如下:

1
2
3
4
5
//<EXTERNAL_HEADERS/mach-o/loader.h>
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

2.2.1 内核处理的Mach-O加载命令

有一些命令由内核加载器(定义在bsd/kern/mach_loader.c)直接使用,其他命令则是由动态连接器处理。

下表是有内核处理的Mach-O加载命令

# COMMAND KERNEL HANDLER FUNCTION (bsd/kern/mach/loader.c) USED FOR
0x01 0x19 LC_SEGMENT LC_SEGMENT_64 load_segment Maps a (32- or 64-bit) segment of the file into the process address space. These are discussed in more detail in “process memory map.”
0x0E LC_LOAD_DYLINKER load_dylinker Invoke dyld (/usr/lib/dyld).
0x1B LC_UUID Kernel copies UUID into internal mach object representation Unique 128-bit ID. This matches a binary with its symbols
0x04 LC_THREAD load_thread Starts a Mach Thread, but does not allocate the stack (rarely used out- side core files).
0x05 LC_UNIXTHREAD load_unixthread Start a UNIX Thread (initial stack layout and registers). Usually, all reg- isters are zero, save for the instruc- tion pointer/program counter. This is deprecated as of Mountain Lion, replaced by dyld’s LC_MAIN.
0x1D LC_CODE_SIGNATURE load_code_signature Code Signing. (In OS X — occasion- ally used. In iOS — mandatory.)
0x21 LC_ENCRYPTION_INFO set_code_unprotect() Encrypted binaries. Also largely unused in OS X, but ubiquitous in iOS.

以上加载命令,我们会在后面详细讲述LC_SEGMENT_64LC_LOAD_DYLINKER,其他命令描述如下:

2.2.2 LC_MAIN

LC_MAINLC_UNIXTHREAD的替代者。

当所有库加载完成之后,dyld的工作就完成了,之后由LC_UNIXTHREAD命令负责启动二进制程序的主线程,主程序总是在可执行文件中,而不会在其他文件中。

根据架构的不同,该命令会列出所有初始化寄存器的状态,不同架构的寄存器状态不同。大部分寄存器都会初始化为0,存在指令指针或程序计算器里,这两个会保存程序的入口地址。

查看段和区,使用jtool,可以看到:

1
LC 11: LC_MAIN               	Entry Point:             0x5a5e (Mem: 0x100005a5e)

2.2.3 LC_CODE_SIGNATURE

Mach-O二进制文件的重要特性就是可以进行数字签名,在iOS进行代码签名是强制性的,但是在macOS没有强制,不过由于代码签名和沙盒机制绑定在一起,所以签名的使用率越来越高。

在macOS中,codesign工具可以用来操作和显示代码签名。比如GDB工具使用该工具进行本地代码签名。

LC_CODE_SIGNATURE包含了Mach-O二进制文件的代码签名,如果这个签名和代码本身不匹配,那么内核会立即给进程发送SIGKILL信号杀掉进行,毫不留情。

下面,就开始讲述其他重要的Load Comand命令。

2.3 段加载

2.3.1 segment

在说明段加载之前,先了解Mach-O中segment以及section的部分知识。

  • Mach-O 被划分成一些 segement,每个 segement 又被划分成一些 section。

  • segment 的名字都是大写的,且空间大小为页的整数。页的大小跟硬件有关,在 arm64 架构一页是 16KB,其余为 4KB。

  • section 虽然没有整数倍页大小的限制,但是 section 之间不会有重叠。

几乎所有 Mach-O 都包含这三个段(segment): __TEXT,__DATA和 __LINKEDIT,在下一篇Mach-O(二)内存分布会更详细的阐述。

那么,如何将segment加载到内存中呢?

2.3.2 LC_SEGMENT

LC_SEGMENT是加载segment最主要的加载命令,指导内核如何设置新运行的进程的内存空间。这些“Segment”直接从Mach-O二进制文件加载到内存中。

每一条LC_SEGMENT[_64] Segment的指令结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//<EXTERNAL_HEADERS/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; /* Virtual memory address of segment described */
uint64_t vmsize; /* Virtual memory allocated for this segment */
uint64_t fileoff; /* Marks the segment beginning offset in the file */
uint64_t filesize; /* Specifies how many bytes this segment occupies in the file */
vm_prot_t maxprot; /* Maximum memory protection for segment pages, in octal (4=r, 2=w, 1=x) */
vm_prot_t initprot; /* Initial memory protection for segment pages */
uint32_t nsects; /* Number of sections in segment, if any */
uint32_t flags; /* Miscellaneous bit flags */
};

对于每一个段,将文件中相应的内容加载到内存中:

  1. 从偏移量为fileoff处加载filesize字节到虚拟内存地址vmaddr处的vmsize字节。
  2. 每一个段的页面都根据initprot进行初始化,initprot指定了如果通过读/写执行位初始化页面的保护级别。段的保护设置可以动态改变,但是不能超过maxprot中制定的值。

2.4 动态库加载

在通过LC_SEGMENT将mach加载到内存后,就会继续加载动态库。

2.4.1 LC_LOAD_DYLINKER

启动动态连接器,用于链接动态库。通常情况下,使用/usr/lib/dyld为动态链接器,不过该命令可以制定任何程序作为参数。

链接器接管刚创建进程的控制权,因为内核将进程的入口点设置为链接器的入口点。

2.4.2 dyld处理

内核加载器中(bsd/kern/mach_loader.c)执行了设置工作,包括根据段的描述初始化进程地址空间,以及其他命令。仅有非常少的进程只需内核加载器就可以完成加载,macOS几乎所有的程序都是动态链接的。

Mach-O镜像中有对外部库和符号的引用需要在启动时填补,需要动态连接器来安插,这个过程也称为”符号绑定(binding)“。

LC_LOAD_DYLINKER 是内核加载dyld的命令,加载完成后,控制权在dyld手上,链接器扫描Mach-O文件头,从中寻找相关的加载命令。

dyld并不是内核一部分,是用户态的一个进程。所以,其处理的命令,不属于内核加载器加载。APP的可执行文件、动态库也是由它加载,常用于加载以下类型的Mach-O文件:

  • MH_EXECUTE

  • MH_DYLIB

  • MH_BUNDLE

其具体命令:

LOAD COMMAND USED FOR
0x02 0x0B LC_SYMTAB LC_DSYMTAB Symbol tables. The symbol tables and string tables are pro- vided separately, at an offset specified in these commands.
0x0C LC_LOAD_DYLIB Load additional dynamic libraries. This command super- sedes LC_LOAD_FVMLIB, used in NeXTSTEP.
0x20 LC_LAZY_LOAD_DYLIB As LC_LOAD_DYLIB, but defer actual loading until use of first symbol from library
0x0D LC_ID_DYLIB Found in dylibs only. Specifies the ID, the timestamp, ver- sion, and compatibility version of the dylib.
0x1F LC_REEXPORT_DYLIB Found in dynamic libraries only. Allows a library to re-export another library’s symbols as its own. This is how Cocoa and Carbon serve as umbrella frameworks for many others, as well as libSystem (which exports libraries in /usr/lib/ system).
0x24 0x25 LC_VERSION_MIN_IPHONEOS LC_VERSION_MIN_MACOSX Minimum operating system version expected for this binary. As of Lion, many binaries are set to 10.7 at a minimum.
0x26 LC_FUNCTION_STARTS Compressed table of function start addresses. New in Mountain Lion
0x2A LC_SOURCE_VERSION Version of source code used to build this binary. Informa- tional only and does not affect linking in any known way.
0x2B ?? (Name unknown) ` Code Signing sections from dylibs

其中LC_LOAD_DYLIB命令,我们在开发中最常接触,就是加载动态库的命令。

二进制文件若使用了外部定义的函数和符号,那么它们在文本段中会有一个名为__stubs(桩)的区,在这个区中存放的是这些本地未定义符号的占位符。编译器生成代码时会创建对符号桩区的调用,链接器在运行时会解决对桩的这些调用。链接器解决方法是,在被调用的地址处放置一条JMP命令。JMP指令将控制权转交给真实的函数体,但是不会以任何方式修改栈,所以真实的函数可以正常返回,就像直接调用一样。

LC_LOAD_DYLIB 命令会告诉链接器在哪找到这些未定义的符号,链接器会加载每一个制定的库,并搜寻匹配的符号。被链接的库有一个符号表,符号表将符号名称和地址关联起来。符号表在Mach-O目标文件中的地址可以通过LC_SYMTAB命令载入。

2.5 Raw segment data

这部分主要针对segment加载来的数据存放,在下一篇Mach-O(二)内存分布会更详细的阐述。

三、通用二进制格式

通用二进制格式,支持了多种架构的二进制文件的打包文件,所以也叫——胖二进制

也就是说,这种格式的文件包含了一个非常简单的文件头,文件头后面依次拷贝了每一种支持架构的二进制文件。

通过file我们可以查看支持的架构:file_arch

  • 因为需要储存多种架构的代码,通用二进制文件通常比单一平台二进制的程序要大;
  • 由于两种架构有共同的一些资源,所以并不会达到单一版本的两倍之多;
  • 由于执行过程中,只调用一部分代码,运行起来也不需要额外的内存;

  • 因为文件比原来的要大,也被称为“胖二进制文件”(Fat Binary)。

3.1 通用二进制格式

定义在<EXTERNAL_HEADERS/mach-o/fat.h>,Xnu源码在Apple source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define FAT_MAGIC	0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
uint32_t magic; /* FAT_MAGIC */
uint32_t nfat_arch; /* number of structs that follow */
};

struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint32_t offset; /* file offset to this object file */
uint32_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
};

fat_arch具体含义可以查看API

屏幕快照 2018-09-11 上午9.28.46

3.2 实验:通过lipo显示通用二进制文件的信息

通用二进制文件的处理工具——lipo,可以提取、删除或替换通用二进制文件中指定架构的二进制代码。因为可以用于对通用二进制文件进行瘦身。当然,我们也可以直接通过MachOExplore直接查看。

1
$ lipo -detailed_info /usr/bin/perl

lipo_mach_o_info

3.3 实验:多重架构以及通用(胖)二进制文件的处理

胖二进制会占用大量磁盘,但macOS会通过胖二进制的文件头结构自动挑选最合适底层硬件平台的二进制代码执行。

  1. Mach加载器解析胖二进制文件头,确定其中可用的架构。
  2. 通过cputype\cpusubtype加载合适的架构。

另外,胖二进制文件的镜像都做了优化,对齐页边界,因此内核只需要加载胖二进制的第一个页面就可以读取文件头,而这个文件头相当于一个目录,内核根据这个目录加载合适的镜像。

下面代码,模拟了部分内核API的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <mach-o/arch.h>
const char *ByteOrder(enum NXByteOrder BO){
switch (BO){
case NX_LittleEndian: return ("Little-Endian");
case NX_BigEndian: return ("Big-Endian");
case NX_UnknownByteOrder: return ("Unknown");default: return ("!?!");
}
}
int main(){
const NXArchInfo *local = NXGetLocalArchInfo(); //获取主机的架构信息
const NXArchInfo *known = NXGetAllArchInfos();
while (known && known->description){
printf ("Known: %s\t%x/%x\t%s\n", known->description,known->cputype, known->cpusubtype,ByteOrder(known->byteorder));known++;
}
if (local) {
printf ("Local - %s\t%x/%x\t%s\n", local->description,local->cputype, local->cpusubtype,ByteOrder(local->byteorder));
}
return(0);
}

四、库

4.1 共享库缓存

​ 当你构建一个真正的程序时,将会链接各种各样的库。它们又会依赖其他一些 framework 和 动态库。需要加载的动态库会非常多。而对于相互依赖的符号就更多了。可能将会有上千个符号需要解析处理,这将花费很长的时间:一般是好几秒钟。

​ 共享库缓存——shared library cache是dyld支持的另一个机制——一些库经过预先链接,,然后保存在磁盘上的一个文件中。

​ 为了缩短这个处理过程所花费时间,在 macOS 和 iOS 上的动态链接器使用了共享缓存。对于每一种架构,操作系统都有一个单独的文件,文件中包含了绝大多数的动态库,这些库都已经链接为一个文件,并且已经处理好了它们之间的符号关系。当加载一个 Mach-O 文件 (一个可执行文件或者一个库) 时,动态链接器首先会检查 共享缓存 看看是否存在其中,如果存在,那么就直接从共享缓存中拿出来使用。每一个进程都把这个共享缓存映射到了自己的地址空间中。这个方法大大优化了 macOS 和 iOS 上程序的启动时间。

macOS缓存目录/private/var/db/dyld目录下。如下:

屏幕快照 2018-09-11 下午5.43.51

在iOS中,目录为:/System/Library/Cache/com.apple.dyld,如果是模拟器,那么也可以找到:

~/Library/Developer/Xcode/iOS DeviceSupport/10.1.1 (14B100)/Symbols/System/Library/Caches/com.apple.dyld

屏幕快照 2018-09-11 下午5.47.38

相比iOS,macOS多了一个map辅助文件。

dyld缓存格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct _dyld_cache_header
{
char magic[16]; // e.g. "dyld_v0 i386"
uint32_t mappingOffset; // file offset to first dyld_cache_mapping_info
uint32_t mappingCount; // number of dyld_cache_mapping_info entries
uint32_t imagesOffset; // file offset to first dyld_cache_image_info
uint32_t imagesCount; // number of dyld_cache_image_info entries
uint64_t dyldBaseAddress; // base address of dyld when cache was built
uint64_t codeSignatureOffset; // file offset of code signature blob
uint64_t codeSignatureSize; // size of code signature blob (zero means to end of file)
uint64_t slideInfoOffset; // file offset of kernel slid info
uint64_t slideInfoSize; // size of kernel slid info
uint64_t localSymbolsOffset; // file offset of where local symbols are stored
uint64_t localSymbolsSize; // size of local symbols information
uint8_t uuid[16]; // unique value for each shared cache file
};

4.2 库的运行时加载

加载库的过程在<load.cpp>中有详细描述。

加载库有两种方式:

  1. 常规方式:#include导入,在链接是链接其他额外的库,这种方式构建的可执行文件只有在解决所有的依赖条件后才能加载执行。
  2. 运行时加载库:通过<dlfcn.h>提供的函数在运行时加载。
  • dlopen (const char *path) is used to find and load the library or bundle specified by path.
  • dlopen_preflight(const char *path) is a Leopard and later extension that simulates the loading process of dlopen() but does not actually load anything.
  • dlsym(void handle, char sym) is used to locate a symbol in a handle previously opened by dlopen().
  • dladdr(char addr, Dl_Info info) populates the DL_Info structure with the name of the bundle or library residing at address addr. This is the same as the GNU extension.
  • dlerror() is used to provide an error message in case of an error by any of the other functions.

Cocoa为dl*系列函数提供了高层包装,以及CFBundle/NSBundle对象,用于加载Mach-O bundle文件。

另外在<mach-o/dyld.h>中提供了更为底层的dyld API函数,可用于检测加载的库和符号,或者镜像的加载与移除的毁掉机制。

4.3 实验:列出进程中所有的Mach-O镜像

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
#include <dlfcn.h> // for dladdr(3)
#include <mach-o/dyld.h> // for _dyld_ functions

void listImages(void)
{
// List all mach-o images in a process
uint32_t i;
uint32_t ic = _dyld_image_count();
printf ("Got %d images\n",ic);
for (i = 0; i < ic; i++)
{
printf ("%d: %p\t%s\t(slide: %p)\n", i,
_dyld_get_image_header(i), _dyld_get_image_name(i), _dyld_get_image_slide(i));
}
}

void add_callback(const struct mach_header* mh, intptr_t vmaddr_slide)
{
// Using callbacks from dyld, we can get the same functionality // of enumerating the images in a binary
Dl_info info;
// Should really check return value of dladdr here...
dladdr(mh, &info);
printf ("Callback invoked for image: %p %s (slide: %p)\n", mh, info.dli_fname, vmaddr_slide);
}

void main (int argc, char **argv)
{
// Calling listImages will enumerate all Mach-O objects loaded into // our address space, using the _dyld functions from mach-o/dyld.h
listImages();
// Alternatively, we can register a callback on add. This callback // will also be invoked for existing images at this point.
_dyld_register_func_for_add_image(add_callback);

}

4.4 dyld特性

4.4.1 两级名称空间

dyld的独有特性,符号空间中还包括所在库的信息,通过”库-符号”两级确定符号,这样子就可让两个不同的库导出相同的符号,与其对应的是平坦名称空间。

4.4.2 函数拦截

DYLD_INTERPOSE宏定义允许一个库将其函数实现替换为另一个函数实现。以下代码取自dyld的源代码,演示了这个功能。

1
2
3
4
5
6
7
#if !defined(_DYLD_INTERPOSING_H_)
#define _DYLD_INTERPOSING_H_

#define DYLD_INTERPOSE(_replacment,_replacee) \ __attribute__((used)) static strut{const void* replacment;const void* replacee;}
_interpose_##_replace \ __attribute__((section ("__DATA,__interpose"))) = { (const void*)(unsigned long)&_replacement, (const void*)(unsigned long)&_replacee};

#endif

dyld的函数拦截功能提供了一个新的__DATA区,名为__interpose,在这个区中依次列出了替换函数和被替换的函数,其他事情就交由dyld处理了。 下面通过一个实验展示如何通过函数拦截机制跟踪malloc()函数

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
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <malloc/malloc.h>

//标准的interpose数据结构
typedef struct interpose_s{
void *new_func;
void *orig_fnc;
}interpose_t;

//我们的原型
void *my_malloc(int size);//对应真实的malloc函数
void my_free(void *);//对应真实的free函数

static const interpose_t interposing_functions[] \
__attribute__ ((section ("__DATA,__interpose"))) = {{(void *)my_free,(void *)free},{(void *)my_malloc,(void *)malloc}};

void *my_malloc (int size){
//在我们的函数中,要访问真正的malloc()函数,因为不想自己管理整个堆,所以就调用了原来的malloc()
void *returned = malloc(size);
//调用malloc_printf是因为printf中会调用malloc(),产生无限递归调用。
malloc_printf("+ %p %d\n",returned,size);
return (returned);
}

void my_free(void *freed){
malloc_printf("- %p\n",freed);
free(freed);
}

int main(int argc, const char * argv[]) {
// 释放内存——打印出地址,然后调用真正的free()
printf("Hello, World!\n");
return 0;
}

执行以下代码编译为dylib并强制插入ls中

1
2
$ cc -dynamiclib 1.c -o libMTrace.dylib -Wall
$ DYLD_INSERT_LIBRARIES=libMTrace.dylib ls

4.4.3 环境变量

ENVIRONMENT VARIABLE USE
DYLD_FORCE_FLAT_NAMESPACE Disable two-level namespace of libraries (for INSERT). Oth- erwise, symbol names also include their library name.
DYLD_IGNORE_PREBINDING Disable prebinding for performance testing.
DYLD_IMAGE_SUFFIX Search for libraries with this suffix. Commonly set to _debug, or _profile so as to load /usr /lib/libSystem.B_debug.dylib or /usr/lib /libSystem.B_profile instead of libSystem.
DYLD_INSERT_LIBRARIES Force insertion of one or more libraries on program load- ing — same idea as LD_PRELOAD on UN*X.
DYLD_LIBRARY_PATH Same as LD_LIBRARY_PATH on UN*X.
DYLD_FALLBACK_LIBRARY_PATH Used when DYLD_LIBRARY_PATH fails.
DYLD_FRAMEWORK_PATH As DYLD_LIBRARY_PATH, but for frameworks.
DYLD_FALLBACK_FRAMEWORK_PATH Used when DYLD_FRAMEWORK_PATH fails.

此外,下面的变量控制dyld调试打印选项。可以在Xcode中的Enviromment Variables。

  • DYLD_PRINT_APIS: Dump dyld API calls (for example dlopen). DYLD_PRINT_BINDINGS: Dump symbol bindings.
  • DYLD_PRINT_ENV: Dump initial environment variables.
  • DYLD_PRINT_INITIALIZERS: Dump library initialization (entry point) calls.
  • DYLD_PRINT_LIBRARIES: Show libraries as they are loaded.
  • DYLD_PRINT_LIBRARIES_POST_LAUNCH: Show libraries loaded dynamically, after load.
  • DYLD_PRINT_SEGMENTS: Dump segment mapping.
  • DYLD_PRINT_STATISTICS: Show runtime statistics.

实验:打印dyld加载的库

1
2
3
4
5
6
7
8
$ (export DYLD_PRINT_LIBRARIES=; ./a )
dyld: loaded: /Users/wenghengcong/Desktop/mach-o/./a
dyld: loaded: /usr/lib/libSystem.B.dylib
dyld: loaded: /usr/lib/system/libcache.dylib
....
dyld: loaded: /usr/lib/libc++.1.dylib
Save, Munde!
Vale

上面将会显示出在加载 Foundation 时,同时会加载的 许多动态库。这是由于 Foundation 依赖于另外一些动态库。运行下面的命令:

1
2
3
4
5
6
7
$ otool -L /System/Library/Frameworks/Foundation.framework/Foundation

/System/Library/Frameworks/Foundation.framework/Foundation:
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1452.23.0)
/usr/lib/libauto.dylib (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration (compatibility version 1.0.0, current version 1.0.0)
......

可以看到 Foundation 使用的 个动态库。

参考

工具

MachOExplorer 查看Mach-O 工具。

otool\jtool:参考macOS开发命令集

链接

  1. XNU source
  2. Mach-O Programming Topics
  3. Mach-O 可执行文件
  4. OS X Assembler Reference
  5. Mach Object Files
  6. OS X ABI Mach-O File Format Reference