iOS应用框架Objective-C RunTimeruntime+runloop

Mach-O简介与App加载流程

2021-01-19  本文已影响0人  凯歌948

Mach-O

【Mach-O】 为 Mach Object 文件格式的缩写,是 iOS 系统不同运行时期 可执行文件 的文件类型统称。它是一种用于 可执行文件、目标代码、动态库、内核转储的文件格式。

【Mach-O】 的三种文件类型:Executable、Dylib、Bundle
Executable 是 app 的二进制主文件。
Dylib 是动态库,动态库分为 动态链接库 和 动态加载库。

动态链接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着被加载到内存中。【随着程序启动而启动】
动态加载库:当需要的时候再使用 dlopen 等通过代码或者命令的方式加载。【程序启动之后】

Bundle 是一种特殊类型的Dylib,你无法对其进行链接。所能做的是在Runtime运行时通过dlopen来加载它,它可以在macOS 上用于插件。
Image (镜像文件)包含了上述的三种类型。
Framework 可以理解为动态库。

Mach-O的结构

mach-o结构.png

Header:保存【Mach-O】的一些基本信息,包括运行平台、文件类型、LoadCommands指令的个数、指令总大小,dyld标记Flags等等。
Load Commands:紧跟Header,这些加载指令清晰地告诉加载器如何处理二进制数据,有些命令是由内核处理的,有些是由动态链接器处理的。加载【Mach-O】文件时会使用这部分数据确定内存分布以及相关的加载命令,对系统内核加载器和动态连接器起指导作用。比如我们的main()函数的加载地址、程序所需的dyld的文件路径、以及相关依赖库的文件路径。
Data:每个segment的具体数据保存在这里,包含具体的代码、数据等等。
segment:【Mach-O】 镜像文件 是由 segments 段组成的。段的名称为大写格式。所有的段都是 page size 的倍数,在arm64上为 16kB,其它架构为 4KB。

常见的segments:
__TEXT:代码段,包含头文件、代码和只读常量。只读不可修改
__DATA:数据段,包含全局变量,静态变量等。可读可写
_LINKEDIT:如何加载程序,包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。只读不可修改。

有两种主要的技术来保证应用的安全:ASLR 和 Code Sign

【ASLR】的全称是Address space layout randomization,翻译过来就是“地址空间布局随机化”。App被启动的时候,程序会被映射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而【ASLR】技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函数的地址。

【Code Sign】相信大多数开发者都知晓,这里要提一点的是,为了在运行时 验证【Mach-O】 文件的签名,在进行【Code Sign】的时候,加密哈希不是针对于整个文件,而是针对于每一个Page的。并存储在 __LINKEDIT 中。这就保证了在dyld进行加载的时候,可以对每一个page进行独立的验证。

dyld

当内核完成映射进程的工作后,会将名字为 dyld 的 Mach-O 文件映射到进程中的随机地址,它将PC 寄存器设为 dyld 的地址并运行。dyld 在应用进程中运行的工作是加载应用依赖的所有动态链接库,准备好运 行所需的一切,它拥有的权限跟应用程序一样。

dyld(the dynamic link editor),【动态链接器】是苹果操作系统一个重要部分,在 iOS / macOS 系统中,仅有很少的进程只需内核就可以完成加载,基本上所有的进程都是动态链接的,所以 Mach-O 镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容填充,这个填充的工作就是由 dyld 来完成的。

【动态链接加载器】在系统中以一个用户态的可执行文件形式存在,一般应用程序会在Mach-O文件部分指定一个 LC_LOAD_DYLINKER 的加载命令,此加载命令指定了dyld的路径,通常它的默认值是“/usr/lib/dyld”。系统内核在加载Mach-O文件时,会使用该路径指定的程序作为动态库的加载器来加载dylib。

dyld 流程

Load dylibs -> Rebase -> Bind -> ObjC ->Initializers

Load dylibs:
从主执行文件header获取到需要加载的所依赖的动态库列表,而header早就被内核映射过。然后它需要找到每个dylib,然后打开文件,读取文件起始位置,确保它是Mach-O文件。接着会找到代码签名并将其注册到内核。然后在dylib文件的每个segment上调用mmap()。应用所依赖的dylib文件可能会再依赖其他dylib,所以dyld所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载100到400 个dylib文件,但大部分都是系统的dylib,它们会被预先计算和缓存起来,加载速度很快。

Fix-ups:
在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是Fix-ups。代码签名使得我们不能修改指令,那样就不能让一个dylib 调用另一个 dylib,这是就需要很多间接层。

Mach-O中有很多符号,有指向当前 Mach-O 的,也有指向其他 dylib 的,比如printf。那么,在运行时,代码如何准确的找到printf的地址呢?

