Mach-O格式分析
推荐使用的工具和文档
- ImHex https://github.com/WerWolv/ImHex
- otool -h ./a.out
- brew install --cask machoview
- Hopper Disassembler
- XNU源码:GitHub

文件类型 (File Type)
MH_OBJECT - 目标文件(.o文件):编译器输出的可重定位目标代码。MH_EXECUTE - 可执行文件:标准二进制可执行程序。MH_DYLIB - 动态库(.dylib文件):类似Windows中的DLL。MH_DYLINKER - 动态链接器(/usr/lib/dyld)。Header结构
magic:魔数,用于标识这是一个Mach-O文件。
0xFEEDFACE- 32位(MH_MAGIC)0xFEEDFACF- 64位(MH_MAGIC_64)0xCEFAEDFE- 32位(MH_CIGAM,字节序反转)0xCFFAEDFE- 64位(MH_CIGAM_64)
cputype / cpusubtype:CPU架构和子类型。
CPU_TYPE_X86 (7)- x86(32位Intel)CPU_TYPE_X86_64 (7 | 0x1000000)- x86_64(64位Intel)CPU_TYPE_ARM (12)- ARM(32位)CPU_TYPE_ARM64 (12 | 0x1000000)- ARM64(Apple Silicon)
filetype:文件类型,见上方Card说明。
ncmds / sizeofcmds:加载命令的数量和总大小。
flags:标志位。常见的有:
MH_NOUNDEFS (0x1)- 无未定义符号MH_DYLDLINK (0x4)- 动态链接MH_TWOLEVEL (0x80)- 两级命名空间MH_PIE (0x200000)- 位置无关可执行文件
Load Commands(加载命令)
位于Header之后,每个命令由
struct load_command 结构体表示:常见Load Command类型
LC_SEGMENT / LC_SEGMENT_64
定义代码段、数据段等。通常是第一个Load Command。
主要段(Segment)
__TEXT- 代码段(只读,可执行)__DATA- 数据段(读写)__LINKEDIT- 链接信息
Section(节)
每个段包含多个Section:
常见Section
__TEXT.__text- 主程序代码__TEXT.__cstring- C语言字符串__TEXT.__const- 常量数据__DATA.__data- 已初始化全局变量__DATA.__bss- 未初始化全局变量__DATA.__common- 公共符号
LC_SYMTAB
符号表(Symbol Table),包含符号名称和地址。
每个符号由
struct nlist_64 表示:LC_DYSYMTAB
动态符号表,提供动态链接符号的信息。
LC_LOAD_DYLIB
指定程序依赖的动态库。
LC_CODE_SIGNATURE
代码签名信息。macOS要求所有可执行文件必须有有效的代码签名。
Hello World 反汇编分析
使用Hopper Disassembler反汇编a.out,得到以下ARM64汇编代码:
这段代码是使用ARM64汇编语言编写的,它实现了一个非常简单的功能:打印"Hello, World!"消息。下面是对这段代码的逐行解释:
```assembly
_main:
0000000100003f54 sub sp, sp, #0x20
```
- `sub sp, sp, #0x20`:将栈指针`sp`向下移动32字节(0x20),为局部变量和保存的寄存器分配空间。
```assembly
0000000100003f58 stp fp, lr, [sp, #0x10]
```
- `stp fp, lr, [sp, #0x10]`:将帧指针`fp`和链接寄存器`lr`保存到栈上,位置是栈指针`sp`加上16字节。
```assembly
0000000100003f5c add fp, sp, #0x10
```
- `add fp, sp, #0x10`:将帧指针`fp`设置为当前栈指针`sp`加上16字节,用于访问函数的局部变量。
```assembly
0000000100003f60 mov w8, #0x0
```
- `mov w8, #0x0`:将寄存器`w8`(即`x8`的低位32位)设置为0。
```assembly
0000000100003f64 str w8, [sp, #0x10 + var_C]
```
- `str w8, [sp, #0x10 + var_C]`:将寄存器`w8`的值存储到栈上,位置是`sp`加上16字节再减去局部变量`var_C`的偏移量。
```assembly
0000000100003f68 stur wzr, [fp, var_4]
```
- `stur wzr, [fp, var_4]`:将零寄存器`wzr`的值存储到帧指针`fp`减去局部变量`var_4`的偏移量的位置。
```assembly
0000000100003f6c str wzr, [sp, #0x10 + var_8]
```
- `str wzr, [sp, #0x10 + var_8]`:将零寄存器`wzr`的值存储到栈上,位置是`sp`加上16字节再减去局部变量`var_8`的偏移量。
```assembly
0000000100003f70 adrp x0, #0x100003000 ; 0x100003f98@PAGE
```
- `adrp x0, #0x100003000`:将寄存器`x0`设置为指向页面地址0x100003000的值。
```assembly
0000000100003f74 add x0, x0, #0xf98 ; 0x100003f98@PAGEOFF, argument "format" for method imp___stubs__printf, "Hello, World! \\n"
```
- `add x0, x0, #0xf98`:将寄存器`x0`的值加上0xf98,得到完整的地址0x100003f98,这个地址存储了字符串"Hello, World! \n"。
```assembly
0000000100003f78 bl imp___stubs__printf ; printf
```
- `bl imp___stubs__printf`:调用`printf`函数,打印`x0`寄存器指向的字符串。
```assembly
0000000100003f7c ldr w0, [sp, #0x10 + var_C]
```
- `ldr w0, [sp, #0x10 + var_C]`:从栈上加载之前存储的值到寄存器`w0`。
```assembly
0000000100003f80 ldp fp, lr, [sp, #0x10]
```
- `ldp fp, lr, [sp, #0x10]`:从栈上恢复帧指针`fp`和链接寄存器`lr`的值。
```assembly
0000000100003f84 add sp, sp, #0x20
```
- `add sp, sp, #0x20`:将栈指针`sp`向上移动32字节,恢复栈状态。
```assembly
0000000100003f88 ret
```
- `ret`:返回到调用函数的地方。
```assembly
; endp
```
- `; endp`:表示函数的结束。
总的来说,这段代码设置了一个栈帧,初始化了一些局部变量,调用`printf`函数打印"Hello, World!",然后清理栈帧并返回。这是一个典型的汇编语言程序结构,展示了如何进行函数调用和栈管理。
常用工具
otool- 查看Mach-O文件结构nm- 查看符号表lipo- 管理通用二进制文件(Universal Binary)codesign- 查看/验证代码签名- Hopper Disassembler / IDA Pro - 反汇编和静态分析
- MachOView - 可视化查看Mach-O结构