Mach-O格式分析

推荐使用的工具和文档
Mach-O Hex View

文件类型 (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结构

参考链接