前言
今天我们重点来分析一下,iOS App运行时,在main()
方法执行之前,程序到底做了哪些事?
准备工作
示例,新建一个iOS应用工程,查看方法加载的顺序
__attribute__((constructor)) void lg_cFunction() {
// printf();
NSLog(@"%s -- 来了", __func__);
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
NSLog(@"%s -- 来了", __func__);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
---------------------------------分割线---------------------------------
@implementation ViewController
+ (void)load {
NSLog(@"%s -- 来了", __func__);
}
@end
运行后
2020-09-28 11:06:39.756959+0800 Test1[49592:4931930] +[ViewController load] -- 来了
2020-09-28 11:06:39.757473+0800 Test1[49592:4931930] lg_cFunction -- 来了
2020-09-28 11:06:39.757671+0800 Test1[49592:4931930] main -- 来了
2020-09-28 11:06:39.800886+0800 Test1[49592:4931930] result is 0
发现当前3个方法的调用顺序是
load --> lg_cFunction(c++方法) -->main入口
,why?
编译过程
带着上述方法调用顺序的疑问,我们先来大致了解下,App编译的一个过程:
大致流程是:
源文件(.h .m .cpp)
-->预编译(检查语法)
-->编译(转化为汇编)
-->汇编(生成机器码文件)
-->链接(也包括一些库的链接)
-->生成可执行文件(在生成的.app中右键打开包文件,里头的exec)
其中链接
这一步苹果系统使用的就是dyld库
来完成的。那什么是dyld
呢?
dyld动态链接器
dyld
是英文 the dynamic link editor 的简写,翻译过来就是动态链接器
,是苹果操作系统的一个重要的组成部分。在 iOS/Mac OSX 系统中,仅有很少量的进程只需要内核就能完成加载,基本上所有的进程都是动态链接的,所以 Mach-O 镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容的填补,这个填补工作就是由 动态链接器dyld
来完成的,也就是符号绑定
。
找入口
你想知道系统调用load之前走了什么流程?很简单,在load方法里打断点,然后lldb bt
指令查看调用堆栈信息。
指令名称 | 释义 |
---|---|
bt | 查看调用堆栈信息,加all可打印多有thread的堆栈 |
上图红框处可知,最开始是从
_dyld_start_
开始的,我们全局搜索_dyld_start_
,寻找入口。看注释,我们注意到会调用
dyldbootstrap::start
再次全局搜索
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
// 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);
}
最终调用dyld::_main
,这个就是我们要找的入口!
dyld::_main流程大致分析
这个函数实现代码量巨大,我们一步步看。
- 首先看函数的返回值
result
,如下图:
再搜索result
的赋值地方,
上图是初始化
上图是一个if特殊情况条件里的返回,不考虑。
上图是个宏编译的if条件的,不考虑。
上图也是一个if条件中的赋值,并return,不作考虑。
同理,不考虑。
首先3是宏条件编译,不考虑,然后1和2是主要赋值的地方,都用到一个共同的变量
sMainExecutable
,应该是关键。
也是if条件里的,不考虑。
最终发现,赋值result的都是通过变量sMainExecutable
,那我们再搜索sMainExecutable的赋值情况:
第一个就找到了,通过方法
instantiateFromLoadedImage
,再看注释// instantiate ImageLoader for main executable
-->为主可执行文件实例化ImageLoader,接着我们重点看看instantiateFromLoadedImage
函数
// 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";
}
关键代码是ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
// sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}
首先sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
根据注释,应该是
定义mach-o文件的格式
,定义什么格式呢?这时需要用到查看
mach-o文件
的软件MachOView,举个例子来看看:先找到工程的.app文件所在位置:
再右键显示包内容:
选择exec可执行文件
拖入到MachOView中:
所以,sniffLoadCommands
定义的就是mach-o
里的区间Load Commands
里的格式。
格式定义完成后,接着进行初始化instantiateMainExecutable
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
不论是压缩模式ImageLoaderMachOCompressed
,还是标准模式ImageLoaderMachOClassic
,最终都是生成ImageLoader
对象,完成一个初始化sMainExecutable
的过程。
至此,我们通过返回值result
反向搜索赋值,找到sMainExecutable
的初始化流程(第6577行),那么在sMainExecutable
的初始化前,又走了哪些流程?我们一点点的往下看:
上述所有图,大致描述了dyld::_main
的大致流程:
- 环境变量配置
- 共享缓存处理
- 主程序表初始化
- 插入动态库
- 链接主程序表
- 链接动态库
- 弱符号绑定
- 初始化所有
- 主程序入口处理
第8步初始化流程详细分析
大家肯定想知道:我们平时写的对象到底是如何初始化的呢,说白了就是我们之前讨论的_objc_init
是在哪里触发被调用的呢?带着这个问题,我们首先看看initializeMainExecutable
源码:
再看看runInitializers
源码:
继续processInitializers
继续recursiveInitialization
很明显,这里面,需要分成两部分探索,一部分是notifySingle函数
,一部分是doInitialization函数
。
首先探索notifySingle函数
小技巧-->全局搜索notifySingle(
函数
红框里是核心代码
再搜索sNotifyObjCInit
,看看哪里赋值处理
上图赋值的是在函数
registerObjCNotifiers
,再搜索其被调用的地方是在
_dyld_objc_notify_register
进行了调用,但是_dyld_objc_notify_register
的函数需要在libobjc源码中
搜索终于,是在 _objc_init
中,这不正是我们最开始要找的问题所在吗,哈哈!
在_objc_init
源码中调用了_dyld_objc_notify_register
,并传入了参数load_images
,那么sNotifyObjCInit
= load_images
,而load_images
中会调用所有的+load
方法。
整个
load
方法的调用链路就是:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一个回调处理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)
load方法的调用栈信息
前面的流程跟我们之前分析的一样:
_dyld_start-->dyld::main-->initialzeMainExecutetable()初始化主程序表-->dyld::notifySingle回调结果-->load_images加载文件
,紧接着就调用了load方法
,具体调用的位置如下图:cxx方法调用栈信息
同理,在cxx方法处打断点,查看调用栈:
与
load
不同的是,在recursiveInitialization
之后,和load不同的是,在
doInitialization
里触发的cxx方法。那我们具体看看doInitialization
的流程是如何处理的。先看
doImageInit
再看
doModInitFunctions
,和doImageInit
差不多的流程接着搜索
LC_SEGMENT_COMMAND
以64位为例,看看
LC_SEGMENT_64
看来是
_TEXT
区间相关,我们查看mach-o符号文件:所以
doModInitFunctions
就是在编译调用上图红框处的区间里所有的函数,其中就包含cxx函数
的调用触发。
cxx函数调用链
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::doModInitFunctions-->func()
那么回到之前的问题,objc_init
是在哪里被调用呢?在initializeMainExecutable
没找到答案,那么接下来,只能使用终极大招了-->符号断点
。
符号断点查看objc_init
前面的流程和
cxx函数调用
大致相同,在第3步时,会调用libSystem
库,再去到libdispatch
库,然后触发_os_object_init
-->_objc_init
,请看下列图:上图可知:objc_init调用链如下:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::doModInitFunctions
libSystem_initlializer-->libdispatch_init-->_os_object_init-->_objc_init
_objc_init-->注册观察者_dyld_objc_notify_register(&map_images, load_images, unmap_image)-->第2个参数load_images == sNotifyObjCInit
,然后再在dyld加载的ImageLoader::recursiveInitialization
这一步里notifySingle-->sNotifyObjCInit
触发回调,让_objc_init
和dyld加载过程形成一个闭环。
main入口
上面我们知道了load
和cxx函数
的调用链,还剩下main()了,它是在哪里被调用的呢?智能在cxx函数
里打断点,然后打开汇编模式,跟着断点一步步看了。
然后按住按键control,点击step over
上图红框里发现,其实和之前分析的流程基本一致,还是回到了
_dyld_start
,看来我们只有回到最初的汇编代码里,去寻找main入口了:在第3步也是_dyld_start
的最后,找到了main()
,此时才调用,当然比load
和cxx函数
的调用时机都晚!
总结
借用Style_月月
的iOS-底层原理 15:dyld加载流程的dyld加载流程图: