13 - dyld源码解析

基本概念简介

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 
    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源码解析)