Mach-O解析

1,809 阅读8分钟

本文仅是记录知识点

Mach-O是Mach Object文件格式的缩写,是一种记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。

Mach-O文件主要由3部分组成:

Mach Header: 描述 Mach-O 的CPU架构、文件类型、加载命令等信息
Load Command: 描述文件中数据等具体组织结构,不同数据类型使用不同等加载命令表示
Data: Data中每一个段(Segment)的数据保存在此,段用来存放数据和代码

在MacOS或者iOS上可执行的程序格式叫做Macho-O,它的主要成分如下图所示:

1421892661838860.gif

利用machOView解析,详细内容如下:

企业微信截图_4e3097c4-3f35-4bd5-8b25-7f27e8b748cf.png

文件头 mach64 Header
加载命令 Load Commands
数据段 __TEXT、__DATA
动态库加载信息 Dynamic Loader Info
入口函数 Function Starts
符号表 Symbol Table
动态库符号表 Dynamic Symbol Table
字符串表 String Table

Mach-O 头文件:

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 位的保留字段 */
};

文件类型filetype有11种:

define     MH_OBJECT   0x1     /* .o文件:编译器对源码编译后得到的中间结果 */
#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 内核扩展 */

Segment 的组成:

LC_SEGMENT_64: 将该段(64位)映射到进程地址空间中
SEG_PAGEZERO    "__PAGEZERO" /* 大小为 4GB,规定进程地址空间的前 4GB 被映射为不可读不可写不可执行。 */
SEG_TEXT        "__TEXT" /* 代码/只读数据段 */
SEG_DATA        "__DATA" /* 数据段 */
SEG_OBJC        "__OBJC" /* Objective-C runtime 段 */
SEG_LINKEDIT    "__LINKEDIT" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */
LC_DYLD_INFO_ONLY:加载动态链接库信息(重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值等信息)
LC_SYMTAB:载入符号表地址
LC_DYSYMTAB:载入动态符号表地址
LC_LOAD_DYLINKER:加载动态加载库,可以看出示例使用的是/usr/lib/dyld 
LC_UUID:确定文件的唯一标识,crash解析中也会有这个,去检测dysm文件和crash文件是否匹配
LC_VERSION_MIN_MACOSX/LC_VERSION_MIN_IPHONEOS:确定二进制文件要求的最低操作系统版本
LC_SOURCE_VERSION:构建该二进制文件使用的源代码版本
LC_MAIN:设置程序主线程的入口地址和栈大小
LC_ENCRYPTION_INFO_64:获取加密信息
LC_LOAD_DYLIB:加载额外的动态库
LC_FUNCTION_STARTS:定义一个函数起始地址表,使调试器和其他程序易于看到一个地址是否在函数内
LC_DATA_IN_CODE:定义在代码段内的非指令的表
LC_CODE_SIGNATURE:获取应用签名信息

Segment 的数据结构:

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

部分的 Segment (主要指的__TEXT 和__DATA)可以进一步分解为 Section。之所以按照 Segment -> Section 的结构组织方式,是因为在同一个 Segment 下的 Section,可以控制相同的权限,也可以不完全按照 Page 的大小进行内存对其,节省内存的空间。而 Segment 对外整体暴露,在程序载入阶段映射成一个完整的虚拟内存,更好的做到内存对齐

Section 的数据结构:

struct section_64 { 
    char        sectname[16];   /* Section 名字 */
    char        segname[16];    /* Section 所在的 Segment 名称 */
    uint64_t    addr;       /* Section 所在的内存地址 */
    uint64_t    size;       /* Section 的大小 */
    uint32_t    offset;     /* Section 所在的文件偏移 */
    uint32_t    align;      /* Section 的内存对齐边界 (2 的次幂) */
    uint32_t    reloff;     /* 重定位信息的文件偏移 */
    uint32_t    nreloc;     /* 重定位条目的数目 */
    uint32_t    flags;      /* 标志属性 */
    uint32_t    reserved1;  /* 保留字段1 (for offset or index) */
    uint32_t    reserved2;  /* 保留字段2 (for count or sizeof) */
    uint32_t    reserved3;  /* 保留字段3 */
};

常见的 Section:

__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 超类引用

Dynamic Loader Info (动态库加载信息或者说链接信息) 一个完整的用户级MachO文件的末端是一系列链接信息。其中包含了动态加载器用来链接可执行文件或者依赖所需使用的符号表、字符串表等,详细内容请查看参考2。

__TEXT.__stubs 是什么:

wikipedia定义:Stub 是指用来替换一部分功能的程序段。桩程序可以用来模拟已有程序的行为(比如一个远端机器的过程)或是对将要开发的代码的一种临时替代。

__la_symbol_ptr 里面的所有表项的数据在开始时都会被 binding 成 __stub_helper。而在之后的调用中,虽然依旧会跳到 __stub 区域,但是 __la_symbol_ptr 中由于在之前的调用中获取到了对应方法的真实地址,所以无需在进入 dyld_stub_binder 阶段,并直接调用函数。这样就完成了一次近似于 lazy 思想的延时 binding 过程。

总结一下 Stub 机制。其实和 wikipedia 上的说法一致,设置函数占位符并采用 lazy 思想做成延迟 binding 的流程。在 macOS 中也是如此,外部函数引用在 __DATA 段的 __la_symbol_ptr 区域先生产一个占位符,当第一个调用启动时,就会进入符号的动态链接过程,一旦找到地址后,就将 __DATA Segment 中的 __la_symbol_ptr Section 中的占位符修改为方法的真实地址,这样就完成了只需要一个符号绑定的执行过程。

如果说前面两部分header和Segment的主要作用,是让kern内核知道如何读取MachO文件,并指定MachO文件的动态链接器(dyly)用来完成后续的动态库加载,然后设置好程序入口等一些列程序启动前的信息,那么Data和链接信息部分,就相当于当程序运行起来后,为每一个映射到虚拟内存中的指令操作提供真实的物理地址支持。

可执行文件运行过程

解析mach-o文件
设置运行环境参数 
文本段VM映射参数
加载命令
动态库信息
符号表地址信息
动态符号表地址信息
常亮字符串表地址信息
动态库加载信息
符号函数地址
依赖动态库信息
动态链接器地址信息
根据动态库加载信息,把桩占位符,填写为指定调用_nl_symbol_ptr的汇编指令
根据LC_MAIN的entry point调用指定entry offset偏移地址
执行entry offset相关二进制(逻辑是按照汇编指令,进行运行)
第一次运行到动态库函数时,进行一次懒加载动态绑定,并且动态链接器自动修改_la_symbol_ptr区的地址,指向动态库对应符号的地址
第二次运行到动态库函数时,直接jmp到指定的符号地址

注意:系统很多动态库都是共有的,所以XOS做了共享库缓存优化,只要有相关进程使用过相关动态库,在另一进程,动态链接器在填桩时,直接会把桩_la_symbol_ptr区的地址,指向动态库对应符号的地址。

mach-O的用法实战:分析多余的类和方法

参考1:xiaozhuanlan.com/topic/67503…

参考2:blog.csdn.net/bjtufang/ar…