MachO文件学习笔记

1,028 阅读12分钟

MachO 文件结构

MachO( Mach Object),泛指 Mach 微内核能加载和执行的文件,类似于 Windows 上的 PE 格式(Portable Executable)、Linux 上的 ELF 格式(Executable and Linkable Format)

MachO 是一种文件规范,是一类文件的统称,包括但不限于以下几种常见的文件类型:
① .o(目标文件)
② .a(静态库文件 ) ③ .dylib(动态库文件 )
④ .framework(库文件)
⑤ dSYM(XCode 调试符号文件)

XCode 调试符号文件(.dSYM)
全称 Xcode Debugging Symbols,是一种具有调试信息的目标文件
Project 在 Release 配置下 Build 时,会生成相应的 dSYM 文件
因为 .dSYM 本质上是一个文件夹 directory
所以在通过 file 指令查看 .dSYM 的文件类型时,需要进入到 .dSYM 里面,找到对应的二进制可执行文件进行查看  

⑥ 可执行文件(没有扩展名)
⑦ dyld(动态链接器,一个特殊的可执行文件)
通过 file 指令可以查看 MachO 文件的类型

LoadCommon 各字段含义

LC_SEGMENT_64(__PAGEZERO):空指针陷阱段

这是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常(用于捕捉对空指针的引用) 在 64 位的操作系统上,这个段的虚拟内存大小是 4GB 4GB 并不是指该段物理文件的真实大小,也不是指该段所占物理内存的真实大小 4GB 是规定了进程地址空间的前 4GB 被映射为:不可读、不可写、不可执行的空间 这就是为什么当读写一个 NULL(0x0) 指针时会得到一个 EXC_BAD_ACCESS 错误

因为 LC_SEGMENT_64(__PAGEZERO) 的物理文件大小为 0 所以 Data(数据区域)中没有与 LC_SEGMENT_64(__PAGEZERO) 对应的部分

LC_SEGMENT_64(__TEXT):代码段

代码段有以下常见的节(Section):

  • 01.__text:可执行文件的代码区域,函数、方法、block 等编译成的汇编代码
  • 02.__stubs:用于存储调用到的外部符号,符号的地址在符号表中。分为:懒绑定与非懒绑定 (__la_symbol_ptr、__nl_symbol_ptr)。__stubs 也被称为"桩节区"
  • 03.__stub_helper:借助 dyld_stub_binder 进行懒绑定的指令段(在 MachO 加载时, dyld 会立即为符号 dyld_stub_binder 进行绑定)
  • 04.__cstring:用于存储代码中出现的字符串(去重后的 C 字符串)
  • 05.__ustring:
  • 06.__objc_methname:用于存储 OC 的方法名(C 字符串)
  • 07.__objc_classname:用于存储 OC 的类名(C 字符串)
  • 08.__objc_methtype:用于存储 OC 的方法类型(OC 的方法签名 / OC方法 的 Type Encoding)
  • 09.__unwind_info:用于存储处理异常情况的信息
  • 10.eh_frame:用于存储调试辅助信息

LC_SEGMENT_64(__DATA):数据段

数据段有以下常见的节(Section):

  • 01.__got:即 __nl_symbol_ptr,里面包含的就是 Non-Lazy Symbol Pointers
  • 02.__nl_symbol_ptr:Non-Lazy Symbol Pointers,非懒加载指针表,dyld 加载时会立即绑定表项中的符号
  • 03.__la_symbol_ptr:Lazy Symbol Pointers,懒加载指针表,每个表项中的指针一开始指向 stub_helper
  • 04.__mod_init_func:模块的构造函数(初始化函数,在 main 函数之前调用)
  • 05.__mod_term_func:模块的析构函数(终止函数,在 main 函数返回之后调用)
  • 06.__const:存储 const 关键字修饰的常量。比如使用 extern const 导出的常量
  • 07.__cfstring:Core Foundation 字符串
  • 08.__objc_classlist:OC 类信息列表
  • 09.__objc_nlclslist:OC 的类的 +load 函数列表,比 __mod_init_func 更早执行
  • 10.__objc_catlist:OC 分类信息列表
  • 11.__objc_nlcatlist:OC 的分类的 +load 函数列表
  • 12.__objc_protolist:OC 的协议列表
  • 13.__objc_imageinfo:objc 镜像信息
  • 14.__objc_const:OC 常量。保存 objc_classdata 结构体数据。用于映射类相关数据的地址,比如类名,方法名等
  • 15.__objc_selrefs:指向 __objc_methname 中的方法名称字符串
  • 16.__objc_protorefs:引用到的 OC 协议
  • 17.__objc_classrefs:引用到的 OC 类
  • 18.__objc_superrefs:引用到的 OC 超类
  • 19.__objc_ivar:实例变量对应的 property、变量名、类型数据
  • 20.__objc_data:用于保存 OC 类需要的数据。最主要的内容是映射 __objc_const 地址,用于找到类的相关数据

