蜀道之难,难于上青天

0%

Mach-O 可重定向目标文件剖析

背景

目标文件,又称为可重定向目标文件,是源代码经编译之后的产物。目标文件是可执行文件、静态库、共享库的原材料。MacOS 系统的目标文件采用的是 Mach-O 格式。本文以一个简单的可重定向文件为例来描述 Mach-O 格式。

生成目标文件

新建 main.c 文件

1
2
3
4
//main.c
int main(int argc, char const *argv[]){
return 2;
}

编译 main.c 得到 main.o 文件。

1
$ gcc -c main.c -o main.o

查看目标文件

文件的本质,包括 main.o 在内,实际上是数据的集合。通过 xxd 查看该文件的十六进制形式

1
xxd main.o

main.o 的十六进制形式的完整数据如下,其中第一列的十六进制数字代表所在行的偏移量,用来方便的定位数据;第二列到第九列为文件数据;最右为数据的 ASCII 表示,非 ASCII 码显示为 .。比如第一行开始数据 cf 的偏移量为 0x00000000,数据 fa 的偏移量则为 0x00000001。了解了数据以及偏移量,我们就知道如何读文件了,在下文我会用 offset 代表偏移量。接下来我们可以按照字节来解析 main.o 文件了。
这种原始的读取方式,可以让我们加深对文件的理解,读者也可以采用 MachOView 来查看,更加直观方便。

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
00000000: cffa edfe 0700 0001 0300 0000 0100 0000  ................
00000010: 0400 0000 b801 0000 0020 0000 0000 0000 ......... ......
00000020: 1900 0000 3801 0000 0000 0000 0000 0000 ....8...........
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 7800 0000 0000 0000 d801 0000 0000 0000 x...............
00000050: 7800 0000 0000 0000 0700 0000 0700 0000 x...............
00000060: 0300 0000 0000 0000 5f5f 7465 7874 0000 ........__text..
00000070: 0000 0000 0000 0000 5f5f 5445 5854 0000 ........__TEXT..
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000090: 1200 0000 0000 0000 d801 0000 0400 0000 ................
000000a0: 0000 0000 0000 0000 0004 0080 0000 0000 ................
000000b0: 0000 0000 0000 0000 5f5f 636f 6d70 6163 ........__compac
000000c0: 745f 756e 7769 6e64 5f5f 4c44 0000 0000 t_unwind__LD....
000000d0: 0000 0000 0000 0000 1800 0000 0000 0000 ................
000000e0: 2000 0000 0000 0000 f001 0000 0300 0000 ...............
000000f0: 5002 0000 0100 0000 0000 0002 0000 0000 P...............
00000100: 0000 0000 0000 0000 5f5f 6568 5f66 7261 ........__eh_fra
00000110: 6d65 0000 0000 0000 5f5f 5445 5854 0000 me......__TEXT..
00000120: 0000 0000 0000 0000 3800 0000 0000 0000 ........8.......
00000130: 4000 0000 0000 0000 1002 0000 0300 0000 @...............
00000140: 0000 0000 0000 0000 0b00 0068 0000 0000 ...........h....
00000150: 0000 0000 0000 0000 3200 0000 1800 0000 ........2.......
00000160: 0100 0000 000f 0a00 040f 0a00 0000 0000 ................
00000170: 0200 0000 1800 0000 5802 0000 0100 0000 ........X.......
00000180: 6802 0000 0800 0000 0b00 0000 5000 0000 h...........P...
00000190: 0000 0000 0000 0000 0000 0000 0100 0000 ................
000001a0: 0100 0000 0000 0000 0000 0000 0000 0000 ................
000001b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000001c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000001d0: 0000 0000 0000 0000 5548 89e5 c745 fc00 ........UH...E..
000001e0: 0000 00b8 0200 0000 5dc3 0000 0000 0000 ........].......
000001f0: 0000 0000 0000 0000 1200 0000 0000 0001 ................
00000200: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000210: 1400 0000 0000 0000 017a 5200 0178 1001 .........zR..x..
00000220: 100c 0708 9001 0000 2400 0000 1c00 0000 ........$.......
00000230: a8ff ffff ffff ffff 1200 0000 0000 0000 ................
00000240: 0041 0e10 8602 430d 0600 0000 0000 0000 .A....C.........
00000250: 0000 0000 0100 0006 0100 0000 0f01 0000 ................
00000260: 0000 0000 0000 0000 005f 6d61 696e 0000 ........._main..

