Mach-O
为Mach Object
文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性,并提升了符号表中信息的访问速度。Mach-O
格式为大部分基于Mach
内核的操作系统所使用的,包括NeXTSTEP
, Mac OS X
和iOS
,它们都以Mach-O
格式作为其可执行文件,动态库,目标代码的文件格式。
Mach-O简介
在iOS开发中,我们的代码在编译后会生成一个.app的文件(Product文件夹下),而.app文件我们可以把它看做是一个文件夹,内部存放了APP正常运行所需要的文件,通常比较容易识别的是一些资源文件。如图:
我们通过显示包内容看下.app文件夹内都有什么:
当然我们这片文章的主角也在这个文件夹内:
系统识别Mach-O(这个名字是项目的名字)为可执行文件(Mach-O是一种可执行文件格式),我们来看下这个文件,Mach-O文件是无法直接打开或者查看包内容,这里我们需要借助MachOView工具来查看,工具是开源的如果你想看具体的实现,你可以看工具的源码,当然你可以直接下载使用。
打开后的页面是这样的:
Mach-O结构
实际上我们从使用MachOView打开后的文件目录也可以看出,Mach-O的文件结构分为三大部分:Header
,Load Commands
,Data
。
下面是官方提供的一张结构图:
根据上图,我们将我们看到的目录大致划分为:
- Mach-O 头(Mach Header):这里描述了 Mach-O 的 CPU 架构、文件类型以及加载命令等信息;
- 加载命令(Load Command):当系统加载Mach-O文件时,load command会指导苹果的动态加载器(dyld)h或内核,该如何加载文件的Data数据。
- 数据区(Data):Mach-O文件的数据区,包含代码和数据。其中包含若干Segment块,每个Segment块中包含0个或多个seciton。Segment根据对应的load command被dyld加载入内存中。
注意
:通过对比我们发现实际上官网给出的结构并不准确,在实际结果中还包含了Dynamic Loader Info
,Function Starts
,Symbol Table
,Data In Code Entries
,Dynamic Symbol Table
,String Table
,Code Signature
等。
下面我们来详细看下每部分的内容
Mach64 Header
这里我们可以和苹果开源的Darwin源码一起看方便理解,源码在这里。
32位
1 |
|
64位:
1 |
|
我们看到32位和64位的mach_header
基本是一致的,只是在64位中新增了reserved
字段,下面我们来看下其中每个字段所表示的意义。
Magic Number
offset | data | description | value |
---|---|---|---|
00000000 | FEEDFACF | Magic Number | MH_MAGIC_64 |
我们可以将其直译为魔数
,他的值(Value)有两个:
1 |
|
用于这个Mach-O文件的标识,有32位和64位两个值。由此可以看出我们的示例是一个64位的Mach-O文件。
CPU Type ,CPU SubType
offset | data | description | value |
---|---|---|---|
00000004 | 0100000C | CPU Type | CPU_TYPE_64 |
00000008 | 00000000 | CPU SubType | |
00000000 | CPU_SubType_ARM64_ALL |
CPU Type和CPU SubType 表示支持的CUP架构类型和子类型,如ARM。而具体的类型有哪些我们可以通过查询/mach/machine.h.
中的定义查看这里不做过多的扩展,具体可以看这里
我们的示例中,APP是支持所有arm64的机型的:CUP_SUBTYPE_ARM64_ALL。
File Type
offset | data | description | value |
---|---|---|---|
0000000C | 00000002 | File Type | MH_EXECUTE |
File Type 表示 Mach-O的文件类型。包括
1 |
|
这里类型均是在loader.h
文件中定义的
对于我们示例中的我们的File Type为 MH_EXECUTE
表示 可执行的二进制文件。
ncmds(Number of Load Commands)
offset | data | description | value |
---|---|---|---|
00000010 | 00000017 | Number of Load Commands | 23 |
ncmds表示load command的数量。在我们的示例中表示数量为23个。
sizeofcmds(Size of Load Commands)
offset | data | description | value |
---|---|---|---|
00000014 | 00000AF8 | Size of Load Commands | 2808 |
sizeofcmds表示所有load command的总大小。示例中总大小为2808。
Flags
offset | data | description | value |
---|---|---|---|
00000018 | 00200085 | Flags | |
00000001 | MH_NOUNDEFS | ||
00000004 | MH_DYLDLINK | ||
00000080 | MH_TWOLEVEL | ||
00200000 | MH_PIE |
Flags 是Mach-O文件的标志位。主要作用是告诉系统该如何加载这个Mach-O文件以及该文件的一些特性。有很多值,我们取常见的几种
1 |
|
结合我们的示例,我们共有4个Flags:
- MH_NOUNDEFS
- MH_DYLDLINK dyld是苹果公司的动态链接库,用来把Mach-O文件加载入内存
- MH_TWOLEVEL 表示其符号空间中还会包含所在库的信息。这样可以使得不同的库导出通用的符号。与其相对的是扁平命名空间。
- MH_PIE 每次系统加载进程后,都会为其随机分配一个虚拟内存空间(在传统系统中,进程每次加载的虚拟内存是相同的。这就让黑客有可能篡改内存来破解软件)
注意
:flags的值也定义在loader.h文件中 都可以通过源码查看。
Load Commands
Load Commands 紧跟在Header之后,用来告诉内核和dyld,如何将各个Segment加载入内存中。load command被源码表示为struct,有若干种load command,但是共同的特点是,在其结构的开头处,必须是如下两个属性:
1 |
|
对应我们示例中的Load Commands
我们在尝试去理解load_command
的注释:
1 |
|
Segment
在这么多的load command中,需要我们重点关注的是segment load command,segment command解释了该如何将Data中的各个Segment加载入内存中,而和我们APP相关的逻辑及数据,则大部分位于各个Segment中。
而和我们的Run time相关的Segment,则位于__DATA类型Segment下。
Segment load command也分为32位和64位:
32位
1 |
|
64位
1 |
|
32位和64位的segment_command基本一致,只是在64位的结构中把和寻址相关的数据类型由uint32_t
改为uint64_t
我们先看下示例中,和Segment相关的Command:
结合源码我们可以看到:
1 |
|
根据前面结构图我们知道Load Commands实际上是一个二级结构:Segment->Section,正如示例中所示
因此,下面我们在看下section的结构
Section
1 |
|
在64位和32位的section定义中,64位新增了一个reserved3保留字段,以及将section的addr和size字段由原来的uint32_t类型升级为uint64_t。
在Data中,程序的逻辑和数据是按照Segment(段)存储,在Segment中,又分为0或多个section,每个section中在存储实际的内容。而之所以这么做的原因在于,在section中,可以不用内存对齐达到节约内存的作用,而所有的section作为整体的Segment,又可以整体的内存对齐。
结合我们示例中的一个section结构如下图:
DATA(数据)
Mach-O的Data部分,其实是真正存储APP二进制数据的位置,前面的header和load command,仅是提供文件的说明以及加载信息的功能。
前面我们介绍过,我们通过Load Commands从DATA中读取数据,而Load Commands被划分成了多个Segment,也就是说 我们通过不同的Load Commands从DATA中读取不同的数据。
在介绍Segment的时候我们说过Segment被划分成__PAGEZERO
,__TEXT
,__DATA
,__LINKEDIT
这几段。
结合我们的示例,我们发现DATA被划分为:__TEXT
,__DATA
下面我们来看下这几个数据段(section):
__TEXT段
__TEXT是程序的只读段,用于保存我们所写的代码和字符串常量,const修饰常量等。
下面是几个我们常见的section:
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 类名称 |
我们来结合示例看下这几个section的内容:
cstring
我们可以从中看到lw_property
,lw_publicproperty
这两个属性名。以及我们打印的NSLog中的内容,同时我们发现,我们可能定义的某些三方key或者appid在这里都暴露在外部。
1 |
|
objc_methname
我们可以看到我们自定义的方法名lw_publicMethod
,lw_privateMethod
以及lw_property
,lw_publicproperty
重写的setter和getter方法。
classname
我们可以看到我们自定义的类的类:LWCustomClass
_DATA
__DATA段用于存储程序中所定义的数据,可读写。__DATA段下常见的sectin有:
下面我们看下常见的__DATA下的section:
Section | 用途 |
---|---|
__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 超类引用 |
这些以objc开头的DATA字段都是跟runtime有关的,后面我们会详细分析。
__objc_imageinfo
1 |
|
我们发现objc_image_info
中主要是有version字段和flag字段,
__objc_classlist
这个section列出了所有的class,包括meta class。
图中的value值是就是这个类结构体的地址(包括元类),类结构体的结构为objc中的objc_class结构体,结构如下:
1 |
|
__objc_catlist
这里可以查看代码中的所有分类,其value的值为指向分类结构体的指针
对应oc中的结构为category_t,具体结构如下:
1 |
|
__objc_protolist
该Section中记录了项目中所有的协议。 其value值为指向协议的指针
协议的结构体为protocol_t,具体如下:
1 |
|
__objc_classrefs
该section记录了哪些class被引用了,这里记录了所有被实例化的class,有些类虽然在包里,但是我们并未使用,因此这里不会有。
__objc_selrefs
这section记录哪些SEL对应的字符串被引用了,有系统方法,也有自定义方法:
__objc_superrefs
该section记录了调用super方法的类。
比如,在子类方法中,我们调用了父类的方法,就会将子类记录在这里。
__objc_const
该section用来记录在OC内存初始化过程中的不可变内容。这里所谓的不可变内容并不是我们在程序中所写的const NSInteger k = 5这种常量数据(它存在__TEXT的const section中),而是在OC内存布局中不可变得部分。
应用启动
根据上面介绍的在应用启动期间,dyld和kern会读取Mach-O文件中的Load Command去读取和加载_DATA数据段下的内容,而这一切都发生在main函数之前。所以我们看下main函数之前都发生了什么?
启动调用堆栈
添加一个符号断点(Symbolic BreakPoint)让应用在执行到_objc_init
方法是断点执行。
这样我们就能看到下面的这个调用栈:
因为_objc_init
方法是runtime的入口,因此在这之前调用的方法都是dyld和ImageLoader的操作
dyld
dyld(the dynamic link editor)动态链接器,系统 kernel 做好启动程序的初始准备后,交给 dyld 负责,dyld的主要工作内容为(参考 dyld: Dynamic Linking On OS X ):
- 从 kernel 留下的原始调用栈引导和启动自己
- 将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
- non-lazy 符号立即 link 到可执行文件,lazy 的存表里
- Runs static initializers for the executable
- 找到可执行文件的 main 函数,准备参数并调用
- 程序执行中负责绑定 lazy 符号、提供 runtime dynamic loading services、提供调试器接口
- 程序main函数 return 后执行 static terminator
- 某些场景下 main 函数结束后调 libSystem 的 _exit 函数
ImageLoader
这里的image不是图片的意思,它是一个二进制文件,你可以把他理解为一个镜像文件。内部是被编译过的符号、代码等,因此ImageLoader
作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader
实例来负责加载。
他的主要工作为:
- 在程序运行时它先将动态链接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻)
- 再从可执行文件 image 递归加载所有符号
ImageLoaderMachO
顾名思义这里应该是去加载MachO文件,从堆栈中我们可以看到主要跟doInitialization
方法和doModInitFunctions
方法。
doInitialization
这个方法的主要作用是:获取Mach-O
的init方法的地址并调用
1 |
|
1 |
|
doModInitFunctions
这个方法的主要作用是:获取Mach-O
的static initializer的地址并调用
1 |
|
总结
上述我们介绍了Mach-O文件的主要结构,以及每个segment和section的功能和字段的作用,结尾处我们通过查看应用启动调用堆栈来确认Mach-O文件何时被ImageLoader解析并加载到内存中,提供给后续的runtime使用。鉴于main函数之前系统内核,dyld,ImageLoader,rumtime做了很多准备,我们决定新开一篇文章来讲述这个过程发生了什么,敬请期待!
参考文章
XNU源码
探秘 Mach-O 文件
Mach-O文件结构理解
Mach-O 与动态链接
iOS 程序 main 函数之前发生了什么