Mach-O

最近看了一下MachO文件,网上很多资料讲的也比较乱,但是讲的最透彻深入的是《深入解析OS X && iOS 操作系统》,这里结合这本经典书籍,做一个记录,并在第三部分通过一个实验验证动态符号绑定

进程是特殊文件在内存中加载得到的结果,这种文件必须是操作系统可理解的、可执行的格式。

可执行文件 魔数 用途
PE32/PETS2+ MZ 可移植的可执行文件: Windows和Intel二进制的原生格式。尽管OS X不支持这个格式,但是引导加载器支持这个格式,并且可以加载boot.efi文件
ELF \x7FELF 可执行文件和库文件的格式:Linux和大部分UNIX的原生格式。OS X不支持
脚本 #! UNIX脚本和一些解释器脚本是用的格式:主要用于shell脚本,但是也常用于其他解释器,例如Perl、AWK、PHP等。
通用二进制格式(胖二进制格式) 0xcafebabe 包含多种架构支持的二进制格式,只在OS X上支持
Mach-O 0xfeedface(32位)0xfeedfacf(64位) OS X原生二进制格式

在这些可执行文件中,OS X目前支持后三种,解释器脚本格式,实际上是一种特殊形式的二进制格式,因为这些文件只不过是只想真正二进制的脚本,而这些被指向的文件才是真正得到执行的可执行文件,因此我们只需要讨论两种格式——通用二进制格式和Mach-O格式。

1通用二进制格式

这种格式的基本思想是提供一种能够在任意架构上执行的完全可以移植的二进制格式。不过实际上通用二进制文件不过是其支持的各种架构文件的二进制文件的打包文件,所以叫『胖二进制』更合适。
通用二进制格式定义在头文件中。
通过下图,我们可以清楚的看到通用二进制的文件结构。文件通过fat header开始,然后跟着若干个fat arch,每个fat arch对应一个Mach-O文件。

image.png

其中fat header结构体:

#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */
struct fat_header {
uint32_t magic; /* FAT_MAGIC or FAT_MAGIC_64 */
uint32_t nfat_arch; /* number of structs that follow */
};
...
#define FAT_MAGIC_64 0xcafebabf
#define FAT_CIGAM_64 0xbfbafeca /* NXSwapLong(FAT_MAGIC_64) */

fat_header中第一部分就是魔数,代表文件类型,取固定值0xcafebabe(代表使用fat_arch32位结构)或 0xcafebabf(代表使用fat_arch64位结构)。第二个字段nfat_arch,代表接下来有几个fat_arch结构体,每个结构体对应一个Mach-O文件。

接下来是fat_arch结构体,分32位和64位的版本:

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

具体使用32位还是64位结构体,是通过fat header的magic值定义的(The magic value in the fat header will indicate which of these 32-bit or 64-bit structs to use,引用自《Apple_Debugging_and_Reverse_Engineering》)。
在fat_arch结构体中cputype代表支持的架构,例如i386、x86_64、arm64等,系统会根据cputype和cpusubtype挑选最匹配当前处理器的镜像(文件)。

实验:

Xcode是Mac上iOS app开发工具,我们以Xcode为例,来验证通用二进制文件结构。

我们可以通过file 命令来查看简要的架构信息

hanliqiangdeMacBook-Pro:~ hanliqiang$ file /Applications/Xcode.app/Contents/MacOS/Xcode
/Applications/Xcode.app/Contents/MacOS/Xcode: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
/Applications/Xcode.app/Contents/MacOS/Xcode (for architecture x86_64): Mach-O 64-bit executable x86_64
/Applications/Xcode.app/Contents/MacOS/Xcode (for architecture arm64):  Mach-O 64-bit executable arm64

我们可以通过MachOView查看一下Xcode的可执行文件结构


image.png

我们也可以通过otool命令来看一下,otool工具是mac自带的查看Mach-O文件的工具,适合分析加载命令和文本段。

image.png

可以看到Xcode支持两种架构,X86_64和arm64,这里比较有意思的就是Xcode竟然可以在arm架构上运行,以后可以在iPad上开发啦~~

2Mach-O文件

一个通用二进制格式包含了很多个 Mach-O 文件格式,下面我们来具体说说这个格式。Mach-O文件的结构如下图所示:


image.png

主要由三部分组成:

  • Mach-O header:Mach-O 头,用于表示支持什么架构,以及接下来有多少Load Command

  • Load Command:加载命令,表示动态加载器如何加载程序

  • Data:数据区,包含程序代码,数据,以及动态链接器使用的符号和其他表等

2.1 Mach-O header

