较为详细的解析了Mach-O文件格式,并着重阐述了动态链接相关的知识点,开始吧~saonian~
简述
进程是可执行文件在内存中加载得到的结果,这种文件必须是操作系统理解的格式,这样操作系统才能解析文件,简历所需要的依赖(如库),初始化运行环境并执行。
Mach-O(Mach Object File Format)是macOS上的可执行文件,Linux和大部分Unix系统采用的是原生格式 ELF(Extensible Firmware Interface)
,windows支持的格式为PE32/PE32+
,macOS
支持三种可执行文件格式:解释器脚本文件、通用二进制格式和Mach-O
格式,如下图所示:
可执行格式 | magic | 用途 |
---|---|---|
脚本 | \x7FELF |
主要用于 shell 脚本,但是也常用语其他解释器,如 Perl, AWK 等。也就是我们常见的脚本文件中在 #! 标记后的字符串,即为执行命令的指令方式,以文件的 stdin 来传递命令 |
通用二进制格式 | 0xcafebabe 0xbebafeca |
包含多种架构支持的二进制格式,只在 macOS 上支持 |
Mach-O |
0xfeedface (32 位) 0xfeedfacf (64 位) |
macOS 的原生二进制格式 |
通用二进制格式(
Universal Binary
)也称为“胖二进制格式(Fat Binary)”,主要是解决历史问题,以支持Power PC(PPC)
架构以及Inter
架构,是一种对多架构的二进制文件的打包集合。
其中常见的包括:可执行文件、动态库文件、动态链接器等都是Mach-O
格式,具体可通过file
命令查看具体的可执行文件格式,如下图:

Mach-O文件格式
其结构如下图所示,主要包括四部分组成:

-
Header
头部描述了该文件的
CPU
类型、文件类型、加载命令等信息; -
Load commands
加载命令描述了文件中数据的具体组织结构,不同数据类型如何使用不同的加载命令表示;
-
Data
数据段存放了包括代码、字符常量、类、方法等代码和数据,并且拥有多个
Segment
段,每个Segment
段都包含零到多个Section
节; -
Loader info
链接信息及其他文件末端包含了一系列链接信息,如动态链接器用来链接可执行文件或者依赖所需使用的符号表、字符串表等,以及签名信息等;

为何
Segment
段中存在Section
节?分段的目的主要:不同段可被映射到不同虚拟存储区域,便于读写权限管理;利用现代CPU缓存体系及程序的局部性原理,将指令和数据缓存分离有利用提升缓存命中率;指令或数据共享,有利于提升内存空间利用率。而分节主要是可以不完全按照
page
的大小进行内存对齐,提升内存空间利用率。
Header
Mach-O
文件头部具体的数据结构如下(区分32位和64位架构):
//32bit
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 */
};
//64bit
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 */
};
32位和64位架构头部结构没有大的区别,只是64位多了一个保留字段,具体的字段名称如下:
-
magic
:魔数,用于确认该文件是32位还是64位 -
cputype
,CPU类型,如arm
、x86_64
-
cpusubtype
,CPU具体类型,如arm64
、armv7
-
filetype
,文件类型,如可执行文件、库文件、动态链接器、符号文件和调试信息等,其中MH_EXECUTE
代表可执行文件,具体的文件类型定义如下:/* 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 */ #define MH_DSYM 0xa /* companion file with only debug */ #define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
-
ncmd
,加载命令条数 -
sizeofcmds
,所有加载命令在文件中占用地址空间大小 -
reserved
,保留字段 -
flags
,标志位,具体的定义如下:#define MH_NOUNDEFS 0x1 // 目前没有未定义的符号,不存在链接依赖 #define MH_DYLDLINK 0x4 // 该文件是dyld的输入文件,无法被再次静态链接 #define MH_PIE 0x200000 // 加载程序在随机的地址空间,只在 MH_EXECUTE中使用 #define MH_TWOLEVEL 0x80 // 两级名称空间
除了用MachOView能查看MachO文件信息,还可以通过otool命令查看,我们先来分析Header中的内容:otool -h xxx
来查看。


Load commands
Load commands
紧跟在头部之后(如下图),这些加载指令清晰地告诉加载器如何处理二进制数据,有些命令是由内核处理的,有些是由动态链接器处理的,常见的加载命令如下:

-
LC_SEGMENT/LC_SEGMENT_64
: 将该段(32/64位)映射到进程地址空间中,包含了Segment
中所有Section
加载信息;其中
_PAGEZERO
段不具有访问权限,用来处理空指针,其值为0;TEXT
为代码段,_DATA/_DATA_CONST
为可读写的数据段;_LINKEDIT
链接段包含了一些符号表、间接符号表、rebase
操作码、绑定操作码、导出符号、函数启动信息、数据表、代码签名、字符串表等数据,该加载命令下没有Section
,需要配合LC_SYMTAB
来解析symbol table
和string table
。_LINKEDIT
加载命令信息中的文件偏移为0x4000(十进制16384
)正好对应Dynamic Loader Info
起始地址,文件大小为0x5840(十进制22592)=0x9840(0x9830+10)-0x4000
,正好对应从Dynamic Loader Info
到文件末尾的数据部分; -
LC_DYLD_INFO_ONLY
:加载动态链接库信息(重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值等信息) -
LC_SYMTAB
:载入符号表地址 -
LC_DYSYMTAB
:载入动态符号表地址 -
LC_LOAD_DYLINKER
:加载动态加载库 -
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
:获取应用签名信息
具体的加载命令的数据结构如下(64位格式,与32位格式差别不大):
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
cmd:
就是Load commands
的类型,这里LC_SEGMENT_64
代表将文件中64位的段映射到进程的地址空间;cmdsize:
代表load command
的大小segment name:
段的名称VM Address :
段的虚拟内存地址VM Size :
段的虚拟内存大小file offset:
段在文件中偏移量file size:
段在文件中的大小nsects:
标示了Segment
中有多少secetion
除了使用MachOView
查看,还可以通过otool -l xxx
查看,如下图所示:


对于段的地址大小可通过size -l -m xxx
查看,如下图:

下面重点阐述几个重要的加载命令,便于后续理解整个程序启动、动态加载、逆向等知识点。
LC_SEGMENT_64(__PAGEZERO)
该加载命令的内容如下图所示:


其中虚拟地址范围为0x0~0x100000000
正好对应4GB
空间,该文件的起始虚拟地址空间也是从0x100000000
开始,即所有代码和数据都是被加载到4GB
之后的地址。对应的文件内容大小为0,即在该文件中不占用实际空间,且具有不可读写不可执行权限,这样内核就可以识别到空指针或指针截断的错误的范围该地址空间的调用而抛出段异常,如EXC_BAD_ACCESS
异常。
LC_SEGMENT_64(__LINKEDIT) & LC_DYLD_INFO_ONLY
__LINKEDIT
包含了动态链接相关的信息,如虚拟地址空间地址及文件偏移、文件权限等,而LC_DYLD_INFO_ONLY
加载命令,包含了重定位、绑定及导出等偏移/大小信息。


LC_SYMTAB
对于LC_SYMTAB
加载命令,其数据结构定义如下:
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};

这个命令告诉了链接器(包括静态链接器或动态链接器)Symbol Table
和String Table
的位置及大小信息。
其中符号的结构由内核定义,如下:
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
n_un
,符号的名字在字符串表中的序号(在一个 Mach-O 文件里,具有唯一性)n_sect
,符号所在的 section index(内部符号有效值从 1 开始,最大为 255;外部符号为0)n_value
,符号的地址值(在链接过程中,会随着其 section 发生变化)n_type
是一个 8 bit 的复合字段:bit[5:8]
: 如果不为 0,表示这是一个与调试有关的符号,值意义类型详见mach-o/stab.hbit[4:5]
: 若为 1,则表示该符号是私有的(外部符号)bit[1:4]
: 符号类型N_UNDF
(0x0): 未定义N_ABS
(0x2): 符号地址指向到绝对地址,链接器后期不会再修改N_SECT
(0xe): 本地符号,即符号定义于当前 Mach-ON_PBUD
(0xc): 预绑定符号N_INDR
(0xa): 表示该符号和另一个符号是同一个,n_value
指向到 string table,即该同名符号的名字
bit[0:1]
: 表示这是外部符号,即该符号要么定义在外部,要么定义在本地但是可以被外部使用;