LC_SEGMENT_64(__LINKEDIT):链接信息段

链接信息段用于存储动态链接器运行时,需要用到的信息包括:符号表、字符串表、重定位项表、签名 等

与代码段和数据段,在 Data(数据区域)规整化一的存储结构不同的是,因为链接信息段存储着诸多类型不同的用于动态链接的加载命令(LoadCommands)所需要的数据,所以链接信息段在 Data(数据区域)的存储结构,会根据具体加载命令(LoadCommands)的不同而不同

以下加载命令需要额外的空间用于存储数据,其数据存储在 Data(数据区域)的链接信息段中:
01.LC_DYLD_INFO_ONLY
02.LC_SYMTAB
03.LC_DYSYMTAB
04.LC_FUNCTION_STARTS
05.LC_DATA_IN_CODE
06.LC_CODE_SIGNATURE

以下加载命令不需要额外的空间用于存储数据,其将所有信息存储在 LoadCommands 区域的加载命令本身:
01.LC_LOAD_DYLINKER
02.LC_UUID
03.LC_VERSION_MIN_IPHONEOS / LC_VERSION_MIN_MACOSX
04.LC_SOURCE_VERSION
05.LC_LOAD_DYLIB
06.LC_RPATH

LC_DYLD_INFO_ONLY 和 LC_DYLD_INFO

该命令包含了动态链接器(dyld)加载目标镜像(MachO / dylib)时所需要的必要信息(重定向的信息、绑定的信息、弱绑定的信息、懒绑定的信息、开放函数的信息)

在 loader.h 中用于描述 LC_DYLD_INFO_ONLY 命令和 LC_DYLD_INFO 命令的数据结构,如下所示

// LC_DYLD_INFO_ONLY 和 LC_DYLD_INFO 使用同一个结构体
struct dyld_info_command {
	// 指令的类型和大小
	uint32_t   cmd;      		/* LC_DYLD_INFO 或者 LC_DYLD_INFO_ONLY */
	uint32_t   cmdsize;      	/* sizeof(struct dyld_info_command) */
	// 重定向:因为使用 ASLR,所以 MachO 会被加载到随机地址,因此需要 rebase 信息
	uint32_t   rebase_off;   	/* file offset to rebase info  */
	uint32_t   rebase_size;  	/* size of rebase info   */
	// 绑定:如果进程依赖其他镜像的符号,则绑定需要 bind 信息
	uint32_t   bind_off; 		/* file offset to binding info   */
	uint32_t   bind_size;    	/* size of binding info  */
	// 弱绑定:对于 C++ 程序而言可能需要通过弱绑定实现代码/数据复用,此时需要 weak bind 信息
	uint32_t   weak_bind_off;    /* file offset to weak binding info   */
	uint32_t   weak_bind_size;   /* size of weak binding info  */
	// 懒绑定:对于一些不需要立即绑定的外部符号可以延时加载,此时需要 lazy bind 信息
	uint32_t   lazy_bind_off;    /* file offset to lazy binding info */
	uint32_t   lazy_bind_size;   /* size of lazy binding infs */
	// 导出符号(开放函数):对于向外部开放的函数,则需要 export 信息
	// 导出符号可以被外部的 MachO 访问,通常动态库会导出一个或多个符号供外部使用,而可执行程序会导出 _main 与 _mh_execute_header 符号供 dyld 使用
	uint32_t   export_off;   	/* file offset to lazy binding info */
	uint32_t   export_size;  	/* size of lazy binding infs */
};

LC_SYMTAB

该命令用于描述符号表的位置和大小(即用于描述符号表的地址信息) 符号表是一个 struct nlist 数组,包含了用于静态链接与动态链接的符号的信息

struct symtab_command {
	uint32_t	cmd;		/* LC_SYMTAB */
	uint32_t	cmdsize;	/* sizeof(struct symtab_command) */
	uint32_t	symoff;		/* 符号表在 MachO 文件中的偏移量 */
	uint32_t	nsyms;		/* 符号表中元素的数量 */
	uint32_t	stroff;		/* 字符串表在 MachO 文件中的偏移量(字符串表记录了所有符号的名字) */
	uint32_t	strsize;	/* 字符串表的总大小(Byte) */
};

