Mach-O文件介绍及dyld加载流程

什么是Mach-O文件?

Mach-O文件是Mach object文件的缩写,它在NeXTSTEP.MacOS,iOS等操作系统中作为可执行文件,目标代表文件,库文件的文件格式.类似于Windows系统中的PE格式,linux系统中的elf格式文件.相对于a.out这样的格式,Mach-O提供了更多的扩展性.

属于Mach-O文件的常见格式:

1.目标文件 *.o

2.库文件 *.a / *.dylib / *.framework

3.可执行文件

4.动态链接器 dyld

5.dsym(编译中转文件,Xcode在编译项目后会生成一个与App同名的dsym文件)

如何查看Mach-O文件格式?

通过file命令可以在终端查看Mach-O文件的一些信息.

file 文件名 可以查看当前目录下的mach-o文件信息

可以看到这个文件是一个通用二进制文件(universal binary).它有三种架构.armv7.armv7s.arm64.

通用二进制文件介绍:

通用二进制文件是苹果公司提出的一种的新的二进制的存储文件的结构,它可以同时存储多种架构的二进制指令.使CPU在读取该二进制文件时候可以自动检测并选用合适的架构以最理想的方式来读取.通常而言因为该文件会同时存储多种架构所以该文件比单一架构的二进制文件要大所以也被称为胖二进制文件,当然这种文件大小的增加并不会是1+1=2这样的夸张,因为通常两种架构有共通的非执行资源,所以不会达到倍数的大小增加.并且因为只会执行单一架构的指令,所以也不会耗费多余的内存空间.

Mach-O文件的合并,拆分:

我们可以通过终端来进行Mach-O文件的合并和拆分.

合并:$lipo -create MachO1   MachO2  -output 输出文件路径

拆分:$lipo MachO文件 –thin 架构 –output 输出文件路径

查看当前Mach-O的架构:$lipo -info MachO文件

Mach-O文件的结构:

Mach-O文件的结构我们大致可以用下面的图来介绍:

Mach-O文件结构

一个完整的Mach-O文件分为:Header.LoadCommands.Data这三大部分.

具体如果要查看Mach-O的文件信息我们可以用otool终端来查看,或者使用MachOView工具.

使用终端otool命令来查看Mach-O文件的信息

当然使用终端来查看或许并不够直观,我们还可以使用MachOView来查看(推荐).

采用MachOView来查看Mach-O的文件

Header:

MachO的Header包含了整个MachO里面的关键信息,使得CPU能够快速的知道这个MachO文件的基本信息.

它在Mach.h中是一个结构体,包含了以下的含义,同样对应着MachO文件里的信息.

header
这里也是Header

我们可以对照着MachOView里的信息来看

Magic                                         指定了这个machO是64位还是32位.

CPU Type                                  代表采用的是哪种基本  架构. 这里是arm

CPU SubType                            代表具体采用的架构名称 这里是armv7

File Type                                    这个Mach-O的类型 这里注明的是可执行文件

Number of Load Commands      loadCommand的指令条数

Size of Load Commands           loadCommand所占大小

Flags                                         该Mach-O可支持的功能

Load Commands:

在Mach-O文件中,loadCommand是用于加载指令的,它的大小和数目在header中已经被提供,在Mach.h下以loadCommand结构体展示

loadcommand

该结构体中有两个成员,一个cmd提供该loadcommand的类型,cmdsize则表示command的大小.

loadCommands中记录了很多信息,包括动态链接器(比如dyld)的位置,程序的入口地址(main),依赖库的信息,代码的位置.符号表的位置等等.

loadCommands

PAGEZERO: 空指针陷阱段,这里是记录的共享虚拟空间信息,它并不会占用实际的磁盘空间,只是一片虚拟内存,这里记录了它的位置和大小,这片空间一般用于置放空指针.

TEXT: 只读数据段,记录了TEXT的起始位置和大小还有偏移值等信息,这些信息会告知具体的TEXT段在哪里.

DATA:读写数据段,记录了DATA段的起始位置和大小还有偏移值等信息,这些信息会告知具体的DATA段在哪里.

