OC底层探索(十二): dyld4应用程序加载初探

所用版本:

  • 处理器: Intel Core i9
  • MacOS 12.3.1
  • Xcode 13.3.1
  • dyld-941.4
  • objc4-838

虽然苹果官网发布的正式版才到dyld-852.2

dyld-852.2

不过github上可以下到最新非正式版本, 写文章时候最新版本为dyld-941.4, 估计以后还会更新。dyld3dyld4我认为改动还是比较大。

dyld-941.4
dyld设计

dyld4的针对于的mach-o解析器 ( iOS上可执行文件格式是Mach-O格式, 下方也有具体解释) 方面跟dyld3相同,但是引入了 JustInTime 的加载器来优化。

  • dyld3: 相比dyld2新增预构建/闭包, 目的是将一些启动数据创建为闭包存到本地,下次启动将不再重新解析数据,而是直接读取闭包内容
  • dyld4: 采用pre-build + just-in-time 预构建/闭包+实时解析的双解析模式, 将根据缓存有效与否选择合适的模式进行解析, 同时也处理了闭包失效时候需要重建闭包的性能问题。

初看下dyld新旧版本对比, 看一下dyld加载流程相较之前改变

dyld旧版本
dyld4新版本模拟器改动
dyld4新版本真机改动


我这里先带入dyld以及dyld做了什么 , 先看个例子

普通的一个OC项目, ViewController中加一个+ (void)load方法, main中加一个函数SAFuc

ViewController
main

运行一下, 看下它们走的顺序, 结果如下

运行

会发现, 先走的load方法, 再走SAFuc, 最后走的main中的Hello world

这块其实就会有疑问, 不应该先走mainHello world么? 所以我们就要看下应用程序加载流程

动态库/静态库/编译过程

先看下编译过程的流程图

我们先了解动态库, 静态库, 代码编译过程这几个概念, 方便后面探索

静态库 / 动态库

通常程序都会依赖系统一些库, 库是什么呢? 其实库就是一些可执行的二进制文件, 能被操作系统加载到内存里面中。库分为两种静态库, 动态库

静态库

.a, .lib等。链接阶段时静态库会被完整地复制, 一起打包在可执行文件中,被多次使用就有多份冗余拷贝。

静态库

  • 优点: 编译完成之后, 链接到目标程序中, 同时打包到可执行文件里面, 不会有外部依赖。

  • 缺点: 静态库会有两份, 所以会导致目标程序体积增大, 对内存, 性能, 速度消耗很大。并且相同静态库每个app中都会拷贝一份。

动态库

.framework等。程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入。苹果大部分都是动态库

动态库
  • 优点: 不需要拷贝到目标程序, 减少App包的体积

    • 多个App可以使用同一个动态库, 共享内存, 节约资源

    • 由于运行时才会去加载, 那么可以在App不使用时随时对库进行替换或更新, 更新灵活

  • 缺点: 动态载入会带来一部分性能损失, 同时动态库也会使得程序依赖于外部环境。一旦动态库没有或消失, 程序会出现问题。

代码编译过程

编译过程
  • 源文件: .h, .m, .cpp, .c等文件
  • 预编译: 预先编译文件(源文件), 词法语法分析, 替换宏, 删除注释, 展开头文件, 产生.i文件
  • 编译: 编译文件, 将.i文件转换为汇编语言, 产生.s文件(汇编文件)
  • 汇编: 将汇编文件转换为机器代码文件, 产生.o文件
  • 链接: 把之前所有操作的文件链接到程序里面来, 对.o文件中引用其他库的地方进行引用, 生成最后的 可执行文件。动态库与静态库区别其实就是链接的区别。
可执行文件

可执行文件位置: 通常编译后的 程序.cpp显示包内容, 可找到可执行文件(黑黑的一个文件)

可执行文件

其实可执行文件就是能够运行起来的文件, 我们也可以把它拖到终端中回车, 可发现也能打印出信息。(ios项目需要真机运行, 直接拖入终端回车会报错)

当然我们如果想要查询系统动态库可执行文件, 以CoreFoundation为例

CoreFoundation
  • 断点image listCoreFoundation按路径搜索 可以找到CoreFoundation.frame
CoreFoundation`可执行文件
  • CoreFoundation.frame右键选择显示包内容即可看到CoreFoundation的可执行文件


dyld

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的重要组成部分,在app被编译打包成可执行文件格式的Mach-O文件后,交由dyld负责连接动静态库,加载程序

dyld流程
  • 这里的image不是图片是镜像文件, 库加载进去就是映射, 映射一份到内存, 而这个东西就image。映射可以理解成, 例如 动态库都存在沙盒路径磁盘里面, 当我们用到相应动态库时候, copy一份(找了一个替身)加载到我们用到程序的内存里面。

探索dyld之前我们要先了解入口, 在load方法处加一个断点,bt查看下, 当然也可以通过左侧的堆栈信息查看。

旧版本
旧版本dyld入口
新版本
dyld4新版本模拟器改动
dyld4新版本真机改动

栈结构, 先进后出, 所以要从后往前看。

  • 旧版本dyld: _dyld_start开始, 接下来走dyldbootstrap, 源码入口需要在dyld_start开始。
  • 新版本dyld: start开始接下来走 dyld4prepare方法, 源码入口需要在start中开始探索。

当然我们也可以走下汇编看下dyld`在哪里,

旧版本

可发现在libdyld.dylib这里面

旧版本在libdyld.dylib

既然在libdyld.dylib里面, 那我们可以去苹果官方 Source Browser 可下到dyld源码, 如下图

新版本

而新版本......不得不说官方很严谨。(后面拿真机做例子)

新版本
新版本
旧版本