LC_DYSYMTAB

该命令用于描述动态符号表,包含了:
① 一组指向符号表中符号的索引
② 一组定义了其他几个表位置的偏移量

在 loader.h 中用于描述 LC_DYSYMTAB 命令的数据结构,如下所示:

struct dysymtab_command {
    uint32_t cmd; 			/* LC_DYSYMTAB */
    uint32_t cmdsize; 		/* sizeof(struct dysymtab_command) */
	// 内部的符号在符号表(Symbol Table)中的索引与数量
    uint32_t ilocalsym; 	/* index to local symbols */
    uint32_t nlocalsym; 	/* number of local symbols */
	// 导出给外部使用的符号在符号表(Symbol Table)中的索引与数量
    uint32_t iextdefsym;	/* index to externally defined symbols */
    uint32_t nextdefsym;	/* number of externally defined symbols */
	// 用于懒绑定的符号在符号表(Symbol Table)中的索引与数量
    uint32_t iundefsym; 	/* index to undefined symbols */
    uint32_t nundefsym; 	/* number of undefined symbols */
	// contents 表在 MachO 文件中的偏移量与元素个数
    uint32_t tocoff; 		/* file offset to table of contents */
    uint32_t ntoc; 			/* number of entries in table of contents */
	// module 表在 MachO 文件中的偏移量与元素个数
    uint32_t modtaboff; 	/* file offset to module table */
    uint32_t nmodtab; 		/* number of module table entries */
	// 引用符号表在 MachO 文件中的偏移量与元素个数
    uint32_t extrefsymoff; 	/* offset to referenced symbol table */
    uint32_t nextrefsyms; 	/* number of referenced symbol table entries */
	// 间接符号表在 MachO 文件中的偏移量与元素个数
    uint32_t indirectsymoff; /* file offset to the indirect symbol table */
    uint32_t nindirectsyms;  /* number of indirect symbol table entries */
	// 外部重定位元素在 MachO 文件中的偏移量与元素个数
    uint32_t extreloff; 	/* offset to external relocation entries */
    uint32_t nextrel; 		/* number of external relocation entries */
	// 内部重定位元素在 MachO 文件中的偏移量与元素个数
    uint32_t locreloff; 	/* offset to local relocation entries */
    uint32_t nlocrel; 		/* number of local relocation entries */
}; 

LC_LOAD_DYLINKER

该命令用于描述 MachO 文件所使用的动态链接器一个 MachO 文件最多只能有一个动态链接器

在 loader.h 中用于描述 LC_LOAD_DYLINKER 命令的数据结构,如下所示:

struct dylinker_command {
	uint32_t	 cmd;		/* LC_ID_DYLINKER or LC_LOAD_DYLINKER */
	uint32_t	 cmdsize;	/* 加载命令的大小(包含了 name 字符串) */
	union lc_str name;		/* 动态链接器的路径 */
};

 // LoadCommands 中用于表示可变字符串的联合体
 // 字符串数据直接存储在 LoadCommands 之后,并且字符串的偏移量是相对于 LoadCommands  计算的
 // LoadCommands 的 cmdsize 字段包含了字符串的长度
union lc_str {
	uint32_t	offset;	/* offset to the string */
#ifndef __LP64__
	char		*ptr;	/* pointer to the string */
#endif 
};

LC_UUID

该命令包含了一个 128 位的唯一随机数,用于标识由静态链接器生成的 MachO 文件

在 loader.h 中用于描述 LC_UUID 命令的数据结构,如下所示

struct uuid_command {
    uint32_t	cmd;		/* LC_UUID */
    uint32_t	cmdsize;	/* sizeof(struct uuid_command) */
    uint8_t	uuid[16];		/* the 128-bit uuid */
};

LC_VERSION_MIN_IPHONEOS 和 LC_VERSION_MIN_MACOSX

该命令用于描述 MachO 文件要求的最低操作系统版本 MachO 文件构建时,可以在 Target - General - Deployment Info 中指定程序所支持的最低的操作系统版本

在 loader.h 中用于描述 LC_VERSION_MIN_IPHONEOS 命令和LC_VERSION_MIN_MACOSX 命令的数据结构,如下所示:

