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

推荐阅读更多精彩内容

  • dyld:动态链接器,加载所有的库和可执行文件 加载App时的函数调用栈 搭建空项目dyldDemo,在main函...
    帅驼驼阅读 429评论 0 3
  • 引言 1.一般我们都知道app的启动都是从main函数开始的,但其实在main函数之前系统做了一些其他的工作。实际...
    qinghan阅读 583评论 0 3
  • 前言 dyld全称the dynamic link editor,即动态链接器,其本质是Mach-O文件,他是专门...
    01_Jack阅读 3,717评论 2 14
  • dyld 全称是 the dynamic link editor。他是苹果的动态链接器,是苹果操作系统一个重要的组...
    正_文阅读 2,278评论 0 4
  • 我们都知道APP的入口函数是main(),而在main()函数调用之前,APP的加载过程是怎样的呢?接下来我们一起...
    忻凯同学阅读 471评论 0 2