iOS底层探索之dyld(下):动态链接器流程源码分析
1.回顾
在上一篇博文中介绍了动态库
和静态库
的区别,对dyld
动态链接器做了初步的探索分析,本篇博文就进一步的对dyld
的源码进行分析。
2. MachO
在上篇文章中,已经找到了dyld
的入口了,但是在分析源码之前,还得补充点内容。
在iOS中
Mach-O
(可执行文件)怎么获取呢?
2.1 macOS工程查看MachO
直接编译运行之后就可以得到Mach-O
,就是下面这个黑不溜秋的东西。

2.2 iOS工程查看MachO
iOS工程的话就需要找到Products
里面的.app
文件

然后
Show in Finder
找到文件所在位置
同样这个黑不溜秋的就是
MachO
可执行文件
2.3 MachOView查看MachO结构
把这个MachO
文件,拖拽到MachOView
里面就可以查看MachO
的结构。
Header
头部,包含可以执行的CPU
架构,比如x86,arm64
Load commands
加载命令,包含文件的组织架构和在虚拟内存中的布局方式Data
,数据,包含load commands
中需要的各个段(segment
)的数据,每一个Segment
都得大小是Page
的整数倍。
3. dyld 源码分析
3.1 dyld::_main
dyld
的入口main
函数,好家伙!我直呼好家伙啊!近千行的代码!
这太长了,代码就不贴出来了,一贴出来,本篇博文基本就结束了,太TM长了😂。
- 弱水三千,我只取一瓢,
- 代码千行,我只看几行!
好诗好诗啊!哈哈😁
3.1.1 环境变量设置
从底层源码的注释也能知道,这里是
dyld
的入口
//
// Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)mainExecutableMH, 0, 0);
}
//Check and see if there are any kernel flags
dyld3::BootArgs::setFlags(hexToUInt64(_simple_getenv(apple, "dyld_flags"), nullptr));
#if __has_feature(ptrauth_calls)
// Check and see if kernel disabled JOP pointer signing (which lets us load plain arm64 binaries)
if ( const char* disableStr = _simple_getenv(apple, "ptrauth_disabled") ) {
if ( strcmp(disableStr, "1") == 0 )
sKeysDisabled = true;
}
else {
// needed until kernel passes ptrauth_disabled for arm64 main executables
if ( (mainExecutableMH->cpusubtype == CPU_SUBTYPE_ARM64_V8) || (mainExecutableMH->cpusubtype == CPU_SUBTYPE_ARM64_ALL) )
sKeysDisabled = true;
}
#endif
上面截取
main
函数部分代码, 主要是if
条件对各种环境变量设置的判断
3.1.2 平台信息设置
在所有镜像文件中设置平台
ID
,以便于调试器可以判断进程类型。
注意
:这里的image
不是图像的意思,是指镜像
3.1.3 共享缓存
检查是否开启,以及
共享缓存
是否映射到共享区域
,这都是系统级别的,系统控制
的,缓存是很宝贵的资源。
- mapSharedCache
if ( sJustBuildClosure )
sClosureMode = ClosureMode::On;
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
mapSharedCache(mainExecutableSlide);
#endif
太难了!这么上千行的代码一行一行的往下看,不说眼睛受不了,人都要疯了!(PS:痛苦)
那么博主,你有什么好的探索方式吗?
哎,巧了!靓仔!还真有哦!
反推,直接看最后一行代码。我们从结果反推,看看都是什么条件导致的最后return
。
通过搜索在
main
函数里面定位result
,发现和sMainExecutable
有关系。
CRSetCrashLogMessage(sLoadingCrashMessage);
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
再搜索看看
sMainExecutable
是什么关键的东西。
貌似找
sMainExecutable
是找对了,在推导的时候,我们要明确我们的目标是要找什么?我们现在不就是要找images
镜像嘛!所以就应该对link
、bind
、load
这些词要敏感。
从上图中代码中的一些关键代码sInsertedDylibCount
/weakBind
/linkingMainExecutable
这些也可以验证,我们找sMainExecutable
是找对了。
3.1.4 link 链接
从代码中也可以发现,sMainExecutable
是link
链接的一个参数
// load any inserted libraries 加载任何插入的库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// record count of inserted libraries so that a flat search will look at
// inserted libraries, then main, then others.
sInsertedDylibCount = sAllImages.size()-1;
// link main executable 链接main可执行文件
gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
if ( mainExcutableAlreadyRebased ) {
// previous link() on main executable has already adjusted its internal pointers for ASLR
// work around that by rebasing by inverse amount
sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
}
#endif
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
- 遍历
DYLD_INSERT_LIBRARIES
环境变量,调用loadInsertedDylib
加载动态库。 - 链接主程序
3.1.5 主程序main的入口
#if SUPPORT_OLD_CRT_INITIALIZATION
// Old way is to run initializers via a callback from crt1.o
if ( ! gRunInitializersOldWay )
initializeMainExecutable();
#else
// run all initializers
initializeMainExecutable();
#endif
// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();
if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
}
ARIADNEDBG_CODE(220, 1);
#if TARGET_OS_OSX
if ( gLinkContext.driverKit ) {
result = (uintptr_t)sEntryOverride;
if ( result == 0 )
halt("no entry point registered");
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
}
else
#endif
{
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();//找到主可执行文件的入口点
if ( result != 0 ) {
// main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
// main 可执行文件使用 LC_MAIN,我们需要使用 libdyld 中的 helper 来调用 main()
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
*startGlue = 0;
}
}
}
- 执行初始化方法
initializeMainExecutable
-
notifyMonitoringDyldMain
通知任何监控进程,此进程即将进入main()
- 通过
if
判断result
寻找主程序的入口点 -
result != 0
时:使用LC_MAIN
,我们需要使用libdyld
中的helper
来调用main()
-
result == 0
: 使用LC_UNIXTHREAD
,dyld
需要让程序中的“start”
为main()
设置
3.2 initializeMainExecutable
3.2.1 runInitializers
拿到镜像文件的个数,循环开始对镜像进行初始化
// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
初始化进行前期的相关准备
runInitializers -> processInitializers
3.2.2 processInitializers
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
for (uintptr_t i=0; i < images.count; ++i) {
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
通过
recursiveInitialization
递归初始化,下面是核心代码部分
3.2.3 recursiveInitialization
// let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// initialize this image
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);
3.2.4 notifySingle
由
recursiveInitialization
可以找到notifySingle
- notifySingle重点代码
路径加载和镜像文件加载是重点
全局搜索
sNotifyObjCInit
,发现是_dyld_objc_notify_init
类型
3.2.5 objc_init和dyld的联系
在
registerObjCNotifiers
方法的第二个参数赋值给了sNotifyObjCInit
全局搜索
registerObjCNotifiers
看看哪里调用了
发现,在dyldAPIs.cpp
文件中找到了registerObjCNotifiers
调用
// _dyld_objc_notify_register
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
这个
_dyld_objc_notify_register
看着好眼熟啊!似曾相识燕归来啊!
这不就是libobjc.dylib
源码里面_objc_init
方法调用了啊!如下图:
在调用
_dyld_objc_notify_register函数时
,传入了三个参数(map_images
的函数地址 、load_images
函数unmap_image
函数)
那么回到dyld
源码的工程,registerObjCNotifiers
里面是这样的
sNotifyObjCMapped = mapped = &map_images
sNotifyObjCInit = init = load_images
sNotifyObjCUnmapped = unmapped = unmap_image
到此也就发现,objc_init
和dyld
关联起来了
objc_init()
向dyld
中注册了三个函数,在dyld
进行动静态库加载过程时,当特定环境满足的条件下,这三个函数会调用执行。
之前一直在
dyld
的源码里面,现在我们回到_objc_init
里面在进行正向的猜测探索。
在_objc_init
函数里面打上断点,查看堆栈信息,发现_oc_object_init
是在libdispatch
的源码里面。
那么现在就去
libdispatch
的源码里面看看
libdispatch
的源码里面发现了_oc_object_init
的调用是在libdispatch_init
,那么libdispatch_init
是由谁来发起的呢?
从堆栈信息发现,是libSystem_initializer
那么现在又得去
LibSystem
源码里面看看,发现libdispatch_init
确实调用了
那么
libSystem_initializer
又是谁来发起的呢?
通过堆栈
信息可以发现是doModInitFunctions
。
doModInitFunctions
又回到了dyld
了,这就是反向推导到正向的验证过程。
doModInitFunctions
在 ImageLoaderMachO::doInitialization
里面被调用了,如下代码:
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
ImageLoader::recursiveInitialization
里面又调用了doInitialization
,递归初始化镜像文件
// let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// initialize this image
bool hasInitializers = this->doInitialization(context);
总结一下:objc_init()
调用流程如下:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)
_dyld_objc_notify_register
里面的方法是在什么时候调用的呢?
3.3 map_images和load_images
3.3.1 map_images
回到
libObjc.dylib
也就是objc
的源码工程,在map_images
和load_images
的方法处分别打上断点,发现是先走到了map_images
处,再控制台bt
打印堆栈
信息
map_images
方法首先执行的,再查看运行堆栈,其流程为:_dyld_objc_notify_register --> registerObjCNotifiers --> notifyBatchPartial --> map_images
,
从dyld
源码中也可以验证,如下:
3.3.2 load_images
点击继续运行到
load_images
断点处,再打印堆栈信息
由此可以知道
load_images
调用时机:
_dyld_objc_notify_register --> registerObjCNotifiers --> load_images
3.4 main调用时机探索
在上一篇博文iOS底层探索之dyld(上)中,我们有一个测试案例,执行顺序是+ load --> c++ --> main函数
那么就去源码里面探索下,到底是怎么走到main
函数的。
3.4.1 load方法调用过程
load_images(const char *path __unused, const struct mach_header *mh)
{
....省略代码....
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
这里对所有类的load
方法prepare
,那么进入prepare_load_methods
方法,主要是对所有镜像的懒加载的类、分类prepare
,说白了就是找load methods
。
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
schedule_class_load
为 类 和任何未加载的superclasses
类递归调度 +load
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->getSuperclass());
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
add_class_to_loadable_list
把所有的+load
收集到一起,先是类,然后再是分类的。
/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
递归获取方法收集在
loadable_classes[loadable_classes_used].method
,是从通过getLoadMethod
方法获取的。
/***********************************************************************
* objc_class::getLoadMethod
* fixme
* Called only from add_class_to_loadable_list.
* Locking: runtimeLock must be read- or write-locked by the caller.
**********************************************************************/
IMP
objc_class::getLoadMethod()
{
runtimeLock.assertLocked();
const method_list_t *mlist;
ASSERT(isRealized());
ASSERT(ISA()->isRealized());
ASSERT(!isMetaClass());
ASSERT(ISA()->isMetaClass());
mlist = ISA()->data()->ro()->baseMethods();
if (mlist) {
for (const auto& meth : *mlist) {
const char *name = sel_cname(meth.name());
if (0 == strcmp(name, "load")) {
return meth.imp(false);
}
}
}
return nil;
}
递归所有的
baseMethods()
,通过strcmp
匹配"load"
,这就是load
方法的调用过程
3.4.2 C++函数调用时机
在
C++
函数开始处打上断点,然后再bt
打印调用堆栈信息查看
从堆栈信息可知是按
doInitialization -->doModInitFunctions --> C++(JPFunc)
调用顺序,然后我们回到dyld源码
搜索doInitialization
在上面已经验证过了
notifySingle
的里面的流程是load_images
方法的调用最后再到load
,所以上图也验证了load
方法是在C++
之前的调用的。
那么再根据堆栈的调用进入到
doModInitFunctions
方法里面。
这个
doModInitFunctions
里面就是对于macho_header
的处理,包括Load commands
和macho_segment_command
,for
循环遍历macho_section
里面的函数指针,也就是包括了C++
方法。
3.4.3 dyld如何到main.m函数
C++
函数后,dyldbootstrap::start
后,会寻找main
函数。从堆栈可以知道是从dyld
的_dyld_start
开始
_dyld_start
是汇编写的,从汇编可以知道main
函数存在rax
寄存器里面,最后会jmp
跳转到rax
执行main
函数。
那么现在去工程代码里面验证一下。
通过代码的
汇编跟踪调试
,也可以发现是和dyld
源码里面的汇编执行是一模模一样样。
通过
register read
打印寄存器信息也可以发现,rax
里面存的就是main
,最后通过register read rax
读取rax
验证了rax = main
函数。这就是dyld
到main
的过程。
这就是dyld
的探索分析过程,试问还有谁能,把这么复杂的东西,分析的这么有条有理,我这该死的无处安放的魅力啊!哈哈😁
4. 总结
-
dyld
分析反推到正向验证 -
notifySingle
单个通知注入 -
_dyld_objc_notify_register
注册回调函数,下句柄,类似block
,dyld
里面实现了map_images
、load_images
、unmap_image
之后回调给_objc_init
就可以开始正常调用了。 -
libSystem
库是最先开始初始化调用的,在ImageLoaderMachO::doModInitFunctions
和ImageLoaderMachO::doImageInit
里面可以验证,因为objc
的相关操作依赖系统,dyld
也等系统相关库初始化完成,才对镜像进行初始化、映射等操作。
doModInitFunctions
-
main 、+ load 、C++
执行顺序是+ load --> c++ --> main函数
-
最后奉上
dyld
流程分析图
更多内容持续更新
🌹 喜欢就点个赞吧👍🌹
🌹 觉得学习到了的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