Mach-O

PE32/PE32+WindowsIntelEFI 的二进制文件的可执行文件格式,ELFLinuxUnix 的可执行文件格式,通用二进制格式(胖二进制格式)只在 OS X 上支持,Mach-OOS X 的原生可执行文件格式。在 Unix 中,任何文件都可以通过简单的 chmod +x 命令标记为可执行文件,但是不一定能保证这个文件可以执行。OS X 目前只支持三种可执行文件格式:解释器脚本格式、通用二进制格式以及 Mach-O 格式。

可执行文件格式 magic(魔数) 用途
脚本 #! UNIX 脚本和一些解释器脚本的使用格式:主要用于 shell 脚本,但是也常用于其他解释器,例如 PerlAWKPHP 等。内核寻找 #! 后面跟着的字符串,然后执行这个字符串表示的命令。文件剩下的部分通过标准输入(stdin)传递这个命令
通用二进制格式(胖二进制格式) 0xcafebabe(小尾顺序) 0xbebafeca(大尾顺序) 包含多种架构支持的二进制格式,只在 OS X 上支持
Mach-O 0xfeedface(32位) 0xfeedfacf(64位) OS X 的原生二进制格式

Mach-O 文件格式

据说了解 Mach-O 最佳方法是查看下面的图片:

Mach-O 格式.png

通过上图可以看出 Mach-O 由三部分构成:

  • Header (mach_header) : 包含 mach_headerCPU 类型、文件类型、加载命令、动态连接器的标志等
  • 加载命令(Load command) : 加载命令紧跟头文件之后,这些命令在调用时清晰的告诉加载器如何设置并加载二进制数据。有一些命令是有内核加载器(定义在 bsd/kern/mach_loader.c 文件中)直接使用的,其他命令是由动态链接器处理的。
  • Data : 每一个 segment 的具体数据都保存在这里,这里包含了具体的代码、数据等

mach_header

Mach-O 具有一个固定的 mach_header,这个文件头的详细信息在 <mach-o/loader.h> 头文件中。 文件头一开始有一个魔数值(magic),加载器可以通过这个魔数值快读判断这个二进制文件用于32位还是64位。在魔数值之后跟着的是 CPU 类型及子类型字段,这两个字段和通用二进制文件的相同字段作用是一样的(用于确保二进制文件适用并且可以在当前架构下运行)。除此之外32位架构和64位架构的文件头结构没有实质差别:除了64位的头文件还包含了一个额外的预留字段,这个字段暂时没有使用。

32位结构:

/*
 * 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 */
};

64位结构:

/*
 * 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 */
};
字段 说明
magic #define MH_MAGIC 0xfeedface 表示32位二进制 , #define MH_MAGIC_64 0xfeedfacf 表示64位二进制
cputype CPU 类型,如arm
cpusubtype CPU 子类型,也是对应的具体类型,如 arm64armv7
filetype 文件类型,如可执行文件、库文件、核心转储文件、内核扩展等
ncmds 用于加载器的“加载命令”的条数
sizeofcmds 所有加载命令的大小
flags 动态连接器(dylib)加载时所需要的标志位
reserved 仅限64位:保留给未来使用

下面是 MachOView 分析的图

image.png

filetype

filetype 用于表示目标文件的类型,这个字段的可取值以宏的形式定义在 <mach-o/loader.h> 中。

  • 文件的布局取决于文件的类型,除了 MH_OBJECT 以外的所有文件类型段被填充并在 segment 上对齐。
  • MH_EXECUTEMH_FVMLIBMH_DYLIBMH_DYLINKERMH_BUNDLE 文件类型的头文件也包含在他们的第一 segment 中。
  • 文件类型 MH_OBJECT 是一个紧凑的格式,用于输出链接编辑器(.o)的汇编程序和输入(可能还有输出)格式。所有部分都在一个没有段填充的未命名段中。当文件很小的时候,这种格式被用作可执行格式段填充大大增加了它的大小。
  • 文件类型 MH_PRELOAD 是一种可执行格式,不在内核下执行(promsstandaloneskernel等)操作。可执行文件可以在内核下执行,但可能需要分页并且不需要预加载。
  • 核心文件是 MH_CORE 格式的,可以是 arbritray 中的任何合法的 Mach-O 文件。
 *
 * Constants for the filetype field of the mach_header
 */
#define    MH_OBJECT    0x1        /* relocatable object file */
#define    MH_EXECUTE    0x2        /* demand paged executable file */
#define    MH_FVMLIB    0x3        /* fixed VM shared library file */
#define    MH_CORE        0x4        /* core file */
#define    MH_PRELOAD    0x5        /* preloaded executable file */
#define    MH_DYLIB    0x6        /* dynamically bound shared library */
#define    MH_DYLINKER    0x7        /* dynamic link editor */
#define    MH_BUNDLE    0x8        /* dynamically bound bundle file */
#define    MH_DYLIB_STUB    0x9        /* shared library stub for static */
                    /*  linking only, no section contents */
#define    MH_DSYM        0xa        /* companion file with only debug */
                    /*  sections */
#define    MH_KEXT_BUNDLE    0xb        /* x86_64 kexts */

解释下文件类型:

File Type 用处 示例
MH_OBJECT 编译器对源码编译后得到的中间结果,文件的后缀是 .o,还有静态库文件(.a),.a文件是有 N 个 .o 合并在一起的文件 gcc -c xx.c 生成的 xx.o 文件
MH_EXECUTE 可执行二进制文件 /usr/bin 目录下的那些二进制文件,以及应用程序的二进制文件
MH_FVMLIB VM 共享库文件(还不清楚是什么东西)  
MH_CORE core 文件,一般在 App Crash 产生 崩溃时的 Dump 文件
MH_PRELOAD 预加载可执行文件  
MH_DYLIB 动态库,文件的后缀是 .dylib /usr/lib/ 里面的那些库文件,还有 framework 文件的 xx 也是动态库文件
MH_DYLINKER 动态连接器 /usr/lib/dyld 文件
MH_BUNDLE 插件:非独立的二进制文件,要加载至其他的二进制文件才能发挥作用。和 DYLIB 类型的文件的不同之处在于,这些二进制文件是可执行文件显式地加载的,通常是调用 NSBundle(Objective-C) 或者 CFBundle(C) 往往是通过 gcc-bundle 生成,/System/Library/QuickLokk 目录下的 QuickLook 插件,System/Library/Spotlight 目录下的 Spotlight Importer 插件,System/Library/Automator 目录下的 Automator action 插件
MH_DYLIB_STUB 静态链接文件,只链接,没有 section 内容 不清楚是个什么东西
MH_DSYM 存储着二进制文件符号信息和调试信息的文件 在解析堆栈符号中常用,常用于分享 App 的崩溃信息,位置一般在 xx.dsym/Contents/Resources/DWARF/xx,通过 gcc -g 生成
MH_KEXT_BUNDLE x86_64 内核扩展文件 64位的内核扩展

flags

<mach-o/loader.h> 中包含了重要的 flags

/* Constants for the flags field of the mach_header */
#define    MH_NOUNDEFS    0x1        /* the object file has no undefined
                       references */
#define    MH_INCRLINK    0x2        /* the object file is the output of an
                       incremental link against a base file
                       and can't be link edited again */
#define MH_DYLDLINK    0x4        /* the object file is input for the
                       dynamic linker and can't be staticly
                       link edited again */
#define MH_BINDATLOAD    0x8        /* the object file's undefined
                       references are bound by the dynamic
                       linker when loaded. */
#define MH_PREBOUND    0x10        /* the file has its dynamic undefined
                       references prebound. */
#define MH_SPLIT_SEGS    0x20        /* the file has its read-only and
                       read-write segments split */
#define MH_LAZY_INIT    0x40        /* the shared library init routine is
                       to be run lazily via catching memory
                       faults to its writeable segments
                       (obsolete) */
#define MH_TWOLEVEL    0x80        /* the image is using two-level name
                       space bindings */
#define MH_FORCE_FLAT    0x100        /* the executable is forcing all images
                       to use flat name space bindings */
#define MH_NOMULTIDEFS    0x200        /* this umbrella guarantees no multiple
                       defintions of symbols in its
                       sub-images so the two-level namespace
                       hints can always be used. */
#define MH_NOFIXPREBINDING 0x400    /* do not have dyld notify the
                       prebinding agent about this
                       executable */
#define MH_PREBINDABLE  0x800           /* the binary is not prebound but can
                       have its prebinding redone. only used
                                           when MH_PREBOUND is not set. */
#define MH_ALLMODSBOUND 0x1000        /* indicates that this binary binds to
                                           all two-level namespace modules of
                       its dependent libraries. only used
                       when MH_PREBINDABLE and MH_TWOLEVEL
                       are both set. */ 
#define MH_SUBSECTIONS_VIA_SYMBOLS 0x2000/* safe to divide up the sections into
                        sub-sections via symbols for dead
                        code stripping */
#define MH_CANONICAL    0x4000        /* the binary has been canonicalized
                       via the unprebind operation */
#define MH_WEAK_DEFINES    0x8000        /* the final linked image contains
                       external weak symbols */
#define MH_BINDS_TO_WEAK 0x10000    /* the final linked image uses
                       weak symbols */

#define MH_ALLOW_STACK_EXECUTION 0x20000/* When this bit is set, all stacks 
                       in the task will be given stack
                       execution privilege.  Only used in
                       MH_EXECUTE filetypes. */
#define    MH_DEAD_STRIPPABLE_DYLIB 0x400000 /* Only for use on dylibs.  When
                         linking against a dylib that
                         has this bit set, the static linker
                         will automatically not create a
                         LC_LOAD_DYLIB load command to the
                         dylib if no symbols are being
                         referenced from the dylib. */
#define MH_ROOT_SAFE 0x40000           /* When this bit is set, the binary 
                      declares it is safe for use in
                      processes with uid zero */

#define MH_SETUID_SAFE 0x80000         /* When this bit is set, the binary 
                      declares it is safe for use in
                      processes when issetugid() is true */

#define MH_NO_REEXPORTED_DYLIBS 0x100000 /* When this bit is set on a dylib, 
                      the static linker does not need to
                      examine dependent dylibs to see
                      if any are re-exported */
#define    MH_PIE 0x200000            /* When this bit is set, the OS will
                       load the main executable at a
                       random address.  Only used in
                       MH_EXECUTE filetypes. */

解释如下

标志 用处
MH_NOUNDEFS 表示目标文件没有带有未定义的符号,这些目标文件大部分都是静态二进制文件,没有进一步的链接依赖关系
MH_INCRLINK 目标文件是针对基本文件的增量链接输出,不能再次链接
MH_DYLDLINK 该文件是动态链接器的输入,不能再次静态链接
MH_BINDATLOAD 加载文件时,动态链接器应绑定未定义的引用
MH_PREBOUND 该文件的动态未定义引用已预先绑定
MH_SPLIT_SEGS 目标文件中的只读 segment 和可读写的 segment 是分开的
MH_LAZY_INIT 共享库的初始化例程是通过将内存错误捕捉到其可读写segments(懒散)来延迟运行。–已过时
MH_TWOLEVEL Image 使用两级名称空间绑定
MH_FORCE_FLAT 使用扁平命名空间(不能和 MH_TWOLEVEL 同时出现)
MH_NOMULTIDEFS 这把伞保证在其子图像符号没有多重定义,所以这两个级别的命名空间的提示总是可以使用
MH_NOFIXPREBINDING 没有 dyld 通知预绑定代理有关此可执行文件
MH_PREBINDABLE 二进制文件不是预先绑定的,但可以重新预绑定。 仅在未设置MH_PREBOUND 时使用
MH_ALLMODSBOUND 表示此二进制文件绑定到其依赖库的所有两级命名空间模块。 仅在MH_PREBINDABLEMH_TWOLEVEL都设置时使用
MH_SUBSECTIONS_VIA_SYMBOLS 通过符号进行死代码剥离,可以安全地将这些部分分成子部分
MH_CANONICAL 二进制文件已经通过非预处理操作进行了规范化
MH_WEAK_DEFINES 二进制文件使用了弱符号(weak
MH_BINDS_TO_WEAK 二进制文件链接了弱符号(weak
MH_ALLOW_STACK_EXECUTION 允许栈可执行,只有可执行文件可以用这个标志,但是通常不建议使用。当发生缓冲区溢出时,可执行的栈会给代码注入带来方便,一般是默认关闭的
MH_DEAD_STRIPPABLE_DYLIB 仅适用于 dylib。 当链接到具有此位设置的dylib 时,如果没有从 dylib 引用符号,静态链接器将自动不为 dylib 创建 LC_LOAD_DYLIB 加载命令
MH_ROOT_SAFE 设置此位时,二进制文件声明在 uid 为零的进程中使用它是安全的
MH_SETUID_SAFE 设置此位时,二进制文件声明当 issetugid()true 时,在进程中使用它是安全的
MH_NO_REEXPORTED_DYLIBS dylib 上设置此位时,静态链接器不需要检查依赖的dylib 是否有任何重新导出
MH_PIE 对可执行的文件类型启用地址空间布局随机化

从表中可以看出有个标志和执行有关:MH_ALLOW_STACK_EXECUTION,这个标志用于防止某些数据的执行,通常称为 NX(Non-eXecutable)。通过将数据所在的内存页面标记为不可执行,一般情况下可以防止黑客进行代码注入,因为黑客不能方便地执行数据段中的代码。如果试图执行数据段中的代码,会引发一个硬件异常,进程会被终止,让进程崩溃,从而避免执行注入的代码

Mach-O 头文件的主要功能在于加载命令(load command)。加载命令紧跟在文件头之后,文件头中的两个字段 ncmdssizeofncmds 用于解析加载命令。

随机地址空间

进程每一次启动,地址空间都会简单地随机化。对于大多数应用程序来说,地址空间随机化是一个和他们完全不相关的实现细节,但是对于黑客来说,它具有重大的意义。 如果采用传统的方式,程序的每一次启动的虚拟内存镜像都是一致的,黑客很容易采取重写内存的方式来破解程序。采用 ASLR 可以有效的避免黑客的中间人攻击等

dyld

动态链接库,这是苹果的一个开源的项目,可以在这里下载,当内核执行LC_DYLINK时,链接器会启动,查找进程所依赖的动态库,并加载到内存中。

二级名称空间

这是 dyld 的一个独有特性,说是符号空间中还包括所在库的信息,这样子就可以让两个不同的库导出相同的符号,与其对应的是平坦名称空间

Load Commands

Mach-O 文件头中包含了非常详细的指令,这些指令在被调用时清晰地指导了如何设置并加载二进制数据。这些指令或者成为“加载指令”,紧跟在 mach_header 之后,所有加载命令的总大小由 mach_header 中的 sizeofcmds 字段给出。所有加载命令必须具有 cmdcmdsize 作为前两个字段。cmd 字段用该命令类型的常亮填充。每种命令类型都有专门针对它的结构。cmdsize 字段是特定加载命令结构的大小(以字节为单位)加上它后面的任何加载命令的一部分(即部分结构,字符串等)。要前进到下一个加载命令,可以将 cmdsize 添加到当前加载命令的偏移量或者指针。32位体系结构的 cmdsize 必须是4字节的倍数,64位体系结构,必须是8字节的倍数(这些永远是任何加载命令的最大对齐)。填充字节必须为0。目标文件中的所有表也必须遵循这些规则,以便文件进行内存映射。否则,指向这些表的指针在某些机器上将无法正常工作或者根本无法正常工作,所有填充都归0,对象将逐字节比较。有一些命令是由内核加载器直接使用的,其他命令由动态链接器处理。 load command 的结构如下:

struct load_command {
    uint32_t cmd;        /* type of load command */
    uint32_t cmdsize;    /* total size of command in bytes */
};

内核处理的 Mach-O 命令

#1 命令 内核中处理的函数(定义在 bsd/kern/mach_loader.c 文件中) 用途
0x01 LC_SEGMENT load_segment 将文件中的(32位)的 segment 映射到进程地址空间中
0x19 LC_SEGMENT_64 load_segment 将文件中的(64位)的 segment 映射到进程地址空间中
0x0E LC_LOAD_DYLINKER load_dylinker 调用 dyld(/usr/lib/dyld)
0x1B LC_UUID 内核将 UUID 复制到内部表示 mach 目标的数据中 一个唯一的128位 ID。这个 ID 匹配一个二进制文件及其对应的符号
0x04 LC_THREAD load_thread 开启一个 Mach 线程,但是不分配栈(很少在核心转储文件之外使用)
0x05 LC_UNIXTHREAD load_unixthread 开启一个 UNIX 线程(初始化栈布局和寄存器)。通常情况下,除了指令指针、程序计数器之外,所有的寄存器值都为0。从 Mountain Lion 开始,这条命令被废弃了,被 dyldLC_MAIN 替换
0x1D LC_CODE_SIGNATURE load_code_signature 代码前面(在 OS X 中偶尔使用。在 iOS 中强制使用)
0x21 LC_ENCRYPTION set_code_unprotect() 加密的二进制文件。同样在 OS X 中几乎不用,在 iOS 中普通使用

加载过程在内核的部分负责新进程的基本设置————分配虚拟内存、创建主线程,以及处理任何可能的代码签名、加密的工作。然后对于动态链接的可执行文件(大部分可执行文件都是动态链接的)来说,真正的库加载和符号解析的工作都通过 LC_LOAD_DYLINKER 命令指定的动态链接器在用户态完成。控制权会转交给连接器,连接器进而接着处理文件头的其他加载指令。 下面详细介绍这些加载指令:

LC_SEGMENT 以及进程虚拟内存设置

LC_SEGMENT(LC_SEGMENT_64)命令是主要的加载命令,这条命令指导内核如何设置新运行的进程的内存空间。这些“段” 直接从 Mach-O 二进制文件加载到内存中。 每一条LC_SEGMENT(LC_SEGMENT_64)命令都提供了段布局的所有必要细节信息:

LC_SEGMENT(LC_SEGMENT_64) 提供了段布局所有必要的参数信息:

参数 用途
cmd LC_SEGMENT(LC_SEGMENT_64)
segment load_segment
cmdsize load command 的大小
segname 段的名称 如 _PAGEZERO
vmaddr 所描述的段的虚拟物理地址
vmsize 为这个段分配的虚拟内存大小
fileoff 表示这个段在文件中的偏移量
filesize 表示这个段在文件中占用的字节数
maxprot 段的页面所需要的最高内存保护,用八进制表示(4=r,2=w,1=x)
initprot 段的页面最初始的内存保护
nsects 段中的区(section)数量(如果存在的话)
flags 杂项标志位

有了 LC_SEGMENT 命令,设置进程虚拟内存的过程就变成遵循 LC_SEGMENT 命令的简单操作。对于每一个段,将文件中相应的内容加载到内存中:从偏移量为 fileoff 处加载 filesize 字节到虚拟内存地址 vmadder 处的 vmsize 字节。每一个 segment 的页面都根据 initprot 进行初始化,initprot 指定了如何通过读/写/执行位初始化页面的保护级别。段的保护设置可以动态改变,但是不能超过 maxprot 中指定的值(在 iOS 中,+x+w 是互斥的)。 _PAGEZERO 段(空指针陷阱)、_TEXT 段(程序代码)、_DATA 段(程序数据,可读/科协的数据)和 _LINKEDID (链接器使用的符号和其他表)段提供了 LC_SEGMENT 命令。段有时候也可以进一步分解为区(section)。 下面列出一些常见的区

Mach-O 可执行文件常见的段和区:

_TEXT section 用途
__text 主程序代码
__stubs 用于动态链接的存根
__stub_helper 用于动态链接的存根
__cstring 程序中硬编码的 C 语言字符串
__const const 修饰的常量变量以及硬编码的常量
__objc_methname Objective-C 方法名称
__objc_methtype Objective-C 方法类型
__objc_classname Objective-C 类名称
__objc_classlist Objective-C 类列表
__objc_protolist Objective-C 原型
__objc_imginfo Objective-C 镜像信息
__objc_const Objective-C 常量
__objc_selfrefs Objective-C 自引用(this)
__objc_protorefs Objective-C 原型引用
__ustring unicode 字符串
__gcc_except_tab 异常处理相关
__unwind_info 异常处理
__eh_frame 异常处理

| __DATA section | 用途 | | ——————- | —————————————— | | __nl_symbol_ptr | 动态符号链接相关,指针数组 | | __got | 全局偏移表, Global Offset Table | | __la_symbol_ptr | 动态符号链接相关,也是指针数组,通过 dyld_stub_binder 辅助链接 | | __mod_init_func | 初始化的全局函数地址,会在 main 之前被调用 | | __const | const 修饰的常量 | | __cstring | 程序中硬编码的 ANSI 的字符串 | | __cfstring | CF 用到的字符串 | | __objc_classlist | Objective-C 类列表 | | __objc_nlclslist | Objective-C load 方法列表 | | __objc_catlist | Objective-C category 列表 | | __objc_protolist | Objective-C protocol 列表 | | __objc_imageinfo | Objective-C 镜像洗洗 | | __objc_const | Objective-C 常量 | | __objc_selrefs | Objective-C 引用的 SEL 列表 | | __objc_protorefs | Objective-C 引用的 protocol 列表 | | __objc_classrefs | Objective-C 引用的 class 列表 | | __objc_supperrefs | Objective-C 父类的引用列表 | | __objc_ivar | Objective-C ivar 信息 | | __objc_data | Objective-C class 信息 | | __bss | 未初始化的静态变量区 | | __data` | 初始化的可变变量 |

段也可以设置一些 <mach/loader,h>头文件中定义的标志。苹果使用的一个标志是 SD_PROTECTED_VERSION_1(0X08),表示这个段的页面是“受保护的”,即加密的。苹果通过这种技术加密一些二进制文件。

通过 otool -v -l xxx 或者 otool -lv xxx 可以查看所有的 Load Commands

/System/Library/CoreServices/Finder.app/Contents/MacOS
➜  MacOS otool -lV Finder 
Finder:
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64  X86_64        ALL LIB64     EXECUTE    80       9928   NOUNDEFS DYLDLINK TWOLEVEL BINDS_TO_WEAK PIE MH_HAS_TLV_DESCRIPTORS
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __PAGEZERO
   vmaddr 0x0000000000000000
   vmsize 0x0000000100000000
  fileoff 0
 filesize 0
  maxprot ---
 initprot ---
   nsects 0
    flags (none)
Load command 1
      cmd LC_SEGMENT_64
  cmdsize 1112
  segname __TEXT
   vmaddr 0x0000000100000000
   vmsize 0x0000000000802000
  fileoff 0
 filesize 8396800
  maxprot rwx
 initprot r-x
   nsects 13
    flags PROTECTED_VERSION_1

在创建 Mach-O 对象时,可以通过 -segcreate 开关让 Xcodeld 创建段。Xcode 还包含一个名为 segedit 的特殊工具,可以用于提取或者替换 Mach-O 文件中的段。这个工具用于提取内嵌的文本信息.

2.LC_UNIXTHREAD

当所有的库都完成加载之后,dyld 的工作也完成了,之后由 LC_UNIXTHREAD 命令负责启动二进制程序的主线程(因此主线程总是在可执行文件中,而不会再其他二进制文件中,例如库文件)。根据架构的不同,这条命令会列出所有初始化寄存器的状态,不同架构的寄存器状态不同,这个不同的架构包括 i386_THREAD_STATEx86_THREAD_STATE64 以及 iOS 中的 ARM_THREAD_STATE。在任何一种架构中,大部分的寄存器应该都会初始化为0,其中指令指针(IntelIP)或者程序计数器(ARMr15)是例外,这些寄存器保存了程序入口点的地址。

3.LC_THREAD

LC_UNIXTHREAD 的功能类似,LC_THREAD 用于核心转储文件,Mach-O 核心转储文件实际上市一组 LC_SEGMENT(LC_SEGMENT_64)命令的合集,这些命令负责建立起进程的内存镜像(只不过现在进程已经无效了),然后就是最后一条 LC_THREAD 命令。LC_THREAD 命令也包含几种不同的类型,每一种类型对应不同的机器状态(即线程、浮点和异常)。只要创建了一个核心转储并通过 otool -l 查看这个核心转储文件就可以找到这个命令。

LC_MAIN

LC_MAIN 这条命令的作用是设置程序主线程的入口点地址和栈大小。这条命令比 LC_UNIXTHREAD 命令更实用一些,因为无论如何除了程序计数器之外所有的寄存器都设置为0了,

LC_CODE_SIGNATURE

Mach-O 二进制文件有一个重要特性就是可以进行数字签名。尽管在 OS X 中仍然没怎么使用数字签名,不过由于代码签名和新改进的沙盒机制绑定在一起,所有签名的使用率也越来越高。在 iOS 中,代码签名是强制要求的,这也是苹果尽可能对系统封锁的另一种尝试:在 iOS 中只有苹果的签名才会被认可。在 OS Xcodesign 工具可以用于操纵和显示代码签名。 LC_CODE_SINGATURE 包含了 Mach-O 二进制文件的代码签名,如果这个签名和代码本身不匹配(或者如果在 iOS 上这条命令不存在),那么内核会立即给进程发送一个 SIGKILL 信号将进程杀掉。untethered 越狱(完美越狱)因为利用了一个内核漏洞所以可以修改这些变量。由于这些变量的默认值都是启用签名检查,所以不完美越狱会导致非苹果签名的应用程序崩溃——除非设备以完美越狱的方式引导。 此外通过 Saurikldid 这类工具可以在 Mach-O 中嵌入伪代码签名。这个工具可以代替 OS Xcodesign,允许生成自我签署认证的伪签名。这在 iOS 中尤为重要,因为签名和沙盒模型的应用程序“entitlement”绑定在一起,而后者在 iOS 中是强制要求的。entitlement 是声明式的许可(以 plist 的形式保存),必须内嵌在 Mach-O 中并通过签名,从而允许执行安全敏感的操作时具有运行权限。

动态库

可执行文件很少是独立的,除了少数的一些静态链接的可执行文件。大部分可执行文件都是动态链接的,这些可执行文件依赖一些预先存在的库,这些苦即可能是操作系统提供的,也可能是第三方库

启动时库的加载

iOS 中,少量的进程只需要内核加载器就可以完成加载,在 OS XiOS 上几乎所有的程序都是动态链接的。Mach-O 镜像中有很多的“空洞”———即对外部的库和符号的引用————这些空洞在程序启动时填补。这些工作是由动态链接器来完成,这个过程叫符号绑定。 动态链接库在内核执行 LC_DYLINKER 加载命令时启动。一般来说使用的是 /usr/lib/dyld 作用动态链接器,不过这条加载命令可以指定任何程序作为参数。链接器接管刚创建进程的控制器,因为内核将进程的入口点设置为链接器的入口点。 链接器所要完成的工作,就是查找进程中所有的符号和库的依赖关系,然后解决这些关系。这个过程必须是递归完成,通常情况下库还会依赖于其他的库。

dyld 是一个用户态的进程。dyld 不属于内核的一部分,而是作为一个单独的开源项目由苹果维护的(当然也属于 Darwin 的一部分),地址在 http://www.opensource.apple.com/source/dyld。从内核的角度看,dyld 是一个可插入的组件,可以替换为第三方的链接器。

链接器在 mach_header 中的相关加载命令:

# 加载命令 用途
0x02 LC_SYMTAB 符号表。符号表和字符串表是由这些命令中指定的偏移量分别提供的
0X0B LC_DSYMTAB 符号表。符号表和字符串表是由这些命令中指定的偏移量分别提供的
0x0C LC_LOAD_DYLIB 加载额外的动态库。这条命令取代了 NeXTSTEP 中使用的 LC_LOAD_FVMLIB
0x20 LC_LAZY_LOAD_DYLIB LC_LOAD_DYLIB 功能一样,但是将实际的加载工作延迟到第一次使用这个库中的符号时。
0x0D LC_ID_DYLIB 只在 dylib 中寻找,指定了 dylibID、时间戳、版本和兼容版本
0x1F LC_REEXPORT_DYLIB 只在动态库中寻找,允许一个库将其他库的符号作为自己的符号重新导出。这是 CocoaCarbon 作为很多其他框架的保护伞框架采用的方法
0x240x25 LC_VERSION_MIN_IPHONEOSLC_VERSION_MIN_MACOSX 这个二进制文件要求的最低操作系统的版本。
0x26 LC_FUNCTION_STARTS 压缩的函数起始地址表
0X2A LC_SOURCE_VERSION 构建这个二进制文件使用的源代码的版本。非正式场合使用,不会对实际的链接产生影响
0x2B ?? (常量名称未知) 来自 dylibs 的代码签名区

通过 otool -L 可以显示库的依赖关系。nm 命令可以显示 Mach-O 二进制文件的符号表。OS Xnm 还支持 -m 开关,这个开关不仅能够显示符号,还能跟踪符号的解析过程。此外,Xcodedyldinfo 命令也能完成统一的功能。使用这个命令,可以显示链接器在加载库时使用的操作码。

显示 dyld 绑定操作码

➜  WeXin  xcrun dyldinfo -opcodes /bin/ls | more
rebase opcodes:
0x0000 REBASE_OPCODE_SET_TYPE_IMM(1)
0x0001 REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(2, 0x00000038)
0x0003 REBASE_OPCODE_DO_REBASE_ULEB_TIMES(76)
0x0005 REBASE_OPCODE_ADD_ADDR_IMM_SCALED(0x10)
0x0006 REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB(16, 16)
0x0009 REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB(48)
0x000B REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB(3, 16)
0x000E REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB(64)
0x0010 REBASE_OPCODE_DO_REBASE_IMM_TIMES(1)
0x0011 REBASE_OPCODE_DONE()
binding opcodes:
0x0000 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM(3)
0x0001 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, __DefaultRuneLocale)
0x0016 BIND_OPCODE_SET_TYPE_IMM(1)
0x0017 BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(0x02, 0x00000010)
0x0019 BIND_OPCODE_DO_BIND()
0x001A BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, ___stack_chk_guard)
0x002E BIND_OPCODE_DO_BIND()
0x002F BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, ___stderrp)
0x003B BIND_OPCODE_DO_BIND()
0x003C BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, ___stdoutp)
0x0048 BIND_OPCODE_DO_BIND()
0x0049 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, _optind)
0x0052 BIND_OPCODE_DO_BIND()
0x0053 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, dyld_stub_binder)
0x0065 BIND_OPCODE_ADD_ADDR_ULEB(0xFFFFFFC8)
:0x0009 REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB(48)
0x000B REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB(3, 16)
0x000E REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB(64)
0x0010 REBASE_OPCODE_DO_REBASE_IMM_TIMES(1)
0x0011 REBASE_OPCODE_DONE()
binding opcodes:
0x0000 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM(3)
0x0001 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, __DefaultRuneLocale)
0x0016 BIND_OPCODE_SET_TYPE_IMM(1)
0x0017 BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(0x02, 0x00000010)
0x0019 BIND_OPCODE_DO_BIND()

如果二进制文件中使用了外部定义的函数和符号,那么在他们的文本段中会有一个名为__stubs(桩)的区,在这个区存放的是这些本地未定义符号的占位符。编译器生成代码时会创建对符号桩区的调用,链接器在运行时会解决对桩的这些调用。链接器解决的方式是在被调用的地址处放置一条 JMP 指令。JMP 指令将控制权转交给真实的函数体,但是不会以任何方式修改栈。因此真是函数可以正常返回。 LC_LOAD_DYLIB 命令告诉链接器在哪里可以找到这些符号。链接器要加载每一个指定的库,并且搜索匹配的符号。被链接的库有一个符号表,符号表将符号名称和地址关联起来。符号表在 Mach-O 目标文件的地址可以通过 LC_SYMTAB 加载命令指定的 symoff 找到。对应的符号名称在 stroff,总共有 nsyms 条符号信息。 和其他所有的 UNIX 一样,Mach-O 格式的库可以在 /usr/lib 目录下找到(在 OS XiOS 中没有 /lib 目录)。然后,和其他 UNIX 有两点不同:

  • OS X 的库不是“共享目标文件(.so)”,因为 OS XELF 不兼容,所有文件是“动态库”文件,带有 dylib 后缀
  • 没有 libc。可能有熟悉其他 UNIXC 运行时库。但是在 OS X 上对应的库 /usr/lib/libc.dylib 只不过是指向 libSystem.b.dylib 的符号链接。libSystem 提供了 LibC 的功能,还提供了其他额外的功能,而在其他 UNIX 上这些额外的功能是提供在不同的库中

libSystem 库是系统上所有二进制代码的绝对先决条件,不论是 CC++还是 Objcetive-C 程序。这是因为这个库是对底层系统调用和内核服务的接口,如果没有这些接口就什么事也干不了。这个库在还是 /usr/lib/system 目录下一些库的保护伞库(通过 LC_REEXPORT_LIB 加载命令重新导出了符号)。在 iOS 中,重新导出了多大20多个库

查看符号和观察加载过程

考虑下面这个简单的“hello world”程序,这个程序调用了两次 printf(),然后退出

➜  Desktop touch a.c
➜  Desktop cat a.c
➜  Desktop cat a.c

void main (int argc,char **argv) {
    printf("Saleve ,Munde!\n");
    printf("Value\n");
    exit(0);
}
➜  Desktop

利用 Xcodedyldinfonm,可以解析绑定信息,并且看出二进制文件导出了哪些符号以及链接了哪些库。

➜  Debug xcrun dyldinfo -lazy_bind a
lazy binding information (from lazy_bind part of dyld info):
segment section          address    index  dylib            symbol
__DATA  __la_symbol_ptr  0x100001010 0x0000 libSystem        _exit
__DATA  __la_symbol_ptr  0x100001018 0x000C libobjc          _objc_autoreleasePoolPush
__DATA  __la_symbol_ptr  0x100001020 0x002C libSystem        _printf
➜  Debug

符号表

➜  Desktop nm a.out    
0000000100000000 T __mh_execute_header
                 U _exit
0000000100000f30 T _main
                 U _printf
                 U dyld_stub_binder
➜  Desktop

利用 Xcodeotool 工具,可以进步深入底层,查看汇编层次的代码:

/// otool 对一个简单二进制文件的反汇编
➜  Debug otool -p _main -tV a
a:
(__TEXT,__text) section
_main:
0000000100000f10    pushq    %rbp
0000000100000f11    movq    %rsp, %rbp
0000000100000f14    subq    $0x20, %rsp
0000000100000f18    movl    $0x0, -0x4(%rbp)
0000000100000f1f    movl    %edi, -0x8(%rbp)
0000000100000f22    movq    %rsi, -0x10(%rbp)
0000000100000f26    callq    0x100000f5e ## symbol stub for: _objc_autoreleasePoolPush
0000000100000f2b    leaq    0x68(%rip), %rdi ## literal pool for: "Hellow dsf\n"
0000000100000f32    movq    %rax, -0x18(%rbp)
0000000100000f36    movb    $0x0, %al
0000000100000f38    callq    0x100000f64 ## symbol stub for: _printf
0000000100000f3d    leaq    0x62(%rip), %rdi ## literal pool for: "fdsfsfd\n"
0000000100000f44    movl    %eax, -0x1c(%rbp)
0000000100000f47    movb    $0x0, %al
0000000100000f49    callq    0x100000f64 ## symbol stub for: _printf
0000000100000f4e    xorl    %edi, %edi
0000000100000f50    movl    %eax, -0x20(%rbp)
0000000100000f53    callq    0x100000f58 ## symbol stub for: _exit
➜  Debug

接下来,通过 otool -l 展示加载命令,这一次特别关注 stubs 区:

➜  Debug otool -l -V a
Section
  sectname __stubs
   segname __TEXT
      addr 0x0000000100000f58
      size 0x0000000000000012
    offset 3928
     align 2^1 (2)
    reloff 0
    nreloc 0
      type S_SYMBOL_STUBS
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
 reserved1 0 (index into indirect symbol table)
 reserved2 6 (size of stubs)
Section
  sectname __stub_helper
   segname __TEXT
      addr 0x0000000100000f6c
      size 0x000000000000002e
    offset 3948
     align 2^2 (4)
    reloff 0
    nreloc 0
      type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
 reserved1 0
 reserved2 0
Section
➜  Debug

通过 nm 显示未决的符号:

➜  Debug nm a | grep "U"
                 U _exit
                 U _objc_autoreleasePoolPush
                 U _printf
                 U dyld_stub_binder
➜  Debug nm a | wc -l  # 一共有多少符号
       6     在 arm 上有7个,还有一个__dyld_func_lookup
➜  Debug

共享库缓存

命令总结

  • otool -hv xxx 查看 xxheader
  • otool -v -l xxx 查看 xx 当前所有的 load command
  • otool -vl xx 查看 xx 当前所有的 load command
  • otool -L xxx 查看 xx 当前所依赖的所有动态库
  • otool -p _main -tV xx 利用 otool_main 开始进行反汇编
  • xcrun dyldinfo -bind xx 查看符号的绑定信息
  • xcrun dyldinfo -opcodes /bin/ls | more 查看 dyld 的绑定的操作码
  • xcrun dyldinfo -bind xx | grep UITableView 查看绑定信息中,包含 UITableView 的部分
  • nm 命令可以显示 Mach-O 二进制文件的符号表。
  • nm xx | grep "U" 显示未决的符号
  • nm xx | wc -l xx 中一共有多少符号

参考

PREVIOUSLLDB手动砸壳
NEXTsynchronized相关知识