Mach Header

Mach-O 文件的开头是个 header,32 位下按照结构体 mach_header 布局,64 位下是 mach_header_64。可以在 apple 开源的 mach-o/loader.h 里找到详细的结构体信息。

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
/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
struct mach_header {
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 */
};

/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */

/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
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 */
};

/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */

不论是 mach_header 还是 mach_header_64,其开头都是一个 uint32_t 的成员 magic,读文件前 4 个字节来查看 magic 的值

1
00000000: cffa edfe

cffa edfe,即 MH_CIGAM_64, 需要按照小端序的方式来解析该文件。确定了读取方式之后,可以参照 mach_header_64 接着往下解析,读出其余成员的值。

name offset value description
magic 0x0 0xcffaedfe MH_CIGAM_64
cputype 0x4 0x01000007 CPU_TYPE_X86_64
cpusubtype: 0x8 0x00000003 CPU_SUBTYPE_X86_64_ALL
filetype 0xc 0x00000001 MH_OBJECT,代表可重定位目标文件
ncmds 0x10 0x00000004 代表有 4 条 Load Commands
sizeofcmds 0x14 0x000001b8 表示 Load Commands 的size
flags 0x18 0x00002000 MH_SUBSECTIONS_VIA_SYMBOLS
reserved 0x1c 0x00000000 保留字

Load Commands

紧接着 mach_header,就是 Load Commands 了。同样是先读取头文件,从 ncmds 可以看出接下来会存在 4 个 load_command ,其结构如下

1
2
3
4
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 的 size,通过 offset + size 可以得出下个 load_commandoffset 地址。main.o 文件的四个 load_command 如下,Load Commands 紧接着 mach_header,所以第一个 load_commandoffset 就是 mach_header_64 的 size 0x20

index cmd offset cmd value cmdsize offset cmdsize value type next cmd offset
0 0x00000020 0x0000019 0x00000024 0x00000138 LC_SEGMENT_64 0x00000158
1 0x00000158 0x00000032 0x0000015c 0x00000018 LC_BUILD_VERSION 0x00000170
2 0x00000170 0x00000002 0x0000174 0x00000018 LC_SYMTAB 0x00000188
3 0x00000188 0x0000000b 0x000018c 0x00000050 LC_DYSYMTAB

LC_SEGMENT_64

第一个 load_command 类型是 LC_SEGMENT_64,从 mach-o/loader.h 可以查到其数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* The 64-bit segment load command indicates that a part of this file is to be
* mapped into a 64-bit task's address space. If the 64-bit segment has
* sections then section_64 structures directly follow the 64-bit segment
* command and their size is reflected in cmdsize.
*/
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 */
};

解析如下,根据 nsects 可以看出该 segment 包含有 3 个 Section,接下来我们看 Section。

name offset value description
cmd 0x00000020 0x00000019 LC_SEGMENT_64
cmdsize 0x00000024 0x00000138 该 command 所占的 size
segname 0x00000028 0 16 位长度的字符串
vmaddr 0x00000038 0 VM Address
vmsize 0x00000040 0x0000000000000078 VM Size
fileoff 0x00000048 0x00000000000001d8 main.o 文件里的偏移量 offset
filesize 0x00000050 0x0000000000000078 File Size
maxprot 0x00000058 0x00000007 Maximum VM Protection,VM_PORT_READ,VM_PORT_WRITE,VM_PORT_EXECUTE
initprot 0x0000005c 0x00000007 Maximum VM Protection,VM_PORT_READ,VM_PORT_WRITE,VM_PORT_EXECUTE
nsects 0x00000060 0x00000003 Section 的数量,包含 3 个 Section
flags 0x00000064 0x00000000 Flags

代码段 __TEXT

同样,第一个 Section 的 header 紧挨着 segment_command_64,从 0x00000068 开始解析,其结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,可以看出这是一个代码段。根据 offsetsize 可以知道该代码段在文件中的具体值

name offset value description
sectname 0x00000068 5f5f 7465 7874 0000 0000 0000 0000 0000 ascii 解析为 “__text”
segname 0x00000078 5f5f 5445 5854 0000 0000 0000 0000 0000 ascii 解析为 “__TEXT”
addr 0x00000088 0x0000000000000000 Address,内存地址,目前为 0,链接时会重定向
size 0x00000090 0x0000000000000012 Size,该 Section 的 size
offset 0x00000098 0x000001d8 该 Section 的 offset
align 0x0000009c 0x00000004 内存对齐数值
reloff 0x000000a0 0x00000000 Relocations Offset,重定向偏移地址
nreloc 0x000000a4 0x00000000 Number of Relocations,重定向数
flags 0x000000a8 0x80000400 Flags,S_REGULAR,S_ATTR_PURE_INSTRUCTIONS,S_ATTR_SOME_INSTRUCTIONS
reserved1 0x000000ac 0x00000000 Reserved1
reserved2 0x000000b0 0x00000000 Reserved2
reserved3 0x000000b4 0x00000000 Reserved3

我们从 0x000001d8 来解析 0x12 个字节

1
2
000001d0: 0000 0000 0000 0000 5548 89e5 c745 fc00  ........UH...E..
000001e0: 0000 00b8 0200 0000 5dc3 0000 0000 0000 ........].......

这是一段机器码,翻译为汇编如下,可以看出这个与 main.c 里的 main 函数的函数体对应。

1
2
3
4
5
6
0x000001d8 55            pushq %rbp
0x000001d9 4889e5 movq %rsp,%rbp
0x000001dc c745fc00000000 movl $"0x0 (_main)",-0x4(%rbp)
0x000001e3 b80x000000 movl $0x2,%eax
0x000001e8 5d popq %rbp
0x000001e9 c3 ret

符号表 LC_SYMTAB

第三个 Load Command 表示符号表,对应的结构体如下

1
2
3
4
5
6
7
8
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};

解析如下,从中我们可以得出符号表的 offset0x00000258 ,符号数量 1 ,以及字符串表的 offset0x00000268size0x0000000b

name offset value description
cmd 0x00000170 0x00000002 LC_SYMTAB
cmdsize 0x00000174 0x00000018 Command Size
symoff 0x00000178 0x00000258 符号表在文件中的 offset
nsyms 0x0000017c 0x00000001 符号数量
stroff 0x00000180 0x00000268 字符串表在文件中的 offset
strsize 0x00000184 0x0000000b 字符串表长度

我们从 0x00000258 来查看符号表,根据结构体 nlist_64 来解析。

1
2
3
4
5
6
7
8
9
10
11
12
/*
* This is the symbol table entry structure for 64-bit architectures.
*/
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};

解析如下,可以根据 n_sectn_value 得到该符号的具体位置,也就是 0x000001d8,根据 n_un.n_strx 从字符串表得到符号名。

name offset value description
n_un.n_strx 0x00000258 0x00000001 在字符串表中的偏移量
n_type 0x0000025c 0x0000000f 代表 N_EXT,N_SECT。N_EXT 代表支持外部符号引用,N_SECT 表示该符号定义在索引为 n_sect 的 Section
n_sect 0x0000025d 0x00000001 符号表在 Section 里的索引号
n_desc 0x0000025e 0x0000
n_value 0x00000260 0x0000000000000000 所在 Section 的偏移量

字符串表

从符号表解析得知字符串表的 offset0x00000268

1
0x00000268 00 5f 6d 61 69 6e 00 00

而唯一的符号,在字符串表的位置是 1,从 0x269 开始读,读到 00 终止,根据 ascii 可以得知其符号名为 _main,对应源文件里的main函数。

其他 Section

第一个 Segment 还包括的 Section 有 __compact_unwind、__eh_frame、reloadcations,跟 debug 调试有关,这里就不多讲了。

总结

以上,我们可以得知 main.o 为 x86_64 平台下的 Mach-O 文件,包含了符号 _main 及其代码段,映射到 main.c 里就是 main 函数。以及一些尚未确定的值,如 vmaddr,这些值只有在链接的时候才能确定。

参考