美文网首页
13 - dyld源码解析

13 - dyld源码解析

作者: 卡布奇诺_95d2 | 来源:发表于2021-05-12 18:26 被阅读0次

    基本概念简介

    dyld

    dyld全名The dynamic link editor。它是苹果的动态链接器,是苹果操作系统一个重要组成部分,在应用被编译打包成可执行文件格式的Mach-O文件之后 ,交由dyld负责链接,加载程序。

    dyld是开源的,我们可以通过官网下载它的源码。并通过源码来阅读理解它的运行方式,了解系统加载动态库的细节。

    共享缓存

    由于iOS系统中UIKit / Foundation等系统库每个应用都会通过dyld加载到内存中,因此,为了节约内存空间,苹果将这些系统库放在了一个地方:动态库共享缓存区 (dyld shared cache)。同理,在Mac OS中也一样有一个动态库的共享缓存区。

    有了共享缓存区,类似NSLog的函数实现地址,就不会在我们自己的工程的Mach-O中,那么问题来了,当我们的工程想要调用NSLog方法 , 如何能找到其真实的实现地址呢?

    在工程编译时,所产生的Mach-O可执行文件中会预留出一段空间,这个空间其实就是符号表,存放在_DATA数据段中(因为_DATA段在运行时是可读可写的),在工程运行时,dyld根据Load Commands中列出的动态库,去做绑定操作,将方法的真实地址写到_DATA段符号表中。

    ASLR

    ASLR的全名:Address Space Layout Randomization,地址空间配置随机加载;是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术;iOS4.3开始引入了ASLR技术。

    ASLR的作用是地址空间配置随机加载,利用随机方式配置数据地址空间,使某些敏感数据(例如APP登录注册、支付相关代码)配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击。

    dyld 源码探索

    本节的重点是探索dyld,因此如何从函数调用栈中找到dyld的入口函数这里只简单描述。
    【步骤1】在ViewController类中增加load方法,并在load方法中设置断点。

    【步骤2】当断点停下时,使用lldb指令bt查看函数调用栈。

    函数调用s栈

    【步骤3】从函数调用栈中可以看到应用程序启动的时候,最先执行的是_dyld_start,通过lldb指令bt + up/down可以来到入口函数_dyld_start处。

    16208025136994.jpg

    【步骤4】上图第 11 行:call就是调用函数的指令(类似bl),这个函数也就是我们App开始的地方。

    dyldbootstrap :: start

    源码如下:

    uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
                    const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
    {
    
        // Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
        dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
    
        // if kernel had to slide dyld, we need to fix up load sensitive locations
        // we have to do this before using any global variables
        rebaseDyld(dyldsMachHeader);
    
        // kernel sets up env pointer to be just past end of agv array
        const char** envp = &argv[argc+1];
        
        // kernel sets up apple pointer to be just past end of envp array
        const char** apple = envp;
        while(*apple != NULL) { ++apple; }
        ++apple;
    
        // set up random value for stack canary
        __guard_setup(apple);
    
    #if DYLD_INITIALIZER_SUPPORT
        // run all C++ initializers inside dyld
        runDyldInitializers(argc, argv, envp, apple);
    #endif
    
        _subsystem_init(apple);
    
        // now that we are done bootstrapping dyld, call dyld's main
        uintptr_t appsSlide = appsMachHeader->getSlide();
        return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
    }
    

    源码说明:

    1️⃣ 重定向dyld,在磁盘上,dyld DATA段中的所有指针都被链接在一起。它们需要被修正成真正的指针来运行。这一步必须在使用任何全局变量之前完成。

    2️⃣ 对栈溢出进行保护

    3️⃣ 初始化dyld

    4️⃣ 计算主程序的ALSR

    5️⃣ 初始化完成后调用dyldmain函数,即:dyld::_main

    注意:
    Slide这个其实就是ALSR,说白了就是通过一个随机值来实现地址空间配置随机加载

    当进程开始运行时,在存储器中所能够使用与控制的地址空间内,对进程地址进行随机分配,这样可以使某些攻击者无法事先获知地址,攻击者难以通过固定地址获取函数或者内存值进行攻击

    镜像的Slide值 = 镜像的mach_header结构体指针 - 镜像文件中第一个__TEXT代码段描述的结构体struct segmeng_command中的vmaddr数据成员的值。

    dyld::_main

    dyld::_main源码太长,这里就不完全复制了。这个函数就是加载App的主要函数。

    uintptr_t
    _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
            int argc, const char* argv[], const char* envp[], const char* apple[], 
            uintptr_t* startGlue)
    {
        //此处省略代码
    }
    

    _main函数的流程:

    1️⃣ 准备工作

    • ① 设置HostCPU等信息

    • ② 设置上下文信息setContext

    • ③ 检测进程是否受限,并在上下文中做出对应处理configureProcessRestrictions。苹果进程受AFMI保护(Apple Mobile File Integrity苹果移动文件保护)

    • ④ 配置相关环境变量,并根据环境变量的配置对上下文信息进行更新。

    2️⃣ 加载共享缓存

    • ① 在checkSharedRegionDisable函数中检查共享缓存的禁用状态。注意:iOS中是不允许禁用共享缓存

    • ② 当共享缓存未被禁用时,需要加载共享缓存mapSharedCache -> loadDyldCache。这里又分为三种情况:

      1. 仅加载到当前进程mapCachePrivate
      2. 共享缓存如果是第一次加载,则进行加载操作mapCacheSystemWide
      3. 共享缓存不是第一次被加载,则说明共享缓存已经被加载,那将不做任务处理。

    3️⃣ dyld3 加载流程
    在iOS 11后,引入了dyld13的闭包模式,以回调的方式加载,该方法加载更快、效率更高。

    在iOS 13 后,动态库和第三方库也使用闭包模式加载。

    • ① 判断当前是否是闭包模式
      sClosureMode == ClosureMode::Off:非闭包模式
      sClosureMode == ClosureMode::On:闭包模式

    • ② 检查共享缓存中是否存在主程序闭包,若存在,则直接接入第⑤步。


      16208047175618.jpg
    • ③ 当共享缓存中没有闭包,或者共享缓存中的闭包无效,则去启动缓存中查找主程序闭包,若存在,则直接进入第⑤步


      16208052099346.jpg
    • ④ 当启动缓存中也不存在主程序闭包时,则构建一个新的主程序启动闭包


      16208052628726.jpg
    • ⑤ 启动主程序闭包


      16208054298563.jpg
    • ⑥ 若主程序闭包启动失败(闭包过期等原因),则又重新构建一个新的主程序启动闭包,并再次启动它


      16208057043389.jpg
    • ⑦ 闭包启动成功,返回main函数地址

      16208057279333.jpg
    • ⑧ 闭包启动失败,则说明dyld3闭包启动不了,则尝试使用dyld2启动程序。

    4️⃣ dyld2 加载流程

    • ① 将dyldUUID添加到非共享缓存镜像UUID列表中。

      16208071201131.jpg
    • ② 实例化主程序


      16208072797717.jpg
      • 进入instantiateFromLoadedImage函数,其内部调用instantiateMainExecutable返回image对象

        16208077503600.jpg
      • 继续进入instantiateMainExecutable函数,该函数里面两个操作。

        • 调用sniffLoadCommands函数,解析Mach-O获取一些参数值。

        compressed:判断Mach-O是Compressed还是Classic类型
        segCount:Segment总数
        libCount:需要加载的动态库的数量
        codeSigCmd:代码签名信息
        encryptCmd:代码的加密信息

        注意,在函数的结果处有这么一段代码

        16208097315972.jpg
        程序的Segment总数,不能超过255
        程序的依赖库总数,不能超过4095
        • 根据compressed结果,执行相应的程序完成主程序的实例化。
          16208099317702.jpg
      • 主程序实例化完成之后,需要将image对象添加至image列表中。从此处可以看出,在image列表中第一个image对象就是主程序。

    • ③ 检测代码,检查设备、系统版本等


      16208104788085.jpg
    • ④ 判断DYLD_INSERT_LIBRARIES环境变量是否有设置值。若有,则遍历DYLD_INSERT_LIBRARIES,依次加载DYLD_INSERT_LIBRARIES变量中的动态库。加载使用loadInsertedDylib函数。

      16208108367034.jpg
    • ⑤ 链接主程序


      16208121000605.jpg
      16208121310100.jpg
      • 记录起始时间,用于记录各步骤的时间间隔
      • 递归加载主程序依赖的库.完成之后发通知
      • 修正ASLR
      • 绑定NoLazy符号
      • 绑定弱符号
      • 递归应用插入的动态库
      • 注册
      • 记录结束时间
      • 计算时间差,当项目配置环境变量,用于显示各步骤耗时
    • ⑥ 链接插入的动态库,这个操作必须在链接主程序之后,被插入的库(例如,libSystem)就不会出现在程序使用的库的前面。


      16208126576354.jpg
    • ⑦ 绑定插入的动态库


      16208130054295.jpg
    • ⑧ 绑定弱符号引用


      16208130722848.jpg
    • ⑨ 运行所有的初始化方法


      16208131066283.jpg
    • ⑩ 通知监控进程即将进入main()函数,返回main()函数地址


      16208133178443.jpg

    至此_main函数执行完成,并返回main()函数地址。

    总结

    dyld流程:

    • start函数

      • 重定位dyld
      • 调用_main函数
    • _main函数

      • 内核检查,然后一系列设置,HostCPU、可执行文件的Header、ASLR、设置上下文、配置进程是否受限(AFMI)
      • 加载共享缓存
      • 选择dyld3或dyld2
      • 实例化主程序
        • 根据compressed判断,使用相应的子类实例化主程序,返回实例对象
        • 拿到实例化后的image对象,将image对象添加到image列表中,返回image对象
        • 所以image列表中,第一个image一定是主程序
      • 加载动态库,优先插入的动态库,依次将image对象添加到image列表中,使用环境变量DYLD_INSERT_LIBRARIES
      • 链接主程序
        • 递归加载主程序依赖的库,完成之后发通知
        • 重定向,修正ASLR
        • 绑定非懒加载符号
        • 绑定弱引用符号
        • 递归应用插入的动态库
        • 注册
      • 初始化主程序,initializeMainExecutable函数
        • 调用runInitializers函数
        • 调用processInitializers函数
        • 调用recursiveInitialization函数
      • 返回主程序的入口函数,开始进入主程序的main函数
    • recursiveInitialization函数

      • 调用notifySingle函数

      • 调用doInitialization函数

    • notifySingle函数

      • 如果sNotifyObjCInit不为空,使用回调指针,执行一个回调函数

      • 通过符号断点看出,回调是_objc_init函数初始化时赋值的

        • _objc_init函数在objc源码中

        • 调用dyld中的_dyld_objc_notify_register函数,传入load_images函数

        • 调用call_class_loads函数,循环调用每个类中的load方法,动态库优先于主程序的load方法执行

    • doInitialization函数
      调用doModInitFunctions函数,内部调用全局C++对象的构造函数__attribute__((constructor))的C函数

    • dyld加载顺序:

      1. load方法
      2. C++构造函数
      3. main()函数

    相关文章

      网友评论

          本文标题:13 - dyld源码解析

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