美文网首页
OC底层探索(十二): dyld4应用程序加载初探

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

作者: ShawnAlex | 来源:发表于2022-05-02 23:58 被阅读0次

    所用版本:

    • 处理器: 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应用程序加载流程图

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/kkpzsltx.html