LINKEDIT:链接器使用段,这里记录了链接器(通常是dyld)需要的信息的位置.

LC_DYLD_INFO_ONLY:记录具体的链接器需要的信息,比如重定向,懒加载,绑定等.

LC_SYMTAB:符号表的信息,记录符号表的位置,偏移量,数据个数等,便于dyld使用

LC_DYSYMTAB:符号表的额外信息,这些信息也会提供给dyld.

LC_LOAD_DYLINKER:该Mach-O使用的链接器信息,记录了具体使用哪个链接器接管内核后续的加载工作,以及链接器的位置信息,通常是dyld.

LC_UUID:Mach-O唯一标识符.

LC_VERSION_MIN_IPHONES:该Mach-O运行的最低系统版本.

LC_SOURCE_VERSION:源代码版本信息.

LC_MAIN:入口地址.dyld会通过这个段去跳转程序的主入口.

LC_ENCRYPTION_INFO_64:加密标识,标识了是否被加密,加密内容的偏移及大小等.

LC_LOAD_DYLIB:依赖库信息,dyld会通过这个段去加载动态库,这个段标注了库的位置以及版本等信息.

LC_RPATH:@rpath的路径信息.

LC_FUNCTION_STARTS:函数起始地址表.

LC_DATA_IN_CODE:代码段非指令的表.

LC_CODE_SIGNATURE:代码签名信息.

DATA:

在loadCommands后就是Data区域,这个区域存储了我们具体的只读代码及可读写的代码,比如我们的方法,符号表字符表,代码数据,链接器所需要的数据比如重定向,符号绑定等等,这里存储的是具体的数据.

在data区中,section节占了相当大的比例.它在Mach.h中是以结构体section_64表示:

section段的结构体

Section节在MachO中集中体现在TEXT和DATA两段里.

DATA段在MachOView的呈现

__text: 主程序代码

__stubs, __stub_helper: 用于动态链接的桩

__cstring: 程序中c语言字符串

__const: 常量

__TEXT,__objc_methname:OC方法名称

__TEXT__objc_methtype:OC方法类型

__TEXT__objc_classname:OC类名

__DATA,__objc_classlist:OC类列表

__DATA,__objc_protollist:OC原型列表

__DATA,__objc_imageinfo:OC镜像信息

__DATA,__objc_const:OC常量

__DATA,__objc_selfrefs:OC类自引用(self)

__DATA,__objc_superrefs:OC类超类引用(super)

__DATA,__objc_protolrefs:OC原型引用

__DATA, __bss: 没有初始化和初始化为0 的全局变量

Dynamic Loader Info:动态链接器所需要使用的信息(重定向,符号绑定,懒加载绑定等..)

后续的信息就是函数起始位置,符号表,字符表,代码签名等.

dyld加载流程:

我们熟知的ios程序是从main函数开始,但是实际上我们在运行时候发现,在堆栈中main并不是最开始的位置,在上面还有一个start.

可以看到main并非一切的开始

从信息中发现,在前面是dyld在搞事情.那么我们可以尝试下通过load方法来查看具体在那之前做了什么.

我们通过load方法来拦截查看堆栈.

load的堆栈信息

可以发现一切的开始是dyldbootstrap::start这个函数.它去调用了dyld::main函数.这个函数从外部传入Mach-O的header,在dyld::main中,dyld会去设置运行环境,配置相关的环境变量.

在dyld的main中配置相关环境变量

在环境变量配置完毕后,dyld会去加载共享缓存

加载的步骤是先通过checkShareRegionDisable函数检查是否被关闭,iOS下必须开启共享缓存,如果没有被禁用,那么就会调用mapSharedCache函数去加载,当然实际加载是在该函数内调用的loadDyldCache函数,加载共三种,fast Path(已经加载的不需要再加载),slow path(第一次调用则去加载.mapCacheSystemWide),还有一种是模拟器下(simulator)的.

共享缓存库的加载

当共享缓存被加载后,接下来,dyld就会继续在main函数中加载我们的主程序也就是我们的可执行文件.

我们在方法中找到instantiateFromLoadedImage这个函数,在这个函数里,dyld会实例化我们的可执行文件.