Mach-O中采用了PIC技术,全称是Position Independ code。意味着代码可以被加载到间接的地址上。当你的程序要调用printf的时候,会先在 __DATA 段中建立一个指针指向printf,在通过这个指针实现间接调用。dyld这时候需要做一些fix-up工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分:rebasing和binding。

Rebasing:在镜像内部调整指针的指向。
Binding: 将指针指向镜像外部的内容。
之所以需要Rebase,是因为刚刚提到的 ASLR 使得地址随机化,导致起始地址不固定,另外由于 Code Sign,导致不能直接修改 Image。Rebase的时候只需要增加对应的偏移量即可。(待Rebase的数据都存放在__LINKEDIT中,可以通过MachOView查看:Dynamic Loader Info -> Rebase Info)

Binding就是将这个二进制调用的外部符号进行绑定的过程。 比如我们objc代码中需要使用到NSObject, 即符号OBJC_CLASS$_NSObject,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要Binding这个操作将对应关系绑定到一起。

Rebase解决了内部的符号引用问题,而外部的符号引用则是由Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于Rebase来说是略慢的。

dyld 2 和 dyld 3
dyld区别.png

在 iOS 13之前,所有的第三方App都是通过dyld 2来启动 App 的,主要过程如下:
解析 Mach-O的Header 和 Load Commands,找到其依赖的库,并递归找到所有依赖的库
加载Mach-O文件
进行符号查找
绑定和变基
运行初始化程序

dyld 3被分为了三个组件:
一个进程外的Mach-O 解析器
预先处理了所有可能影响启动速度的search path、@rpaths和环境变量
然后分析Mach-O的Header和依赖,并完成了所有符号查找的工作
最后将这些结果创建成一个启动闭包
这是一个普通的daemon进程,可以使用通常的测试架构

一个进程内的引擎,用来运行启动闭包
这部分在进程中处理
验证启动闭包的安全性,然后映射到dylib之中,再跳转到main函数
不需要解析Mach-O的 Header 和依赖,也不需要符号查找。

一个启动闭包缓存服务
系统App的启动闭包被构建在一个Shared Cache 中,我们甚至不需要打开一个单独的文件
对于第三方的App,我们会在App安装或者升级的时候构建这个启动闭包。
在iOS、tvOS、watchOS中,这一切都是App启动之前完成的。在macOS上,由于有Side Load App,进程内引擎会在首次启动的时候启动一个daemon进程,之后就可以使用启动闭包启动了。

dyld 3 把很多耗时的查找、计算和I/O 的事件都预先处理好,这使得启动速度有了很大的提升。

App加载流程

编译过程

其中编译过程如下图所示,主要分为以下几步:
源文件:载入.h、.m、.cpp等文件
预处理:替换宏,删除注释,展开头文件,产生.i文件
编译:将.i文件转换为汇编语言,产生.s文件
汇编:将汇编文件转换为机器码文件,产生.o文件


编译过程.jpg

dyld加载流程分析

根据dyld源码,以及libobjc、libSystem、libdispatch源码协同分析
在load方法处加一个断点,通过bt堆栈信息查看app启动是从哪里开始的

+ (void)load{
    NSLog(@"%s",__func__);  //此处加断点
}


控制台数据结果:
/*
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x000000010532ee17 002-应用程加载分析`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
    frame #1: 0x00007fff201805e3 libobjc.A.dylib`load_images + 1442
    frame #2: 0x0000000105342e54 dyld_sim`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 425
    frame #3: 0x0000000105351887 dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 437
    frame #4: 0x000000010534fbb0 dyld_sim`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
    frame #5: 0x000000010534fc50 dyld_sim`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
    frame #6: 0x00000001053432a9 dyld_sim`dyld::initializeMainExecutable() + 199
    frame #7: 0x0000000105347d50 dyld_sim`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4431
    frame #8: 0x00000001053421c7 dyld_sim`start_sim + 122
    frame #9: 0x000000010bea485c dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) + 2308
    frame #10: 0x000000010bea24f4 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 837
    frame #11: 0x000000010be9d227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
    frame #12: 0x000000010be9d025 dyld`_dyld_start + 37
(lldb) 
*/

【app启动起点】:通过程序运行发现,是从dyld中的_dyld_start开始的,所以需要去OpenSource下载一份dyld的源码来进行分析

dyld加载流程.png

参考资料:

[iOS-底层原理 15:dyld加载流程](https://www.jianshu.com/p/db765ff4e36a
[iOS 应用程序加载](https://www.jianshu.com/p/bffb5bdb4f13

上一篇下一篇

猜你喜欢

热点阅读