dyld源码之后全局搜索dyld_start(找入口), 入口是汇编写的, 看arm环境的就行。 能找到接下去走dyldbootstrap, 这也跟之前bt打印内容一致

dyld_start

dyldStartup__dyld_start(入口函数)查找时发现,是由dyld汇编实现(.s汇编文件),通过注释发现, 下面会调用call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue),是一个C++方法, 那么我们根据名字dyldbootstrap, 去寻找他的start方法。

新版本

新版本我们直接搜索dyldbootstrap, 肯定是无了

错误示范

我们先搜索start, 在同样的dyldStartup

汇编`start`

其实注释已经告诉我们, 此汇编代码对齐堆栈并跳入C代码:dyld:: start(const KernelArgs* kernArgs) 那么搜索 start(const KernelArgs* kernArgs)方法看一下, 有

void start(const KernelArgs* kernArgs)

这里的start方法是dyld的入口点。那么我们接着这里进行探索。 往下看有一个"prepare"准备 方法MainFunc appMain = prepare(state, dyldMA);,

prepare

可看到这个方法是处理相关依赖绑定的方法, 那么进入看下其源码, 看看到底准备些什么内容

prepare底层

[ 配置环境/平台/路径/版本等信息 ]

看下gProcessInfostruct dyld_all_image_infos* gProcessInfo = &dyld_all_image_infos;是一个存储dyld所有镜像信息的一个结构体

gProcessInfo底层

dyld_all_image_infos

可看出dyld_all_image_infos包含信息比较多, mach_header, dyld_uuid_info, dyldVersion等等。

其中mach_headerMach-O的头部,而dyld加载的文件就是Mach-O类型的,即Mach-O类型是可执行文件类型,由四部分组成:Mach-O头部Load CommandsectionOther Data,可以通过MachOView可查看可执行文件信息

MachOView可查看可执行文件信息

回到prepare方法, 接着往下看

[进行pre-build, 创建mainLoader]

预构建

接下来会创建一个mainLoader 主装载器, 如果熟悉dyld3的小伙伴知道, 旧版本是创建一个ImageLoader镜像装载器

旧版本镜像装载器
mainLoader 主装载器

mainLoader主装载器, 可以理解成一个容器, 这里面陆续添加 可执行文件, 动态库等等, 都装载完成之后经由后续一些处理, 就是我们打开的App。

[ 创建just-in-time ]

just-in-time

这是dyld4一个新特性, dyld4在保留了dyld3mach-o 解析器基础上,同时也引入了 just-in-time的加载器来优化, 这里稍微细说一下。

首先dyld3 出于对启动速度的优化的目的, 增加了预构建(闭包)。App第一次启动或者App发生变化时会将部分启动数据创建为闭包存到本地,那么App下次启动将不再重新解析数据,而是直接读取闭包内容。当然前提是应用程序和系统应很少发生变化,但如果这两者经常变化等, 就会导闭包丢失或失效。所以dyld4 采用了 pre-build + just-in-time 的双解析模式,预构建 pre-build 对应的就是 dyld3 中的闭包,just-in-time 可以理解为实时解析。当然just-in-time 也是可以利用 pre-build 的缓存的,所以性能可控。有了just-in-time, 目前应用首次启动、系统版本更新、普通启动,dyld4 则可以根据缓存是否有效选择合适的模式进行解析。

[ 装载内容 ]

装载

往下看可看到, mainLoader进行装载, 装载可执行文件, 动态库等等

装载

记录插入信息, 遍历所有dylibs, 一些记录检查操作继续往下走。

[插入缓存]

插入缓存

这里是对dyld缓存的一个处理, 其中stateprepare传入进来的参数, 其定义APIs& state = APIs::bootstrap(config, sLocks);是APIs方法里面的bootstrap引导程序方法。

Loader类
applyInterposingToDyldCache

image.png

接下来是一些其他通知和写入操作, 简单看一下, 之后是下一个重点内容runAllInitializersForMain

[运行初始化方法]

runAllInitializersForMain

前面稍微提过state定义APIs& state = APIs::bootstrap(config, sLocks); , 源自DyldAPIs, 那么我们进入看一下

runAllInitializersForMain

notifyObjCInit 函数

在执行完初始化之后会执行notifyObjCInit, 告诉objc 去运行所有 +load 方法, 而此时系统main还没有执行, 这也就是为什么+ load方法执行在main前面的原因。我们看一下notifyObjCInit内部

notifyObjCInit

其中 _notifyObjCInit我们看一下。首先可以看到_notifyObjCInit定义是_dyld_objc_notify_init _notifyObjCInit = nullptr;

_notifyObjCInit

因为判断是要_notifyObjCInit 非null才继续后面, 所以我们要搜索下_notifyObjCInit什么地方赋值

setObjCNotifiers

_notifyObjCInitsetObjCNotifiers方法中的第二参数_dyld_objc_notify_init init
继续找setObjCNotifiers_dyld_objc_notify_register

_dyld_objc_notify_register

这个方法其实在objc4源码_objc_init方法中见过

objc_init

我们在objc_init内部调用了dyld_objc_notify_register方法, 并为其传入参数load_images (第二个参数 init)。

load_images
call_load_methods
call_class_loads

load_imagescall_load_methodscall_class_loads内部也可以看出会 循环调用所有+load 方法,直到不再有。

[link动态库和主程序]

runInitializersBottomUpPlusUpwardLinks

回到runAllInitializersForMain继续看, runInitializersBottomUpPlusUpwardLinks循环link动态库, 再link可执行文件

runInitializersBottomUpPlusUpwardLinks
link动态库

[加载主程序入口]

runAllInitializersForMain准备工作完成之后, 寻找App中main函数, App正常运行

找main

综上也验证了dyld 打印信息

dyld4打印信息
dyld4应用程序加载流程图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容