目录
- 一、什么是Mach-O
- 二、属于Mach-O格式的文件类型
- 三、常见的Mach-O文件类型
- 四、Universal Binary通用二进制文件
- 五、Mach-O基本结构
- 5.1 窥探Mach-O的结构
- 5.2 Mach-O文件包含3个主要区域
- 5.2.1 Header
- 5.2.2 Load commands
- 5.2.3 Section
- 5.2.4 _debug相关section
- 六、Mach-O加载过程
一、什么是Mach-O
Mach-O
是Mach Object
的缩写,是Mac/iOS上用于存储程序、库的标准格式
二、属于Mach-O格式的文件类型
xnu内核源码-loader.h文件
代码语言:javascript复制#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 */
三、常见的Mach-O文件类型
- MH_OBJECT
- 目标文件(.o)
- 静态库文件(.a),静态库文件其实就是N个.o合并在一起
- MH_EXECUTE:可执行文件
- MH_DYLB:动态库文件
- .dylib
- .framework/xx
- MH_DYLINKER:动态链接编辑器
- /usr/bin/dyld
- MH_DSYM:存储着二进制文件符号信息的文件
- .dSYM/Contents/Resources/DWARF/xx(常用于分析app的奔溃信息)
四、Universal Binary通用二进制文件
五、Mach-O基本结构
5.1 窥探Mach-O的结构
5.2 Mach-O文件包含3个主要区域
- Header(头部) : 指明了cpu架构、大小端序、文件类型、Load commands个数等一些基本信息
- Load commands(加载命令) : 描述文件在虚拟内存中的逻辑结构、布局
- Raw segment data(数据区) : 在Load commands中定义的Segment的原始数据,包含了代码和数据等。
5.2.1 Header
字段 | |
---|---|
magic | 很多类型的文件,其起始的几个字节的内容是固定的,根据这几个字节的内容就可以确定文件类型,因此这几个字节的内容被称为魔数 (magic number)。 |
cputype | CPU类型以及子类型字段,该字段确保系统可以将适合的二进制文件在当前架构下运行 |
cpusubtype | CPU指定子类型,对于inter,arm,powerpc等CPU架构,其都有各个阶段和等级的CPU芯片,该字段就是详细描述其支持CPU子类型 |
filetype | 说明该mach-o文件类型(可执行文件,库文件,核心转储文件,内核扩展,DYSM文件,动态库) |
ncmds | 说明加载命令条数 |
sizeofcmds | 表示加载命令大小 |
flags | 标志位,该字段用位表示二进制文件支持的功能,主要是和系统加载,链接相关 |
reserved | 保留字段 |
- magic number 苹果平台有以下几种magic类型: 脚本 - x7FELF,常用于shell及其他解释器,如 Perl, AWK 等 通用二进制格式 - 0xcafebabe、0xbebafeca,包含多种架构支持的二进制格式,只在 macOS 上支持 MachO格式 - 根据苹果xnu内核源码,OSX和iOS上分别有以下几种不同架构对应的Magic number:
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中也可以看到下列源码:
代码语言:javascript复制 _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;
}
5.2.2 Load commands
常见的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使用到的一些数据。 |
5.2.3 Section
- 常见的section
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 | 没有初始化过的符号声明 |
利用dyld调用__mod_init_func机制脱壳
5.2.4 _debug相关section
六、Mach-O加载过程
通过前面内容,我们知道Mach-O
有多种文件类型,比如MH_DYLIB
文件、MH_BUNDLE
文件、MH_EXECUTE
文件(这些需要dyld动态加载),MH_OBJECT
(内核加载)等。所以一个进程往往不是只需要内核加载器就可以完成加载的,需要dyld
来进行动态加载配合。
加载过程底层执行:
代码语言:javascript复制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相关信息) - 解析Mach-O执行文件
- 加载共享动态库
默认的动态库会合并成一个大缓存文件,放到
/System/Library/Cache/com.apple.dyld/
目录下,按不同的架构分别保存着。其中包括UIKit
,Foundation
等基础库。 - Rebase/Bind
在系统动态加载
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
. - 准备Objc环境
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
工作。 - Initializers
通过
ObjC Runtime
在dyld
注册的通知,当Mach-O
镜像准备完毕后,dyld
会回调到ObjC
中执行load
方法,包括以下步骤: (1)获取所有non-lazy class
列表。 (2)按继承以及category
的顺序将类排入待加载列表。 (3)对待加载列表中的类进行方法判断并调用load
方法。 执行C/C
初始化构造器,如通过attribute((constructor))
注解的函数。 如果包含C
,则dyld
同样会回调到libc
库中对全局静态变量、隐式初始化等进行调用。