iOS之武功秘籍⑦: dyld加载流程 -- 应用程序的加载

2021-02-23  本文已影响0人  長茳

iOS之武功秘籍 文章汇总

写在前面

我们平时编写的程序的入口函数都是main.m文件里面的main函数,但是这就是App的生命起点了吗?玩过逆向的iOSer都知道可以往+load方法注入代码来进行安全攻防,而+load方法先于main函数执行,那么main函数之前都发生了哪些有趣的事呢?本文就将带着大家来揭开这片神秘面纱!

本节可能用到的秘籍Demo

一、编译过程与动静态库

我们先来看个🌰:

为什么是这么一个顺序?按照常规的思维理解,main不是入口函数吗?为什么不是main最先执行?
下面根据这个问题,我们来探索在走到main函数之前,到底还做了什么.

在探索分析app启动之前,我们需要先了解iOS中App代码的编译过程以及动态库静态库.

① 编译过程

在日常开发过程中,开发者会使用成千上万次的Command + B/R进行开发调试,但可能很少有人关注过这个过程中 Xcode帮我们做了哪些事情(iOS开发者往往会吐槽Xcode越来越难用了,但不得不承认它越来越强了)

事实上,这个过程分解为4个步骤,分别是预处理(Prepressing)、编译(Compilation)、汇编(Assembly)和链接(Linking).------ 摘自《程序员的自我修养-- 链接、装载与库》

在以上4个步骤中,IDE主要做了以下几件事:

FoundationUIKit这种可以共享代码、实现代码的复用统称为——它是可执行代码的二进制文件,可以被操作系统写入内存,它又分为静态库动态库

② 静态库

静态库是指链接时完整的拷贝到可执行文件,多次使用多次拷贝,造成冗余,使包变的更大

.a.lib都是静态库

③ 动态库

动态库是指链接时不复制,程序运行时由系统加在到内存中,供系统调用,系统只需加载一次,多次使用,共用节省内存.

.dylib.framework都是动态库

二、dyld

① dyld简介

dyld(The dynamic link editor)是苹果的动态链接器,负责程序的链接及加载工作,是苹果操作系统的重要组成部分,存在于MacOS系统的(/usr/lib/dyld)目录下.在应用被编译打包成可执行文件格式的Mach-O文件之后 ,交由dyld负责链接,加载程序.

所以 App的启动流程图如下

② dyld_shared_cache

由于不止一个程序需要使用UIKit系统动态库,所以不可能在每个程序加载时都去加载所有的系统动态库.为了优化程序启动速度和利用动态库缓存,苹果从iOS3.1之后,将所有系统库(私有与公有)编译成一个大的缓存文件,这就是dyld_shared_cache,该缓存文件存在iOS系统下的/System/Library/Caches/com.apple.dyld/目录下

三、dyld加载流程

在前文的Demo中,在load方法main方法处加一个断点

点击函数调用栈/使用LLDB——bt指令打印,都能看到最初的起点_dyld_start

接下来怎么去研究dyld呢,我们将通过dyld源码展开分析

① 1._dyld_start

在源码中全局搜索_dyld_start,会发现它是由汇编实现的

arm64中,_dyld_start调用了一个看不懂的方法

从注释中得出可能是dyldbootstrap::start方法(其实在“函数调用栈”那张图中汇编代码已经把这个方法暴露出来了)

② dyldbootstrap::start

其实dyldbootstrap::start是指dyldbootstrap这个命名空间作用域里的 start函数

源码中搜索dyldbootstrap找到命名作用空间.

再在这个文件中查找start方法,其核心是返回值调用了dyldmain函数,其中macho_headerMach-O的头部,而dyld加载的文件就是Mach-O类型的,即Mach-O类型可执行文件类型,由四部分组成:Mach-O头部Load CommandsectionOther Data,可以通过MachOView查看可执行文件信息

start()函数中主要做了一下几件事:

③ dyld::_main()

dyld::_main()主要流程为:

③.1 环境变量配置

只要设置了这两个环境变量参数,在App启动时就会打印相关参数、环境变量信息(自行尝试研究)

③.2 共享缓存

③.3 主程序的初始化

③.4 插入动态库

遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载,通过该环境变量我们可以注入自定义的一些动态库代码从而完成安全攻防,loadInsertedDylib内部会从DYLD_ROOT_PATHLD_LIBRARY_PATHDYLD_FRAMEWORK_PATH等路径查找dylib并且检查代码签名,无效则直接抛出异常

③.5 link 主程序

③.6 link 动态库

③.7 弱符号绑定

③.8 执行初始化方法

先回顾一下函数调用栈

在这里,需要分成两部分探索,一部分是notifySingle函数,一部分是doInitialization函数,首先探索notifySingle函数

注意:_dyld_objc_notify_register的函数需要在libobjc源码中搜索

都到这了,那就顺便看看load函数的加载吧
下面我们进入load_images的源码看看其实现,以此来证明load_images中调用了所有的load函数

所以,load_images调用了所有的load函数,以上的源码分析过程正好对应堆栈的打印信息

那么问题又来了,_objc_init是什么时候调用的呢?请接着往下看

这里也需要分成两部分,一部分是doImageInit函数,一部分是doModInitFunctions函数

可以通过测试程序的堆栈信息来验证,在C++方法出加一个断点

走到这里,还是没有找到_objc_init的调用?怎么办呢?放弃吗?当然不行,我们还可以通过_objc_init加一个符号断点来查看调用_objc_init前的堆栈信息

结合上面的分析,从初始化_objc_init注册的_dyld_objc_notify_register的参数2,即load_images,到sNotifySingle --> sNotifyObjCInie=参数2sNotifyObjcInit()调用,形成了一个闭环

也可以简单的理解为sNotifySingle这里是添加通知即addObserver_objc_init中调用_dyld_objc_notify_register相当于发送通知,即push,而sNotifyObjcInit相当于通知的处理函数,即selector.

③.9 寻找主程序入口

最后注意:main是写定的函数,写入内存,读取到dyld,如果修改了main函数的名称,会报错

所以,综上所述,最终dyld加载流程,如下图所示,图中也诠释了前文中的问题:为什么是load-->Cxx-->main的调用顺序

写在后面

和谐学习,不急不躁.我还是我,颜色不一样的烟火.

上一篇 下一篇

猜你喜欢

热点阅读