背景
目标文件,又称为可重定向目标文件,是源代码经编译之后的产物。目标文件是可执行文件、静态库、共享库的原材料。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
3900000000: 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) */
/*
* 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) */
不论是 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
4struct 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_command
的 offset
地址。main.o 文件的四个 load_command
如下,Load Commands 紧接着 mach_header,所以第一个 load_command
的 offset
就是 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 | struct section_64 { /* for 64-bit architectures */ |
解析如下,根据 sectname
为 __text
,可以看出这是一个代码段。根据 offset
和 size
可以知道该代码段在文件中的具体值
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
2000001d0: 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
60x000001d8 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
8struct 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 */
};
解析如下,从中我们可以得出符号表的 offset
是 0x00000258
,符号数量 1
,以及字符串表的 offset
是 0x00000268
, size
是 0x0000000b
。
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_sect
和 n_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 的偏移量 |
字符串表
从符号表解析得知字符串表的 offset
为 0x00000268
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
,这些值只有在链接的时候才能确定。
参考
- ascii 表
https://www.asciitable.com - apple 开源的 mach-o/loader.h
https://opensource.apple.com/source/cctools/cctools-795/include/mach-o/loader.h - 《深入理解计算机系统》
- 《程序员的自我修养》