Mach Header的定义可以在中找到。结构体定义如下:

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) */

struct mach_header_64 {
    uint32_t    magic;        /* mach magic 标识符 */
    cpu_type_t    cputype;    /* CPU 类型标识符,同通用二进制格式中的定义 */
    cpu_subtype_t    cpusubtype;    /* CPU 子类型标识符,同通用二级制格式中的定义 */
    uint32_t    filetype;    /* 文件类型 */
    uint32_t    ncmds;        /* 加载器中加载命令的条数 */
    uint32_t    sizeofcmds;    /* 加载器中加载命令的总大小 */
    uint32_t    flags;        /* dyld 的标志 */
    uint32_t    reserved;    /* 64 位的保留字段 */
};
#define MH_MAGIC_64 0xfeedfacf /*the 64-bit mach magic number*/ 
#define MH_CIGAM_64 0xcffaedfe /*NXSwapInt(MH_MAGIC_64)*/

magic 取值0xfeedface(32位系统)或0xfeedfacf(64位系统),用于标识文件类型。
cputype 代表支持的架构
ncmds 标识第二部分加载命令的数量

有针对32位系统的mach_header和针对64位系统的mach_header_64两种结构。

由于 Mach-O 支持多种类型文件,所以此处引入了 filetype 字段来标明,这些文件类型定义在 loader.h 文件中同样可以找到。

#define    MH_OBJECT    0x1        /* Target 文件:编译器对源码编译后得到的中间结果 */
#define    MH_EXECUTE    0x2        /* 可执行二进制文件 */
#define    MH_FVMLIB    0x3        /* VM 共享库文件(还不清楚是什么东西) */
#define    MH_CORE        0x4        /* Core 文件,一般在 App Crash 产生 */
#define    MH_PRELOAD    0x5        /* preloaded executable file */
#define    MH_DYLIB    0x6        /* 动态库 */
#define    MH_DYLINKER    0x7        /* 动态连接器 /usr/lib/dyld */
#define    MH_BUNDLE    0x8        /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */
#define    MH_DYLIB_STUB    0x9        /* 静态链接文件(还不清楚是什么东西) */
#define    MH_DSYM        0xa        /* 符号文件以及调试信息,在解析堆栈符号中常用 */
#define    MH_KEXT_BUNDLE    0xb        /* x86_64 内核扩展 */

2.2 Load Command

每条命令都采用"类型-长度-值"的格式,32位cmd值,32位cmdsize,以及命令本身。有一些命令是由内核加载器直接使用的,其他命令是有动态连接器处理的。
下边列举一些内核中使用的加载命令

# 命令 内核中处理的函数 用途
0x01 0x19 LC_SEGMENT[_64] load_segment 将文件中的段映射到进程地址空间中
0x0E LC_LOAD_DYLINKER load_dylinker 调用dyld
0x1B LC_UUID 内核将UUID复制到内部表示mach目标的数据中 一个唯一的128位ID。这个ID匹配一个二进制文件及对应的符号
0x04 LC_THREAD load_thread 开启一个Mach线程,但不分配栈,很少在核心转储文件之外使用
0x05 LC_UNIXTHREAD load_unixthread 当所有库加载完,dyld的工作也加载完,之后LC_UNIXTHREAD命令负责启动二进制程序的主线程
0x1D LC_CODE_SIGNATURE load_code_signature 代码签名。OS X中很少使用,但在iOS中强制使用
0x21 LC_CENCRYPTION_INFO set_code_unprotect() 加密二进制文件。在OS X中几乎不加密,但是在iOS中却很普遍

LC_SEGMENT命令是最主要的加载命令,这条命令指导如何设置新进程的内存空间,每一条LC_SEGMENT[_64]命令都提供了段布局的所有必要细节信息。并且如果有section的情况下,还会紧跟着section数据结构说明。
我们以LC_SEGMENT是一个结构体,以64位为例:

struct segment_command_64 { 
    uint32_t    cmd;        /* LC_SEGMENT_64 */
    uint32_t    cmdsize;    /* section_64 结构体所需要的空间 */
    char        segname[16];    /* segment 名字 */
    uint64_t    vmaddr;        /* 所描述段的虚拟内存地址 */
    uint64_t    vmsize;        /* 为当前段分配的虚拟内存大小 */
    uint64_t    fileoff;    /* 当前段在文件中的偏移量 */
    uint64_t    filesize;    /* 当前段在文件中占用的字节 */
    vm_prot_t    maxprot;    /* 段所在页所需要的最高内存保护,用八进制表示 */
    vm_prot_t    initprot;    /* 段所在页原始内存保护 */
    uint32_t    nsects;        /* 段中 Section 数量 */
    uint32_t    flags;        /* 标识符 */
};