struct version_min_command {
    uint32_t	cmd;		/* LC_VERSION_MIN_MACOSX 或 LC_VERSION_MIN_IPHONEOS 或 LC_VERSION_MIN_WATCHOS 或 LC_VERSION_MIN_TVOS */
    uint32_t	cmdsize;	/* sizeof(struct min_version_command) */
    uint32_t	version;	/* X.Y.Z is encoded in nibbles xxxx.yy.zz */
    uint32_t	sdk;		/* X.Y.Z is encoded in nibbles xxxx.yy.zz */
};

LC_SOURCE_VERSION

该命令用于描述 MachO 文件在构建时,所使用的源代码版本 MachO 文件构建时,可以在 Target - General - Identity - Version 中指定程序的外部版本号

在 loader.h 中用于描述 LC_SOURCE_VERSION 命令的数据结构,如下所示:

struct source_version_command {
    uint32_t  cmd;		/* LC_SOURCE_VERSION */
    uint32_t  cmdsize;	/* 16 */
    uint64_t  version;	/* A.B.C.D.E packed as a24.b10.c10.d10.e10 */
};

LC_MAIN

该命令用于设置程序主线程的入口地址和栈大小。即用于指定:主可执行文件中 main() 函数的入口地址(通过文件偏移量的形式)

在 loader.h 中用于描述 LC_MAIN 命令的数据结构,如下所示:

 // entry_point_command 用于替代 thread_command
 // 如果在构建主可执行文件时,用到了 -stack_size 选项
 // 那么 stacksize 字段将会包含主线程所需的栈大小
struct entry_point_command {
    uint32_t  cmd;	/* LC_MAIN only used in MH_EXECUTE filetypes */
    uint32_t  cmdsize;		/* 24 */
    uint64_t  entryoff;		/* file (__TEXT) offset of main() */
    uint64_t  stacksize;	/* if not zero, initial stack size */
};

LC_ENCRYPTION_INFO_64

该指令用于描述加密信息段(encrypted segment)的位置和大小,以及使用了哪种加密体系

在 loader.h 中用于描述 LC_ENCRYPTION_INFO_64 命令的数据结构,如下所示:

struct encryption_info_command_64 {
   uint32_t	cmd;		/* LC_ENCRYPTION_INFO_64 */
   uint32_t	cmdsize;	/* sizeof(struct encryption_info_command_64) */
   uint32_t	cryptoff;	/* file offset of encrypted range */
   uint32_t	cryptsize;	/* file size of encrypted range */
   uint32_t	cryptid;	/* which enryption system, 0 means not-encrypted yet */
   uint32_t	pad;		/* padding to make this struct's size a multiple of 8 bytes */
};

LC_LOAD_DYLIB

该命令用于描述 MachO 文件依赖的动态库的信息,每条命令对应一个动态库 动态链接器根据该命令提供的信息去加载和链接 MachO 文件依赖的动态库 (动态库包含:系统的 + 第三方的)

在 loader.h 中用于描述 LC_LOAD_DYLIB 命令的数据结构,如下所示:

struct dylib_command {
	uint32_t	 cmd;		/* LC_ID_DYLIB 或 LC_LOAD_{,WEAK_}DYLIB 或 LC_REEXPORT_DYLIB */
	uint32_t	 cmdsize;	/* includes pathname string */
	struct dylib dylib;		/* the library identification */
};

 // 一个动态库由两部分标识:
 // 1.动态库的路径(更具体地,动态库可执行文件的路径)
 // 2.兼容的版本号
 // 要使用一个动态库,必须保证:
 // 1.外部动态库的存储路径必须和 dylib.name 中声明的路径相匹配
 // 2.外部动态库的版本必须大于等于 dylib.compatibility_version 中声明的版本
struct dylib {
    union lc_str  name;				/* library's path name */
    uint32_t timestamp;				/* library's build time stamp */
    uint32_t current_version;		/* library's current version number */
    uint32_t compatibility_version;	/* library's compatibility vers number*/
};

 // LoadCommands 中用于表示可变字符串的联合体
 // 字符串数据直接存储在 LoadCommands 之后,并且字符串的偏移量是相对于 LoadCommands 计算的
 // LoadCommands 的 cmdsize 字段包含了字符串的长度
union lc_str {
	uint32_t	offset;	/* offset to the string */
#ifndef __LP64__
	char		*ptr;	/* pointer to the string */
#endif 
};

LC_RPATH

该命令用于描述动态链接器在搜索 MachO 文件依赖的动态库时,所用到的搜索路径 ,每条命令对应一个 @rpath 可以在 Target - Build Settings - Linking - Runpath Search paths 中指定程序用到的 @rpath