在instantiateFromLoadedImage中去实例化我们的可执行文件

我们进入这个函数,它实际上是通过我们传进来的machO的header判断当前cpu是否支持当前我们的machO的架构.如果支持则调用instantiateMainExecutable函数去实例化我们的可执行文件,并添加到imageList中.

可执行文件的实例化

当然可执行文件的实例化是在instantiateMainExecutable函数内部实现的,在该函数内部先调用了sniffLoadCommands,这个函数通过读取loadCommand段内的信息.去加载.

在加载前调用了该函数

在sniffLoadCommands中严格判断了loadCommands的条数,不能超过255条,依赖的库不能超过4095个,

sniffLoadCommand内部的判断

最后该函数会修改Compress值,外部的instantiateMainExecutable函数会通过这个值来决定加载主程序的方式.

在instantiateMainExecutable函数中会通过sniffLoadCommands修改的compressed值来决定加载方式

在主程序被实例化加载后,接下来dyld就会继续在main函数中去加载我们插入的动态库,

加载插入的动态库

具体加载函数在loadInsertedDylib里进行.

加载插入动态库

在加载插入动态库后就是link链接我们的插入库

链接Link

在link方法中不光是链接我们的插入动态库,还会在函数内通过recursiveLoadLibraries函数循环加载我们的所有的依赖库.在加载后再Rebase每一个都添加上偏移值以得到真正的依赖库的地址也就是重定位.

链接,定位依赖库

在链接定位后,还是在这个函数中继续对依赖库进行符号绑定,弱绑定等一系列操作,当这些都做完了主程序也就被加载链接完成.

符号绑定,弱绑定

跟主程序加载链接一致,dyld当得知插入依赖库长度大于0会遍历加载链接这些库.链接完毕后就会将主程序与这些库绑定起来.

循环遍历进行链接加载这些插入的动态库

在这些依赖库的操作全部完成,就会调用initializeMainExecutable函数来初始化我们的主程序!

初始化我们的主程序

通过堆栈我们发现在initializeMainExecutable中调用了runInitializers函数,在runInitializers函数中调用了processInitializers函数,在processInitializers函数中调用的recursiveInitialization函数.继续更进,此时发现无法跳转了,我们通过commad+shift+o直接查找函数名,找到它的源文件,在这个文件下我们找到notifySingle这个函数.

notifySingle

我们继续通过查找源文件查看这个函数.在这个函数内,我们通过堆栈得知接下来会调用loadImages函数,但是通篇看下来并没有调用这个函数,此时我们发现这里有个函数指针比较可疑.这个指针可能就是指向的loadImages.

可疑的函数指针

我们搜索这个sNotifyObjCInit指针找到为其赋值的函数registerObjCNotifiers.

赋值sNotifyObjCInit指针的函数

此时搜索整个工程,我们查找调用registerObjCNotifiers函数的调用者,找到_dyld_objc_notify_register这个函数,也即是这个函数提供了sNotifyObjCInit这个值的来源.但是我们再搜索_dyld_objc_notify_register时,发现没有人调用它,

_dyld_objc_notify_register

此时我们通过给项目下符号断点直接断_dyld_objc_notify_register,我们发现是objc_init这个函数调用的.

断点查找

我们在objc项目源文件中搜索_objc_init可以发现,也就是在这里调用了我们之前的_dyld_objc_notify_register函数,在这里调用了load_images方法!

load_images

我们跟进load_images可以发现在这里调用了call_load_methods

call_load_methods

我们跟进call_load_methods,发现函数内就是循环调用我们的Objc类的load方法!

循环加载OBJC的类的load方法

接下来dyld就会调用doModInitFunctions这个函数会调用执行我们程序的特殊函数,比如全局的C++的构造方法.其实实质上就是dyld会读取Mach-O里DATA段中的init_func这个字段进行调用里面的函数.

调用程序的特殊函数

最终一系列的操作完毕后,dyld就会去查找我们主程序的入口,对应我们Mach-O的LC_MAIN.在找到后返回一个result结果,也就调起了我们主程序的main函数,结束掉dyld_start整个流程.

找到我们主程序的入口

推荐阅读更多精彩内容