有了LC_SEGMENT[_64]命令,设置进程虚拟内存的过程就变成遵循LC_SEGMENT[_64]命令的简单操作。

可以通过MachOView看一下Safari中的LC_SEGMENT_64命令


image.png

2.3 Data区域

Mach-O 的 Data 区域,由 Segment 段和 Section 节组成。Segment是具有特殊访问权限的内存区域,同一个 Segment 下的 Section,可以控制相同的权限,一个Segment可以包含0个或者多个Section。
主要分为下边几个区:

  • __PAGEZERO segment 空指针陷阱
  • __TEXT segment 程序代码
  • __DATA segment 程序数据
  • __LINKEDIT Segment 包含需要被动态链接器使用的符号和其他表等

下边列举一些常见的端和区

用途
__TEXT 主程序代码
__TEXT.__text 主程序代码
__TEXT.__cstring 程序中硬编码的C 语言字符串
__TEXT.__const const 关键字修饰的常量
__TEXT.__stubs 用于 Stub 的占位代码,很多地方称之为桩代码。
__TEXT.__stubs_helper 当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methname Objective-C 方法名称
__TEXT.__objc_methtype Objective-C 方法类型
__TEXT.__objc_classname Objective-C 类名称
__DATA.__data 初始化过的可变数据
__DATA.__la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA.nl_symbol_ptr 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__DATA.__const 没有初始化过的常量
__DATA.__cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs)
__DATA.__bss BSS,存放为初始化的全局变量,即常说的静态内存分配
__DATA.__common 没有初始化过的符号声明
__DATA.__objc_classlist Objective-C 类列表
__DATA.__objc_protolist Objective-C 原型
__DATA.__objc_imginfo Objective-C 镜像信息
__DATA.__objc_selfrefs Objective-C self 引用
__DATA.__objc_protorefs Objective-C 原型引用
__DATA.__objc_superrefs Objective-C 超类引用

3 实验: 通过对__DATA.__la_symbol_ptr 的探究查看符号和观察加载过程

这里通过一个例子来验证。我们写一个简单的测试程序helloworld

#include 
int main(int argc, char *argv[])
{
    printf("Hello World!\n");
    printf("Hello china!\n");
    return 0;
}

并通过xcrun clang helloworld.c编译运行这个程序,编译之后会产生一个名字叫a.out的可执行文件。

我们通过MachOView来看一下这个文件的__Text.__stubs区域

  • 我们看到有一个符号_printf
  • stubs在TEXT区域,这里的数据是0xFF2588400000,是一段汇编代码的16进制表示


    image.png

使用Hopper打开a.out查看反汇编代码

image.png

可以看到是一句jmp跳转指令,含义就是跳转到_printf指向的代码

在Hopper中双击跳转链接,跳转到__la_symbol_ptr区域,这里是DATA区域,这里就代表__Text.__stubs区域的符号指针的定义和取值。我们看到了刚才的符号_printf,所以这里的内存区域就是符号指针_printf,这块内存区域的数值就是符号指针的取值

image.png

继续点击,跳转的地址全是0,这里是Hopper的bug


image.png

我们使用MachOView查看__la_symbol_ptr区域

image.png

我们看到符号指针_printf的值是0x0000000100003F88

在Hopper中查看0x0000000100003F88


image.png

可以看到0x0000000100003F88在__TEXT.__stub_helper区域,并且对应的指令是压栈,以及jmp指令。
程序继续跳转到__stub_helper区域的起始地址处执行了


image.png

接下来又是jmp指令,但是这里的jmp会跳转到dyld_stub_binder处执行,动态连接器会动态找到本次需要执行的代码,同时将找到的要执行的代码的地址赋值给__DATA.__la_symbol_ptr区域对应的符号,符号也就完成动态绑定,下次执行相同的桩代码,程序会直接跳转到目的代码处执行,不需要再次调用dyld_stub_binder

如果继续跟踪这个程序,在main函数的两处printf出设置断点,可以验证上边的结论。在第一次调用桩代码会调用到dyld_stub_binder,第二次调用桩代码,会直接跳转到printf函数地址,也就是说__la_symbol_ptr对应表项的符号完成了动态绑定。

image.png

可以看到第一次的打印和第二次不一样,第二次已经变成函数的地址

下边是通过LLDB调试验证的过程