在 loader.h 中用于描述 LC_RPATH 命令的数据结构,如下所示:

struct rpath_command {
    uint32_t	 cmd;		/* LC_RPATH */
    uint32_t	 cmdsize;	/* includes string */
    union lc_str path;		/* path to add to run path */
};

 // LoadCommands 中用于表示可变字符串的联合体
 // 字符串数据直接存储在 LoadCommands 之后,并且字符串的偏移量是相对于 LoadCommands 计算的
 // LoadCommands 的 cmdsize 字段包含了字符串的长度
union lc_str {
	uint32_t	offset;	/* offset to the string */
#ifndef __LP64__
	char		*ptr;	/* pointer to the string */
#endif 
};

LC_FUNCTION_STARTS

该命令用于描述函数的起始地址信息,指向了链接信息段中 Function Starts 的首地址 Function Starts 定义了一个函数起始地址表,调试器和其他程序通过该表可以很容易地判断出一个地址是否在函数内

在 loader.h 中用于描述 LC_FUNCTION_STARTS 命令的数据结构,如下所示:

struct linkedit_data_command {
    uint32_t	cmd;		/* LC_CODE_SIGNATURE,   
                                   LC_SEGMENT_SPLIT_INFO,
                                   LC_FUNCTION_STARTS, 
                                   LC_DATA_IN_CODE,
                                   LC_DYLIB_CODE_SIGN_DRS, 
                                   LC_LINKER_OPTIMIZATION_HINT,
                                   LC_DYLD_EXPORTS_TRIE,
                                   LC_DYLD_CHAINED_FIXUPS. */
    uint32_t	cmdsize;	/* sizeof(struct linkedit_data_command) */
    uint32_t	dataoff;	/* file offset of data in __LINKEDIT segment */
    uint32_t	datasize;	/* file size of data in __LINKEDIT segment  */
};

LC_DATA_IN_CODE

该命令使用一个 struct linkedit_data_command 指向一个 data_in_code_entry 数组 data_in_code_entry 数组中的每一个元素,用于描述代码段中一个存储数据的区域

在 loader.h 中用于描述 LC_DATA_IN_CODE 命令的数据结构,如下所示:

struct linkedit_data_command {
    uint32_t	cmd;		/* LC_CODE_SIGNATURE,      
                                   LC_SEGMENT_SPLIT_INFO,
				   LC_FUNCTION_STARTS, 
                                   LC_DATA_IN_CODE,
				   LC_DYLIB_CODE_SIGN_DRS,
                                   LC_LINKER_OPTIMIZATION_HINT,
				   LC_DYLD_EXPORTS_TRIE,
                                   LC_DYLD_CHAINED_FIXUPS. */
    uint32_t	cmdsize;	/* sizeof(struct linkedit_data_command) */
    uint32_t	dataoff;	/* file offset of data in __LINKEDIT segment */
    uint32_t	datasize;	/* file size of data in __LINKEDIT segment  */
};

struct data_in_code_entry {
    uint32_t	offset;  	/* from mach_header to start of data range*/
    uint16_t	length;  	/* number of bytes in data range */
    uint16_t	kind;    	/* a DICE_KIND_* value  */
};
#define DICE_KIND_DATA              0x0001
#define DICE_KIND_JUMP_TABLE8       0x0002
#define DICE_KIND_JUMP_TABLE16      0x0003
#define DICE_KIND_JUMP_TABLE32      0x0004
#define DICE_KIND_ABS_JUMP_TABLE32  0x0005

LC_CODE_SIGNATURE 命令

该命令用于描述 MachO 代码签名信息的位置和大小

在 loader.h 中用于描述 LC_CODE_SIGNATURE 命令的数据结构,如下所示

struct linkedit_data_command {
    uint32_t	cmd;		/* LC_CODE_SIGNATURE, 
                                   LC_SEGMENT_SPLIT_INFO,
                                   LC_FUNCTION_STARTS,
                                   LC_DATA_IN_CODE,
                                   LC_DYLIB_CODE_SIGN_DRS,
                                   LC_LINKER_OPTIMIZATION_HINT,
                                   LC_DYLD_EXPORTS_TRIE,
                                   LC_DYLD_CHAINED_FIXUPS. */
    uint32_t	cmdsize;	/* sizeof(struct linkedit_data_command) */
    uint32_t	dataoff;	/* file offset of data in __LINKEDIT segment */
    uint32_t	datasize;	/* file size of data in __LINKEDIT segment  */
};