目录
Mach-O
是Mach Object
的缩写,是Mac/iOS上用于存储程序、库的标准格式
#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 */
#define MH_FILESET 0xc /* set of mach-o's */
字段 | |
---|---|
magic | 很多类型的文件,其起始的几个字节的内容是固定的,根据这几个字节的内容就可以确定文件类型,因此这几个字节的内容被称为魔数 (magic number)。 |
cputype | CPU类型以及子类型字段,该字段确保系统可以将适合的二进制文件在当前架构下运行 |
cpusubtype | CPU指定子类型,对于inter,arm,powerpc等CPU架构,其都有各个阶段和等级的CPU芯片,该字段就是详细描述其支持CPU子类型 |
filetype | 说明该mach-o文件类型(可执行文件,库文件,核心转储文件,内核扩展,DYSM文件,动态库) |
ncmds | 说明加载命令条数 |
sizeofcmds | 表示加载命令大小 |
flags | 标志位,该字段用位表示二进制文件支持的功能,主要是和系统加载,链接相关 |
reserved | 保留字段 |
MH_CIGAM
是MH_MAGIC
的反写,表示在小端序(litter endian)环境下使用,所以MH_MAGIC是在大端序(big endian)环境下使用
/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM NXSwapInt(MH_MAGIC)
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 NXSwapInt(MH_MAGIC_64)
Magic number显示涉及到大小端字节序,在class-dump中也可以看到下列源码:
_byteOrder = CDByteOrder_LittleEndian;
CDDataCursor *cursor = [[CDDataCursor alloc] initWithData:data];
_magic = [cursor readBigInt32];
if (_magic == MH_MAGIC || _magic == MH_MAGIC_64) {
_byteOrder = CDByteOrder_BigEndian;
} else if (_magic == MH_CIGAM || _magic == MH_CIGAM_64) {
_byteOrder = CDByteOrder_LittleEndian;
} else {
return nil;
}
常见的command及作用
command | 作用 |
---|---|
LC_SEGMENT/LC_SEGMENT_64 | 将对应的段中的数据加载并映射到进程的内存空间去 |
LC_SYMTAB | 符号表信息 |
LC_DYSYMTAB | 动态符号表信息 |
LC_LOAD_DYLINKER | 标明我们的MachO是被谁加载进去的,即动态加载连接器dyld |
LC_UUID | 标示该二进制文件唯一的 UUID,128bit |
LC_VERSION_MIN_IPHONEOS/MACOSX | 要求的最低系统版本(Xcode中的Deployment Target) |
LC_MAIN | 设置程序主线程的入口地址和栈大小 |
LC_ENCRYPTION_INFO | 加密信息 |
LC_LOAD_DYLIB | 加载的动态库,包括动态库地址、名称、版本号等 |
LC_FUNCTION_STARTS | 函数地址起始表 |
LC_CODE_SIGNATURE | 代码签名信息 |
LC_SEGMENT/LC_SEGMENT_64用于描述如何加载数据到进程,最为重要,常见的有:
常见Segment | 含义 |
---|---|
__TEXT | 代码段/只读数据段 |
__PAGEZERO | __PAGEZERO 是在可执行文件有的,动态库里没有。这个段开始地址为0(NULL指针指向的位置),是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常。 |
__DATA | 数据段 |
__LINKEDIT | 包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等。该段是只可读,不可写不可执行 |
__OBJC | 包含会被Objective Runtime使用到的一些数据。 |
Section | 含义 |
---|---|
__text | 主程序可执行的机器码 |
__stubs | 用于动态库链接的桩,本质上是一小段会直接跳入lazybinding的表对应项指针指向的地址的代码。 |
__stub_helper | 动态库链接的桩的辅助函数。上述提到的lazybinding的表中对应项的指针在没有找到真正的符号地址的时候,都指向这。 |
__cstring | 去重后的常量字符串符号表描述信息,通过该区信息,可以获得常量字符串符号表地址 |
_TEXT __const | 初始化过的常量 |
__unwind_info | 用于存储处理异常情况信息 |
__objc_methname | 保存OC里面方法名 |
__objc_classname | 保存OC类的名字 |
__objc_methtype | 保存ObOCjc类的一些信息(函数签名) |
__objc_classlist | OC的类列表 |
__objc_nlclslist | OC的 +load 函数列表,比 __mod_init_func 更早执行 |
__objc_catlist | OC的category列表 |
__objc_protolist | OC的协议列表 |
__objc_imageinfo | 保存文件中OC执行代码的一些信息 |
__objc_selrefs | 指向selectors的引用 |
__objc_protorefs | 指向protocol的引用 |
__objc_classrefs | 指向classes的引用 |
__objc_superrefs | 指向super classes的引用 |
__mod_init_func | 初始化的全局函数地址,在 main 之前被调用 |
__bss | 未初始化的静态变量 |
_got | 存储引用符号的实际地址,类似于动态符号表 |
__bss | 未初始化的静态变量 |
__nl_symbol_ptr | 非lazy-binding的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号 |
__la_symbol_ptr | lazy-binding的指针表,每个表项中的指针一开始指向stub_helper |
DATA.common | 没有初始化过的符号声明 |
通过前面内容,我们知道Mach-O
有多种文件类型,比如MH_DYLIB
文件、MH_BUNDLE
文件、MH_EXECUTE
文件(这些需要dyld动态加载),MH_OBJECT
(内核加载)等。所以一个进程往往不是只需要内核加载器就可以完成加载的,需要dyld
来进行动态加载配合。
加载过程底层执行:
execve
__mac_execve
exec_activate_image
exec_mach_imgact
load_machfile
parse_machfile
load_dylinker
一、内核加载流程
fork
进程。Mach-O
到进程空间。dyld
并将控制权交给 dyld
处理。二、dyld处理流程
主要有以下步骤:Load dylibs
-> Rebase
-> Bind
-> ObjC
-> Initializers
Xcode
进行相关的配置,进行对应的操作(如Log相关信息)
/System/Library/Cache/com.apple.dyld/
目录下,按不同的架构分别保存着。其中包括UIKit
,Foundation
等基础库。
Mach-O
文件的时候,会经过Rebase
以及Bind
两个阶段,其中Rebase
是将内部指针进行固定数值的偏移,而Bind
则正式用于将外部符号转为实际指针的步骤。Rebase
数据描述了哪些是对指向Mach-O
内部的引用并将其修正,而Bind
数据描述哪些是指向外部的引用并进行修正。rebasing
和binding
包括weak_bind
以及lazy_bind
,它们在__LINKEDIT
段内数据流的编码协议基本相同,都是以操作数(opcode)、立即数(immediate)以及uleb128/sleb128编码的偏移组成。
Rebase
- 程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行修复才能正常访问,这个操作就是Rebase
。
rebasing
的协议和操作相对简单,都是找到地址后给其值加上偏移即可。
rebase
协议:通过byte
& 0xF0
得到opcode
(操作数),byte
& 0x0F
得到immediate
(立即数),根据操作数(opcode)进行分支处理。
Bind
- 由于符号在不同的库里面,所以需要符号绑定(Bind
)这个过程。
binding
相对rebasing
较复杂一些,它多了查找依赖库的部分,不过总体协议是相似的。包含non-lazy binding
、lazy binding
和weak binding
。在ObjC
中,类继承关系以及protocol
等是non-lazy
的,启动时就需要开始绑定,而在函数里的调用外部函数等等都是lazy binding
的,在第一次调用时才会进行绑定。
binding
协议:和rebasing
相同,通过byte
& 0xF0
得到opcode
(操作数),byte
& 0x0F
得到immediate
(立即数),根据操作数(opcode)进行分支处理。每次binding
是在 rebasing
之后进行的,他们交替进行,每个Mach-O
镜像加载完成后需要将内部的地址引用都修正为偏移之后的正确地址,然后执行binding
来修改外部引用地址。
Export
- export
数据描述了对外可见的符号,通过objdump
命令可查看外部可见符号;
在进行rebasing
之前,内核只是将Mach-O
数据映射到虚拟内存,还未加载到内存。当rebasing
阶段开始在__DATA
段进行读取时,发现没有数据,产生了page fault
内核异常,这个时候内核才会从磁盘将相应的页(page)读到内存继续进行rebasing
.
dyld
将主程序Mach-O
基址指针和包含的ObjC
相关类信息传递到libobjc
。
ObjC Runtime
从__DATA
段中获取ObjC
类信息,由于ObjC
是动态语言,可以通过类名获取其实例,所以Runtime
维护了一个映射所有类的全局类名表。当加载的数据包含了类的定义,类的名字就需要注册到全局表中。
获取 protocol
、category
等类相关属性并与对应类进行关联。ObjC
的调用都是基于 selector
的,所以需要对 selector
全局唯一性进行处理。
以上步骤由 dyld
启动 libSystem.dylib
统一对基础库进行调用执行,这里面就包含了 libobjc
的 Runtime
,同时 Runtime
会在 dyld
绑定回调,当 dyld
处理完相关数据后就会调用 ObjC Runtime
执行 Setup
工作。
ObjC Runtime
在 dyld
注册的通知,当 Mach-O
镜像准备完毕后,dyld
会回调到 ObjC
中执行 +load
方法,包括以下步骤:
(1)获取所有 non-lazy class
列表。
(2)按继承以及 category
的顺序将类排入待加载列表。
(3)对待加载列表中的类进行方法判断并调用 +load
方法。
执行 C/C++
初始化构造器,如通过 attribute((constructor))
注解的函数。
如果包含 C++
,则 dyld
同样会回调到 libc++
库中对全局静态变量、隐式初始化等进行调用。