hanliqiangdeMacBook-Pro:objcio-command-line hanliqiang$ otool -p _main -tV a.out
a.out:
(__TEXT,__text) section
_main:
0000000100003f30    pushq   %rbp
0000000100003f31    movq    %rsp, %rbp
0000000100003f34    subq    $0x20, %rsp
0000000100003f38    movl    $0x0, -0x4(%rbp)
0000000100003f3f    movl    %edi, -0x8(%rbp)
0000000100003f42    movq    %rsi, -0x10(%rbp)
0000000100003f46    leaq    0x45(%rip), %rdi        ## literal pool for: "Hello World!\n"
0000000100003f4d    movb    $0x0, %al
0000000100003f4f    callq   0x100003f72             ## symbol stub for: _printf
0000000100003f54    leaq    0x45(%rip), %rdi        ## literal pool for: "Hello china!\n"
0000000100003f5b    movl    %eax, -0x14(%rbp)
0000000100003f5e    movb    $0x0, %al
0000000100003f60    callq   0x100003f72             ## symbol stub for: _printf
0000000100003f65    xorl    %ecx, %ecx
0000000100003f67    movl    %eax, -0x18(%rbp)
0000000100003f6a    movl    %ecx, %eax
0000000100003f6c    addq    $0x20, %rsp
0000000100003f70    popq    %rbp
0000000100003f71    retq
hanliqiangdeMacBook-Pro:objcio-command-line hanliqiang$ lldb ./a.out
(lldb) target create "./a.out"
Current executable set to '/Users/hanliqiang/Desktop/objcio-command-line/a.out' (x86_64).
(lldb) b main
Breakpoint 1: where = a.out`main, address = 0x0000000100003f30
(lldb) r
Process 21392 launched: '/Users/hanliqiang/Desktop/objcio-command-line/a.out' (x86_64)
Process 21392 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100003f30 a.out` main
a.out`main:
->  0x100003f30 <+0>:  push   rbp
    0x100003f31 <+1>:  mov    rbp, rsp
    0x100003f34 <+4>:  sub    rsp, 0x20
    0x100003f38 <+8>:  mov    dword ptr [rbp - 0x4], 0x0
    0x100003f3f <+15>: mov    dword ptr [rbp - 0x8], edi
    0x100003f42 <+18>: mov    qword ptr [rbp - 0x10], rsi
    0x100003f46 <+22>: lea    rdi, [rip + 0x45]         ; "Hello World!\n"
    0x100003f4d <+29>: mov    al, 0x0
Target 0: (a.out) stopped.
(lldb) b 0x100003f4f
Breakpoint 2: where = a.out`main + 31, address = 0x0000000100003f4f
(lldb) b 0x100003f60
Breakpoint 3: where = a.out`main + 48, address = 0x0000000100003f60
(lldb) c
Process 21392 resuming
Process 21392 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x0000000100003f4f a.out` main  + 31
a.out`main:
->  0x100003f4f <+31>: call   0x100003f72               ; symbol stub for: printf
    0x100003f54 <+36>: lea    rdi, [rip + 0x45]         ; "Hello china!\n"
    0x100003f5b <+43>: mov    dword ptr [rbp - 0x14], eax
    0x100003f5e <+46>: mov    al, 0x0
    0x100003f60 <+48>: call   0x100003f72               ; symbol stub for: printf
    0x100003f65 <+53>: xor    ecx, ecx
    0x100003f67 <+55>: mov    dword ptr [rbp - 0x18], eax
    0x100003f6a <+58>: mov    eax, ecx
Target 0: (a.out) stopped.
(lldb) x/g 0x100008000
0x100008000: 0x0000000100003f88 ##可以看到这里指向stubs_helper代码区
(lldb) c
Process 21392 resuming
Hello World!
Process 21392 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x0000000100003f60 a.out` main  + 48
a.out`main:
->  0x100003f60 <+48>: call   0x100003f72               ; symbol stub for: printf
    0x100003f65 <+53>: xor    ecx, ecx
    0x100003f67 <+55>: mov    dword ptr [rbp - 0x18], eax
    0x100003f6a <+58>: mov    eax, ecx
    0x100003f6c <+60>: add    rsp, 0x20
    0x100003f70 <+64>: pop    rbp
    0x100003f71 <+65>: ret
a.out'printf:    0x100003f72 <+0>: jmp    qword ptr [rip + 0x4088]  ; (void *)0x00007fff6a3e3370: printf
Target 0: (a.out) stopped.
(lldb) x/g 0x100008000
0x100008000: 0x00007fff6a3e3370 ##可以看到这里已经变为时机函数地址
(lldb)

参考文献:《深入解析OS X && iOS 操作系统》
《Apple_Debugging_and_Reverse_Engineering》

你可能感兴趣的:(Mach-O)