这是我参与更文挑战的第1天,活动详情查看: 更文挑战
应用程序的加载
前边文章中我们介绍了很多底层的源码,那么在应用程序加载的过程中,这些代码是怎么写到内存中的呢?我们工程中的动态库和静态库是如何加载到内存中的?这一篇文章我们来分析一下应用程序的加载原理!
动态库和静态库
我们的应用程序在加载的过程用会依赖很多底层的基础库,比如UIKit
,CoreFoundation
等一系列库,这些库其实是一些可执行的二进制文件
,能够被操作系统加载到内存
里面;库分类两种:动态库
和静态库
;
动态库
和静态库
的区别就在于库的链接
;
在理解链接
之前,我们先要了解一下编译的过程:
编译的流程
源文件
会经过一个预编译
过程,进行词法树
和语法树
的分析;- 之后交给
编译器
进行编译处理; 编译
之后会形成一些汇编
文件;汇编
文件会进行链接
装载;- 最终变成
可执行文件
;
动态链接和静态链接
既然动态库
和静态库
的区别在于链接
,那么他们的链接过程有什么不同呢?我们来看下边的图片:
我们可以发现:静态库在装载的时候会一个一个进行装载,最终会有多个库重复装载的问题存在,会浪费性能和空间;而动态库在装载的时候会被多个需要装载的地方共享,对内存和空间都有很大优化;
那么这些库是如何加载到内存中的呢?这就需要用到链接器
,也就是我们常说的dyld
;
应用程序的加载流程
dyld
的引出
那么什么是dyld
呢?如何找到dyld
呢?我们新建一个工程,然后在main
函数中打上断点运行:
发现,在main
函数执行前,还有一个start
被执行了,那么这个start
是什么呢?
start
来自于libdyld.dylib
,这个libdyld.dylib
就是dyld
;它是一个动态链接器
,用来链接库
那么我们如何研究dyld
呢,start
符号断点无法被拦截,但是ViewController
的load
方法在main
函数之前调用了(load方法在main函数之前调用),我们以此为切入点,在load
方法中断点执行:
最开始执行的是dyld
的_dyld_start
,接下来就需要借助dyld
的源码进行分析了dyld源码下载地址
dyld
的流程
我们现在所用的是dyld3
dyld_start
在dyld
的源码中搜索_dyld_start
,找到_dyld_start
被调用的地方
dyldbootstrap::start
dyldStartup.s
是dyld
的起始文件,是一个汇编文件,我们以arm
架构来分析,虽然汇编看不太懂,但是根据注释,可以清楚_dyld_start
中调用了dyldbootstrap::start
,也就是调用了命名空间dyldbootstrap
下的start
方法:
dyld::_main
在start
方法的最后,调用了dyld::_main
方法
由于_main
方法太过庞大,其中包含了配置环境
、平台
、版本
、路径
、主机
等一系列信息来为后边的操作做准备条件,我们主要看一下其中的几个核心的地方:
getHostInfo(mainExecutableMH, mainExecutableSlide); // 对主机信息的处理 架构等
setContext(mainExecutableMH, argc, argv, envp, apple); // 设置dyld上下文信息
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide); // 确定当前是否有共享缓存
mapSharedCache(mainExecutableSlide); // 加载共享缓存
reloadAllImages: // 加载镜像文件
_main
方法最终返回了一个result
,而根据源码分析发现result
跟sMainExecutable
息息相关,那么sMainExecutable
是在哪里创建的呢?
很明显reloadAllImages
这个方法体是我们需要研究的核心方法;接下来我们分析一下,系统在reloadAllImage
方法中都做了什么操作?
reloadAllImage
instantiateFromLoadedImage 初始化主程序
镜像文件加载器:从
MachO
文件中,按照格式加载镜像文件,初始化主程序
loadInsertedDylib 加载动态库
加载插入的
动态库
link 主程序和动态库
链接
主程序
以及链接插入的动态库
weakBind 弱引用绑定
所有的镜像文件链接完毕后进行弱引用绑定
initializeMainExecutable 运行主程序
运行所有初始化主程序
notifyMonitoringDyldMain 通知进入main
主程序运行之后,通知
dyld
可以进入main
函数
initializeMainExecutable
前面我们大致分析了一下dyld
的大致流程,接下来,我们分析一下主程序运行过程中的一些细节问题
initializeMainExecutable
方法中只有一个核心方法runInitializers
,它就是我们主要研究的内容:
runInitializers 镜像文件初始化
在主程序
运行一开始,就根据镜像文件的个数allImagesCount()
初始化每个镜像文件
在每一个镜像文件初始化的过程中,都会执行processInitializers
和notifyBatch
方法;
processInitializers 准备工作
在递归调用的过程中,recursiveInitialization
进行初始化工作
recursiveInitialization 初始化
uninitUps.imagesAndPaths
拿到镜像文件路径;dependentImage->recursiveInitialization
递归初始化依赖文件
在初始化镜像文件
之前,我们需要先通知注入一些依赖文件(比如C++文件),然后才能开始初始化镜像文件
,镜像文件初始化完成之后,通知程序初始化完成;
这也就是
objc
中的C++的构造函数
比我们自己写的构造函数
更早执行的原因
notifySingle
搜索发现,此方法在这里被赋值
最终调用的是:
此方法核心内容是给sNotifyObjCInit
进行赋值,那么sNotifyObjCInit
是在什么时候调用的呢?
在registerObjCNotifiers
方法被赋值,那么registerObjCNotifiers
方法什么时候被调用了呢?
registerObjCNotifiers
是被_dyld_objc_notify_register
调用的,我们继续向上查找发现无法找到其被调用的地方,那么接下来我们通过符号断点
的方式看一下
我们发现是_objc_init
调起的_dyld_objc_notify_register
,那么_objc_init
在哪里呢?
结果发现,_objc_init
在objc
的源码中被调用,那么我们在源码中搜索查看:
发现,在_objc_init
中确实调用了_dyld_objc_notify_register
,objc
的源码和dyld
的源码是有关联的;
_objc_init
那么,在这个地方就会引出一个问题,是谁调用了_objc_init
呢?我们断点运行objc
的源码查看堆栈信息:
根据堆栈信息显示_objc_init
是由_os_object_init
来调起的,那么_os_object_init
来自什么地方呢?
_os_object_init
来源于libdispatch.dylib
,接下来下载libdispatch
的源码分析libdispatch源码下载地址
在libdispatch
源码中的_os_object_init
方法里确实调用了_objc_init
方法,而且搜索发现,此方法确实来源于objc
的源码
那么_os_object_init
又是谁调起的呢?根据堆栈信息:
_os_object_init
是由libdispatch.dylib
的libdispatch_init
这个方法调用了,我们在源码中验证一下:
根据堆栈信息libdispatch_init
是由libSystem.B.dylib
的libSystem_initializer
方法调用的
接下来,下载libSystem
的源码验证一下:
在libSystem.B.dylib
的libSystem_initializer
方法中调用了libdispatch_init
方法;
那么继续分析,libSystem_initializer
的调用者是谁?
调用者为dyld
的doModInitFunctions
方法,重新又回到了dyld
的源码中;
doModInitFunctions
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
if ( fHasInitializers ) {
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = §ionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
Initializer* inits = (Initializer*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);
// <rdar://problem/23929217> Ensure __mod_init_func section is within segment
if ( (sect->addr < seg->vmaddr) || (sect->addr+sect->size > seg->vmaddr+seg->vmsize) || (sect->addr+sect->size < sect->addr) )
dyld::throwf("__mod_init_funcs section has malformed address range for %s\n", this->getPath());
for (size_t j=0; j < count; ++j) {
Initializer func = inits[j];
// <rdar://problem/8543820&9228031> verify initializers are in image
if ( ! this->containsAddress(stripPointer((void*)func)) ) {
dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
}
if ( ! dyld::gProcessInfo->libSystemInitialized ) {
// <rdar://problem/17973316> libSystem initializer must run first
const char* installPath = getInstallPath();
if ( (installPath == NULL) || (strcmp(installPath, libSystemPath(context)) != 0) )
dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
}
if ( context.verboseInit )
dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
bool haveLibSystemHelpersBefore = (dyld::gLibSystemHelpers != NULL);
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
bool haveLibSystemHelpersAfter = (dyld::gLibSystemHelpers != NULL);
if ( !haveLibSystemHelpersBefore && haveLibSystemHelpersAfter ) {
// now safe to use malloc() and other calls in libSystem.dylib
dyld::gProcessInfo->libSystemInitialized = true;
}
}
}
else if ( type == S_INIT_FUNC_OFFSETS ) {
const uint32_t* inits = (uint32_t*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uint32_t);
// Ensure section is within segment
if ( (sect->addr < seg->vmaddr) || (sect->addr+sect->size > seg->vmaddr+seg->vmsize) || (sect->addr+sect->size < sect->addr) )
dyld::throwf("__init_offsets section has malformed address range for %s\n", this->getPath());
if ( seg->initprot & VM_PROT_WRITE )
dyld::throwf("__init_offsets section is not in read-only segment %s\n", this->getPath());
for (size_t j=0; j < count; ++j) {
uint32_t funcOffset = inits[j];
// verify initializers are in image
if ( ! this->containsAddress((uint8_t*)this->machHeader() + funcOffset) ) {
dyld::throwf("initializer function offset 0x%08X not in mapped image for %s\n", funcOffset, this->getPath());
}
if ( ! dyld::gProcessInfo->libSystemInitialized ) {
// <rdar://problem/17973316> libSystem initializer must run first
const char* installPath = getInstallPath();
if ( (installPath == NULL) || (strcmp(installPath, libSystemPath(context)) != 0) )
dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
}
Initializer func = (Initializer)((uint8_t*)this->machHeader() + funcOffset);
if ( context.verboseInit )
dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
#if __has_feature(ptrauth_calls)
func = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)func, ptrauth_key_asia, 0);
#endif
bool haveLibSystemHelpersBefore = (dyld::gLibSystemHelpers != NULL);
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
bool haveLibSystemHelpersAfter = (dyld::gLibSystemHelpers != NULL);
if ( !haveLibSystemHelpersBefore && haveLibSystemHelpersAfter ) {
// now safe to use malloc() and other calls in libSystem.dylib
dyld::gProcessInfo->libSystemInitialized = true;
}
}
}
}
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
在这个方法中,确保libSystem
必须被优先初始化
doInitialization 初始化镜像文件
而根据源码分析得出结论:doModInitFunctions
是被doInitialization
方法调起的;
而doInitialization
我们在之前分析过,它是在recursiveInitialization
方法中调用的:
map_images和load_images
在源码中断点执行:
发现,map_images
会先于load_images
方法执行;map_images
和load_images
是沟通了dyld
和objc
的桥梁
map_images
执行流程:
_dyld_objc_notify_register
-->registerObjCNotifiers
-->notifyBatchPartial
-->map_images
在_dyld_objc_notify_register
调用的时候,我们注意到map_images
是第一个参数,那么根据调用流程最后判断在registerObjCNotifiers
方法中的sNotifyObjCMapped
就是map_images
:
那么,在这里赋值给sNotifyObjCMapped
之后,又是在什么时候调用的呢?全局搜索sNotifyObjCMapped
:
sNotifyObjCMapped
是在notifyBatchPartial
方法中被调用的,那么notifyBatchPartial
方法又是什么时候被调用的呢?
最终发现又回到了registerObjCNotifiers
方法中,再次方法中将map_images
赋值给sNotifyObjCMapped
之后,紧接着直接在notifyBatchPartial
方法中调用了sNotifyObjCMapped
,也就是map_images
;
load_images
执行流程:
_dyld_objc_notify_register
-->registerObjCNotifiers
-->load_images
load_images
里边是所有的load
方法的集合,他作为_dyld_objc_notify_register
的第二个参数,赋值给了sNotifyObjCInit
,那么sNotifyObjCInit
是在什么时候调用的呢?
sNotifyObjCInit
实在notifySingle
方法中被调用执行的,搜索notifySingle
找到它被调用的地方:
那么,这里就有一个疑问了?按照流程,明明是在doInitialization
的执行过程中执行到registerObjCNotifiers
方法中,才将load_images
赋值给sNotifyObjCInit
,那为什么在doInitialization
执行之前就去调用了notifySingle
了呢?
因为doInitialization
是对当前镜像文件的初始化,并非对整个程序的初始化,所以当前镜像文件的load
方法应该是在doInitialization
之后的notifySingle
方法中调用,在doInitialization
之前的notifySingle
的方法调用的是依赖文件的方法,recursiveInitialization
是一个递归初始化方法;
我们通过objc
的源码来验证一下:
在objc-os.mm
文件中添加以下代码:
__attribute__((constructor)) void objcFunc() {
printf("这是objc的Func--->%s \n", __func__);
}
在类Person
中添加load
方法:
@implementation Person
+ (void)load {
NSLog(@"%s", __func__);
}
@end
在main
文件中添加以下代码:
__attribute__((constructor)) void mineFunc() {
printf("这是我自己的Func--->%s \n", __func__);
}
运行打印结果如下:
这是objc的Func--->objcFunc
+[Person load]
这是我自己的Func--->mineFunc
Hello, World!
结论
objc
源码中的C++构造方法
先执行,然后执行我们自己程序中的类的load
方法,最后执行我们自己程序中的C++构造方法
与上述分析结论一致;