LC_DYSYMTAB
对于LC_DYSYMTAB
加载命令,其数据结构如下:
struct dysymtab_command {
uint32_t cmd; /* LC_DYSYMTAB */
uint32_t cmdsize; /* sizeof(struct dysymtab_command) */
uint32_t ilocalsym; /* index to local symbols */
uint32_t nlocalsym; /* number of local symbols */
uint32_t iextdefsym;/* index to externally defined symbols */
uint32_t nextdefsym;/* number of externally defined symbols */
uint32_t iundefsym; /* index to undefined symbols */
uint32_t nundefsym; /* number of undefined symbols */
uint32_t tocoff; /* file offset to table of contents */
uint32_t ntoc; /* number of entries in table of contents */
uint32_t modtaboff; /* file offset to module table */
uint32_t nmodtab; /* number of module table entries */
uint32_t extrefsymoff; /* offset to referenced symbol table */
uint32_t nextrefsyms; /* number of referenced symbol table entries */
uint32_t indirectsymoff; /* file offset to the indirect symbol table */
uint32_t nindirectsyms; /* number of indirect symbol table entries */
uint32_t extreloff; /* offset to external relocation entries */
uint32_t nextrel; /* number of external relocation entries */
uint32_t locreloff; /* offset to local relocation entries */
uint32_t nlocrel; /* number of local relocation entries */
};
主要包含了本地、外部符号、未定义外部符号、间接符号表的位置及数目,其中indriectsymoff
指定了Dynamic Symbol Table
的文件偏移位置及数目;


可使用
otool -I xxx
来获取间接符号表内容;
其中间接符号包含了符号名、符号所处的节及符号间接地址,其所处的Section
处在__stubs
、__got
、及__la_symbol_ptr
等节;
对于后续需要动态链接定位的符号头部,如LC_SEGMENT_64
中的_TEXT.__stubs
、_DATA_CONST.__got
、_DATA.__la_symbol_ptr
,其头部字段中包含了Indirect Sym Index(Reserverd1)
字段,该字段指明在Indirect Symbol Table
间接符号表中的条目序号,如下图:

_la_symbol_ptr
中的符号在间接符号表中的起始条目序号为26。
LC_LOAD_DYLINKER
该加载命令包含了重要的程序启动动态链接器的路径,如下图x86_64
的为/usr/lib/dyld
。

Segment & Section

Section
的数据结构
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};
sectname:
比如_text
、stubs
segname :
该section
所属的segment
,比如_TEXT
addr :
该section
在内存的起始位置size:
该section
的大小offset:
该section
的文件偏移align :
字节大小对齐reloff :
重定位入口的文件偏移nreloc:
需要重定位的入口数量flags:
包含section
的type
和attributes
常见的 Section
如下表所示:
Section | 用途 |
---|---|
_TEXT.__text |
主程序代码 |
_TEXT.__cstring |
C 语言字符串 |
_TEXT.__const |
const 关键字修饰的常量 |
_TEXT.__stubs |
用于 Stub 的占位代码,很多地方称之为桩代码,用于重定向到 lazy 和 non-lazy 符号的 section ,被标记为 S_SYMBOL_STUBS 。TEXT Segment 里代码和 dylib 外部符号的引用地址对函数符号的引用都指向了 stubs。其中每项都是 jmp 代码间接寻址,可跳到la_symbol_ptr Section 中。 |
_TEXT.__stubs_helper |
当 Stub 无法找到真正的符号地址后的最终指向 |
_TEXT.__objc_methname |
Objective-C 方法名称 |
_TEXT.__objc_methtype |
Objective-C 方法类型 |
_TEXT.__objc_classname |
Objective-C 类名称 |
_TEXT.__eh_frame |
调试辅助信息 |
_TEXT.__unwind_info |
用于存储处理异常情况信息 |
_DATA.__data |
初始化过的可变数据 |
_DATA.__la_symbol_ptr |
lazy binding 的指针表,表中的指针一开始都指向 __stub_helper |
_DATA.nl_symbol_ptr |
非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号 |
_DATA.__got |
全局偏移表 |
_DATA.__const |
没有初始化过的常量 |
_DATA.__cfstring |
程序中使用的 Core Foundation 字符串(CFStringRefs ) |
_DATA.__bss |
BSS ,存放为初始化的全局变量,即常说的静态内存分配 |
_DATA.__common |
没有初始化过的符号声明 |
_DATA.__mod_init_func |
初始化函数,在main 之前调用 |
_DATA.__mod_term_func |
终止函数,在main 返回之后调用 |
_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 超类引用 |
_DATA.__got
对于_DATA.__got
节,其内容如下图所示:

其类似一个表,每个条目是一个地址值,定义的是Non-Lazy Symbol Pointers
即非懒加载符号地址,所有条目的内容都是0。其引入的目的是解决程序在链接阶段存放不能确定目的地址的符号,当镜像被加载时,动态链接器dyld
会对每个条目对应的符号进行重定位,将其真正的地址写入,作为条目的内容。对于dyld
如何确定符号信息的,可以通过上面的Indirect Symbol Table
中的符号看出,包含了符号名称、间接符号地址。
_DATA.__la_symbol_ptr
与之对应的是_DATA.__la_symbol_ptr
节,其内容如下图所示:

其实际内容都指向了_TEXT.__stub_helper
节,最终通过jumpq
指令跳转到了dyld_stub_binder
符号,即__got
节中的Non_Lazy Symbol Pointer
中的条目,该符号为一个函数,定义于dyld_stub_binder.S,由 dyld
提供。
dyld_stub_binder
函数其大致逻辑是:内部会寻找锁调用符号的真实地址,并写入_la_symbol_ptr
条目中,然后跳转到真实地址执行;

_TEXT.__stubs
对于_TEXT.__stubs
节,其内容如下:

该内容也是一个表,每个条目都是一段数据,称为“符号桩”。通过otool -v xx -s _TEXT __stubs
命令查看内容如下:

其内容都是jmpq
跳转指令,跳转的地址以第一条地址为例计算:
0x100003000 = 0x100001dbc(rip) + 0x1244
该地址指向的是__la_symbol_ptr
节,而该节最终都指向了dyld_stub_binder
。

Loader info
链接加载信息包含了动态加载信息Dynamic Loader Info
(包含了重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值等信息,其加载命令为LC_DYLD_INFO_ONLY
),函数起始地址表Function Starts
(其加载命令为LC_FUNCTION_STARTS
),符号表Symbol Table
,动态符号表Dynamic Symbol Table
,代码段非指令表Data in Code Table
,字符串表String Table
(以空值为终止符)及代码签名Code Signature
,如下图所示:
Dynamic Loader Info
由于地址空间随机化技术(ddress space layout randomization, ASLR
)和地址无关可执行技术(position-indendent excutable, PIE
),使得程序在内存的加载地址是随机的,因此需要程序在动态链接阶段将内部地址进行修正。Rebase
数据描述了哪些是对指向 MachO
内部的引用并将其修正,而 Bind
数据描述哪些是指向外部的引用并进行修正。Lazy Bind
数据描述了哪些符号需要延迟绑定,即仅在第一次使用时才会绑定,不会在启动时进行,提高启动效率;Export
数据描述了对外可见的符号。其内容都是以操作数(Opcodes
)、立即数(immediate)
以及采用uleb128/sleb128
编码的偏移值组成。
PIE(position-independent executable)
是一种生成地址无关可执行程序的技术。如果编译器在生成可执行程序的过程中使用了PIE,那么当可执行程序被加载到内存中时其加载地址存在不可预知性。PIE还有个孪生兄弟PIC(position-independent code)
。其作用和PIE相同,都是使被编译后的程序能够随机的加载到某个内存地址。区别在于PIC是在生成动态链接库时使用(Linux中的so),PIE是在生成可执行文件时使用。

以Rebase
举例,其协议和操作就是找到地址后将其值加上偏移即可,具体的获取操作数和立即数是通过REBASE_OPCODE_MASK(0xF0)
和REBASE_IMMEDIATE_MASK(0x0F)
对数据进行与&
操作,如0x100004000
的数据字节0x11
,其操作数为0x10=0x11&0xF0
对应的是REBASE_OPCODE_SET_TYPE_IMM
,立即数0x01=0x11&0x0F
为type=1(REBASE_TYPE_POINTER)
,具体的操作数及立即数对应的逻辑可查阅dyld
源码。
注意:MachOView中标注的
Actions
存在误导性,重定位、绑定等操作都是按照字节数据顺序读取并操作直至完整的读取完所有的数据,其标注具体原因未知,待确认补充!
Dynamic Symbol Table
对于Dynamic Symbol Table
中的Indirect Symbols
其内容为一个表,每个条目的内容为其在Symbol Table
中的序号,如下图:


其内容为0x3c=60
,对应的就是符号表第60个符号,通过符号表中的起始地址0x4380
,每个符号占用0x10
,则0x4740=0x4380+0x10*0x3c
,对应的就是_CFRunLoopAddSource
符号地址。
String Table
对于字符串表String Table
中内容为所有的符号名称,每个名称中间通过空字符串间隔,如下图所示:

Symbol Table
中的String Table Index
字段就是字符串表中对应的第index
个字符串。