dyld加载流程图
建议大家在阅读文章的时候,结合流程图阅读。这样方便理解这个流程,可以将图片下载到本地,一边阅读一边比对。
1、dyld
1.1 简介
dyld(The dynamic link editor)
--- Apple的动态链接器。
是苹果操作系统一个重要的组成部分,在应用被编译打包成Mach-O
文件之后,交由dyld
负责链接,加载程序。在MacOS系统中,其在/usr/lib/dyld
目录下。
1.2 共享缓存
在日常开发的过程中,我们会用到很多的系统库,比如:UIKit
、Foundation
等等;这些系统库都是dyld
帮我们加载到内存中的。但是不同的APP会用到相同的系统库,如果每一个APP运行的时候,dyld
都去加载一遍,那岂不是对内存极大的浪费。于是,为了节省空间,Apple将这些系统库统一的放在了一个地方:动态库共享区(dyld shared cache)
2、dyld加载流程
- 2.1 Demo准备
在探索dyld
加载流程之前,我们先做好准备工作。
首先请大家思考一个问题:我们APP的启动,是先执行main
函数吗?在main
函数之前,还有其他的操作吗?
这里我们创建一个Demo,一起来探索一下。我们在ViewController
中重写load
方法,然后在main.m
中添加一个C++
方法,然后观察一下它们的执行顺序:
我们会发现,作为APP入口的main
函数并不是第一个被执行的函数。这又是为什么呢?
接下来我们就一步一步的探索。 - 2.2 APP启动流程探索
通过上面,我们知道,load
方法是最先被执行的,那么我们就在load
处打一个断点,来看看一下load
之前还有没有什么操作。
通过bt
指令,在控制台打印堆栈信息:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 2.1
* frame #0: 0x0000000104ea9d8c test`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
frame #1: 0x00000001a922c25c libobjc.A.dylib` + 944
frame #2: 0x0000000104ef621c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 464
frame #3: 0x0000000104f075e8 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512
frame #4: 0x0000000104f05878 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
frame #5: 0x0000000104f05940 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
frame #6: 0x0000000104ef66d8 dyld`dyld::initializeMainExecutable() + 216
frame #7: 0x0000000104efb928 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 5216
frame #8: 0x0000000104ef5208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
frame #9: 0x0000000104ef5038 dyld`_dyld_start + 56
可以看到在[ViewController load]
还有很多dyld
的方法被执行。最早的一个是_dyld_start
。
这个时候就需要我们去下载dyld源码
去分析一下了。源码地址
本次使用的是dyld-832.7.3
- 2.2.1 _dyld_start
我们在源码中搜索_dyld_start
,会发现是由汇编实现的,我们找到arm64架构
对应的代码如下:
看到这里大家可能会懵,这些汇编代码都是干什么用的?
其实我们没必要每一句代码都弄清楚,我们只需要找到关键信息,理解其意图就可以了。首先我们会看到下面这一段代码以及注释:
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
通过注释可以看到,这段代码进入的是dyldbootstrap::start
这个函数;这里请注意,有没有感觉这个函数名有点熟悉。没错,我们在上面打印堆栈信息的时候,_dyld_start
紧跟着的就是dyldbootstrap::start
。
- 2.2.2 dyldbootstrap::start
dyldbootstrap::start
是指dyldbootstrap
这个命名空间作用域内的start
函数。
cmd
+Shift
+o
搜索dyldbootstrap
:
在作用域内搜索start
函数:
可以看到start
函数的返回值是通过dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue)
获得的;这是dyld
的main
函数。 - 2.2.3 dyld::main()
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
函数的代码过长,这里就不展示出来了,下面我们把关键的部分讲清楚就可以了。
- 2.2.3-1 配置相关环境变量
1️⃣ :获取相关环境信息
如上面的:
getHostInfo(mainExecutableMH, mainExecutableSlide);
就是获取当前运行环境的架构信息。
sMainExecutableMachHeader = mainExecutableMH;
设置MachHeader
(这个我就不多做解释,不了解的同学可以看这里3、iOS强化 --- Mach-O 文件).
sMainExecutableSlide = mainExecutableSlide;
设置slide
;这个slide
就是ASLR计算出来的一个随机值,保证程序每一次运行的偏移值都不一样,防止黑客通过固定地址进行恶意攻击。
2️⃣ :设置上下文信息,检测进程是否受限
i
、 调用setContext
函数,传入MachHeader
以及一些参数设置上下文。
ii
、 configureProcessRestrictions
检测进程是否受限,在上下文中做出对应的处理。
// _main函数
setContext(mainExecutableMH, argc, argv, envp, apple);
......
configureProcessRestrictions(mainExecutableMH, envp);
3️⃣ :配置环境变量
// 检查设置的环境变量
checkEnvironmentVariables(envp);
// 如果DYLD_FALLBACK为nil,将其设置为默认值
defaultUninitializedFallbackPaths(envp);
- 2.2.3-2 共享缓存
1️⃣ :检查是否开启了共享缓存 checkSharedRegionDisable
(iOS下不会被禁用)。
2️⃣ :加载共享缓存库 mapSharedCache ---> loadDyldCache
;加载共享缓存有几种情况:
i
:仅加载到当前进程 mapCachePrivate
,模拟器仅支持加载到当前进程。
ii
:共享缓存是第一次被加载,就去做加载操作 mapCacheSystemWide
。
iii
:共享缓存不是第一次被加载,那么就不做任何处理。
- 2.2.3-3 实例化主程序
// The kernel maps in main executable before dyld gets control. We need to
// make an ImageLoader* for the already mapped in main executable.
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
// throw "main executable not a known format";
}
isCompatibleMachO((const uint8_t*)mh, path)
--- 通过macho_header
里面的magic
、cputype
、cpusubtype
去检测是否兼容。
当检测通过之后,执行
instantiateMainExecutable
(实例化image
),接着将image
添加到镜像列表中(addImage(image)
)。
instantiateMainExecutable
中,使用sniffLoadCommands
来实例化主程序;下面我们来简单了解一下这个函数:
void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
const linkedit_data_command** codeSigCmd,
const encryption_info_command** encryptCmd)
{
*compressed = false;
*segCount = 0;
*libCount = 0;
*codeSigCmd = NULL;
*encryptCmd = NULL;
......
......
......
// fSegmentsArrayCount is only 8-bits
if ( *segCount > 255 )
dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);
// fSegmentsArrayCount is only 8-bits
if ( *libCount > 4095 )
dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);
if ( needsAddedLibSystemDepency(*libCount, mh) )
*libCount = 1;
// dylibs that use LC_DYLD_CHAINED_FIXUPS have that load command removed when put in the dyld cache
if ( !*compressed && (mh->flags & MH_DYLIB_IN_CACHE) )
*compressed = true;
}
○ compressed
--- 根据LC_DYLD_INFO_ONLY
来决定。
○ segCount
--- MachO文件中segment
的数量,最大不能超过 255
个。
○ libCount
--- 依赖库数量,最大不能超过 4095
个。
○ codeSigCmd
--- 应用签名。
○ encryptCmd
--- 应用加密信息
- 2.2.3-4 加载插入动态库
○ 首先利用DYLD_INSERT_LIBRARIES
环境变量来判断,是否需要插入动态库。
○ 然后调用loadInsertedDylib
加载插入动态库。
○ 最后记录插入动态库的数量,sInsertedDylibCount = sAllImages.size()-1;
。
插入动态库
这整个是一个名字,这样的机制给我逆向的时候的代码注入提供了可能。 - 2.2.3-5 链接主程序
- 2.2.3-6 链接动态库
- 2.2.3-7 符号绑定
○sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
递归绑定符号表
○sMainExecutable->weakBind(gLinkContext);
弱符号绑定 - 2.2.3-8 执行初始化方法
我们在上面打印的函数调用栈里面,main
之后就是:
frame #6: 0x0000000104ef66d8 dyld`dyld::initializeMainExecutable() + 216
我们进入initializeMainExecutable
内部看一下:
我们发现
initializeMainExecutable
内部有一个循环遍历,每次都会执行runInitializers
。
cmd
+ shift
+ o
搜索runInitializers
,跟进去:
发现
runInitializers
中又调用processInitializers
为初始化做准备。那么我们跟进processInitializers
:
注意看
processInitializers
里面的for循环
,recursiveInitialization
递归初始化镜像。
同样的搜索recursiveInitialization
,跟进去:
我们会发现,在
recursiveInitialization
中关键的两步是:
i
: 初始化镜像 --- doInitialization
ii
:镜像初始化完成后,发送广播通知 --- notifySingle
notifySingle
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
//dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());
std::vector* handlers = stateToHandlers(state, sSingleHandlers);
if ( handlers != NULL ) {
dyld_image_info info;
info.imageLoadAddress = image->machHeader();
info.imageFilePath = image->getRealPath();
info.imageFileModDate = image->lastModified();
for (std::vector::iterator it = handlers->begin(); it != handlers->end(); ++it) {
const char* result = (*it)(state, 1, &info);
if ( (result != NULL) && (state == dyld_image_state_mapped) ) {
//fprintf(stderr, " image rejected by handler=%p\n", *it);
// make copy of thrown string so that later catch clauses can free it
const char* str = strdup(result);
throw str;
}
}
}
if ( state == dyld_image_state_mapped ) { // 是否被映射
// Save load addr + UUID for images from outside the shared cache
// Include UUIDs for shared cache dylibs in all image info when using private mapped shared caches
if (!image->inSharedCache()
|| (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion)) {
dyld_uuid_info info;
if ( image->getUUID(info.imageUUID) ) {
info.imageLoadAddress = image->machHeader();
addNonSharedCacheImageUUID(info);
}
}
}
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
uint64_t t0 = mach_absolute_time();
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
uint64_t t1 = mach_absolute_time();
uint64_t t2 = mach_absolute_time();
uint64_t timeInObjC = t1-t0;
uint64_t emptyTime = (t2-t1)*100;
if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
timingInfo->addTime(image->getShortName(), timeInObjC);
}
}
// mach message csdlc about dynamically unloaded images
if ( image->addFuncNotified() && (state == dyld_image_state_terminated) ) {
notifyKernel(*image, false);
const struct mach_header* loadAddress[] = { image->machHeader() };
const char* loadPath[] = { image->getPath() };
notifyMonitoringDyld(true, 1, loadAddress, loadPath);
}
}
这个函数中,重点代码是:
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
全局搜索sNotifyObjCInit
,并没有发现其函数实现,但是我们找到了它的赋值操作:
这样我们继续跟进,查找
registerObjCNotifiers
在哪里被调用了:
⚠️⚠️⚠️ :这个时候,我们发现在函数
_dyld_objc_notify_register
中,registerObjCNotifiers
被调用。_dyld_objc_notify_register
的函数调用,我们dyld
源码中并没有找到,这个时候,我们需要去libobjc
源码中去寻找。(本次使用的是objc4-818.2
)
- 在
objc4-818.2
中我们找到了这个一段代码
我们仔细的比对会发现,_dyld_objc_notify_register
传入的第二个参数是load_images
,那么也就是说给sNotifyObjCInit
赋的值就是load_images
;而load_images
会调用所有的+load
方法。因此:notifySingle
是一个回调函数
。 - load_images
接着跟进call_load_methods
:
我们会发现call_load_methods
的核心是通过do-while
循环调用call_class_loads()
。那我们就继续跟进:
这里就可以很清晰的看到,最终循环调用了所有的+load
方法。
这个时候我们来梳理一下
load
的函数调用栈:_dyld_start
-->dyldbootstrap::start
-->dyld::_main
-->dyld::initializeMainExecutable
-->ImageLoader::runInitializers
-->ImageLoader::processInitializers
-->ImageLoader::recursiveInitialization
-->dyld::notifySingle
-->sNotifyObjCInit
-->load_images
_dyld_objc_notify_register
_objc_init
_objc_init
我们继续往下探索
doInitialization
我们会发现,
doInitialization
主要分成两部分:
i
:doImageInit
ii
:doModInitFunctions
-
doImageInit
进入doImageInit
源码发现,其核心是一个for循环
,注意看代码的注释,libSystem
的初始化必须先执行。 -
doModInitFunctions
doModInitFunctions
这个方法内部会调用全局C++
对象的构造函数__attribute__((constructor))
的C函数
。
这个我们可以通过测试Demo的函数调用栈来验证一下:
探索到这里,我们仍然没有发现_objc_init
被调用的相关信息。这个时候,我们就需要下一个符号断点,来查看_objc_init
对应的堆栈信息了。
符号断点埋下之后,运行程序,
bt
打印堆栈信息:
大家观察此时的函数调用栈,是不是清晰了很多。
我们先来捋一下
_objc_init
的函数调用栈(此时用的是模拟器,整理函数调用栈的时候,省略了模拟器相关的一些函数调用):_dyld_start
-->dyldbootstrap::start
-->dyld::_main
-->dyld::initializeMainExecutable
-->ImageLoader::runInitializers
-->ImageLoader::processInitializers
-->ImageLoader::recursiveInitialization
-->ImageLoaderMachO::doInitialization
-->ImageLoaderMachO::doModInitFunctions
-->libSystem_initializer(libSystem.B.dylib)
-->libdispatch_init(libdispatch.dylib)
-->_os_object_init(libdispatch.dylib)
-->_objc_init(libobjc.A.dylib)
这里我们一起来回忆一下,在初始化
_objc_init
的时候,调用_dyld_objc_notify_register
,load_images
被注册;在我们分析notifySingle
的时候,发现了重要的函数sNotifyObjCInit
,并且我们之后还发现sNotifyObjCInit = init = load_images
。这样就形成了一个闭环。
- 2.2.3-9 寻找主程序入口
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
探索了那么多,我们来串一下dyld::main
的调用流程:
- 参考文档
iOS-底层原理 15:dyld加载流程
iOS探索 浅尝辄止dyld加载流程
iOS 底层 - 从头梳理 dyld 加载流程