本篇文章将探究应用程序是如何加载的。我们平时都认为main
是程序的入口,是不是这样呢?
1.案例分析
引入一个案例,在ViewController.m
中添加一个load方法
,程序main.m
中添加一个C++函数
。
ViewControler
代码如下:
@interface ViewController ()
@end
@implementation ViewController
+ (void)load{
NSLog(@"%s", __func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"viewDidLoad --- ");
}
@end
复制代码
main.m
代码如下:
__attribute__((constructor)) void kcFunc(){
printf("C... %s \n",__func__);
}
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"main ...");
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
复制代码
运行程序,根据运行日志,发现程序首先运行了load方法
,然后调用了C++函数
,最后才进入到main方法
中。
流程:load
-> C++
-> main
。为什么?main函数
难道不是应用程序的入口吗?在main
之前到底做了什么?我们一步步来探究。
在分析启动流程之前,先学习一些概念。
2.应用程序编译过程
1.静态库
在链接阶段,会将汇编生成的目标程序与引用的库一起链接打包到可执行文件当中。此时的静态库就不会在改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的。例如:.a
、.lib
优点:
编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖,直接就可以运行缺点:
由于静态库可能会有两份,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大
2.动态库
程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入。例如:.so
、.framwork
、.dll
优点:
减少打包之后app
的大小,共享内存,节约资源,更新动态库,达到更新程序缺点:
动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行
3.编译过程
.h、.m、.cpp
等源文件->预编译->编译->汇编->链接(动静态库加持)->可执行文件。源文件:
载入.h、.m、.cpp等文件预处理:
替换宏,删除注释,展开头文件,产生.i文件编译:
将.i文件转换为汇编语言,产生.s文件汇编:
将汇编文件转换为机器码文件,产生.o文件链接:
对.o文件中引用其他库的地方进行引用,生成最后的可执行文件
4.动态链接器dyld
dyld(the dynamic link editor)
是苹果的动态链接器
,是苹果操作系统的重要组成部分,在app
被编译打包成可执行文件格式的Mach-O文件
后,交由dyld
负责连接,并加载程序。
dyld的作用:
加载各个库,也就是image镜像文件
,由dyld
从内存中读到表中,加载主程序,link
链接各个动静态库,进行主程序初始化。
3.dyld入口探索
如何寻找入口?没错,bt
!!!
在Viewcontroller
的load方法
添加断点,运行程序。在控制台输入指令bt
,查看运行的函数调用堆栈信息。见下图:
我们找到了程序的入口dyld_start
,从左侧的堆栈信息也看到了入口。其实从这里我们已经可以看到一些端倪。
调用流程
:_dyld_start -> dyldbootstrap::start -> dyld::main -> dyld::initializeMainExecutable -> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> dyld::notifySingle -> load_images -> [ViewController load]
。
下载dyld
源码:dyld源码下载地址,当前最新版本是dyld-852.7 。这部分源码是不能编译的。
打开dyld源码
,全局搜索_dyld_start
,开始探索程序加载流程。最终在dyldStartup.s
文件中搜索到了伪汇编流程。见下图:
针对不同的环境提供了不同的实现方式,比如,我们现在看到的就是针对arm64真机环境
的实现。能力有限,看不懂,但是看注释发现,不论是何种环境,最终都会走到dyldbootstrap::start
流程中。这也和我们前面bt
看到的函数调用堆栈信息结果是一致的:_dyld_start -> dyldbootstrap::start
。
继续!下面全局搜索dyldbootstrap
。见下图:
在dyldInitialization.cpp
文件中搜索到了dyldbootstrap命名空间
。其中start方法
是我们所需要关注的,通过注释我们可以了解到这是引导dyld
的代码。完成dyld
的引导,并执行dyld
的main
函数。
-
补充一点
:macho_header
是什么?我们知道程序在编译后形成可执行的Mach-O文件
,交由dyld
连接并加载。也就说dyld
加载的就是可执行文件Mach-O
,macho-header
就是可执行文件的头文件。我们可以通过工具MachOView
查看可执行文件信息。
典型的Mach-O文件
包含三个区域:
Header
:保存Mach-O
的一些基本信息,包括平台、文件类型、指令数、指令总大小,dyld
标记Flags
等等。Load Commands
:紧跟Header
,加载Mach-O文件
时会使用这部分数据确定内存分布,对系统内核加载器和动态连接器起指导作用。Data
:每个segment
的具体数据保存在这里,包含具体的代码、数据等等。
4.dyld main函数分析
进入main函数
,600多行代码,这可如何是好?好吧,从return result;
进行反推,确定主程序初始化的过程,总结dyld main
主要做了哪些工作呢?
- 环境变量的配置
根据环境变量设置相应的值并获取当前运行框架。
- 共享缓存
检查是否开启,以及共享缓存是否映射到共享区域,例如UIKit
、CoreFoundation
等。
- 主程序初始化
调用instantiateFromLoadedImage
函数实例化了一个ImageLoader对象
。
- 加载动态库
遍历DYLD_INSERT_LIBRARIES
环境变量,调用loadInsertedDylib
。
- 链接主程序
- 链接动态库
- 执行初始化方法
- 寻找主程序入口即
main函数
从Load Command
读取LC_MAIN入口
,如果没有,就读取LC_UNIXTHREAD
,这样就来到了日常开发中熟悉的main函数
了。
下面详解主程序初始化和主程序执行流程
5.主程序初始化
主程序变量为sMainExecutable
,它通过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
实例对象,其创建方法为instantiateMainExecutable
。进入instantiateMainExecutable源码
:
// 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
}
复制代码
其作用为创建主程序映射,返回一个ImageLoder类型
的image
对象,即主程序
。其中sniffLoadCommands函数
获取Mach-O文件
的load Command
相关信息,并对其进行各种校验。
6.主程序执行流程
1.流程分析
通过上面的分析,我们已经跟踪到了以下的程序流程:
_dyld_start -> dyldbootstrap::start -> dyld::main
。
继续跟踪主程序执行执行流程,进入initializeMainExecutable
函数:
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// 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]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
复制代码
此过程会为所有插入的dylib
调用initialzers
,进入runInitializers函数
,代码如下:
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);
}
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
uint64_t t1 = mach_absolute_time();
mach_port_t thisThread = mach_thread_self();
ImageLoader::UninitedUpwards up;
up.count = 1;
up.imagesAndPaths[0] = { this, this->getPath() };
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized, false);
mach_port_deallocate(mach_task_self(), thisThread);
uint64_t t2 = mach_absolute_time();
fgTotalInitTime += (t2 - t1);
}
复制代码
runInitializers
中的核心代码是processInitializers
,在processInitializers函数
中,对镜像列表调用recursiveInitialization函数
进行递归实例化。进入recursiveInitialization函数
,源码如下:
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);
if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
// record termination order
if ( this->needsTermination() )
context.terminationRecorder(this);
// 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);
if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.addTime(this->getShortName(), t2-t1);
}
}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}
recursiveSpinUnLock();
}
复制代码
至此,程序加载流程如下:
_dyld_start -> dyldbootstrap::start -> dyld::main -> initializeMainExecutable -> runInitializers -> processInitializers -> recursiveInitialization(递归)
。
下面着重分析recursiveInitialization函数
。通过注释不难理解下面这段代码是核心。
看注释,这三个部分是一个渐进的过程:
- 将要初始化镜像文件;
- 初始化镜像文件;
- 完成镜像文件初始化。
这里有两个函数我们重点关注一下:notifySingle
,doInitialization
。
2. notifySingle探索
dyld源码
全局搜索notifySingle
,找到notifySingle函数
实现。见下图:
其中关键代码是1112行的sNotifyObjCInit
,调用该函数,并且参数为镜像文件的路径和machO头
。再全局搜索sNotifyObjCInit
,找到该函数定义初始化的位置。在registerObjCNotifiers函数
中,sNotifyObjCInit
被设置为init函数
。见下图:
registerObjCNotifiers函数
在何处被调用呢?全局搜索registerObjCNotifiers函数
。见下图:
找到了调用registerObjCNotifiers
的地方,_dyld_objc_notify_register函数
。全局搜索_dyld_objc_notify_register函数
,没有找到被调用的地方,但是很多函数都会有这样的一个注释:
// Also note, this function must be called after _dyld_objc_notify_register.
复制代码
也就是说必须在调用_dyld_objc_notify_register
之后,才可以执行相关的流程。另外,通过字面了解,objc通知注册
,与libobjc.A.dylib
源码有什么关系?
解开谜底的时候到了,在libobjc.A.dylib源码
中搜索_dyld_objc_notify_register
,在objc初始化方法objc_init()
中找到了调用。见下图:
在调用_dyld_objc_notify_register函数
时,传入了三个参数。
map_images
的函数地址load_images
函数实现unmap_image
函数实现
那么回到dyld
源码,不难理解:
sNotifyObjCMapped = mapped = &map_images
sNotifyObjCInit = init = load_images
sNotifyObjCUnmapped = unmapped = unmap_image
也就是说:
objc_init()
向dyld
中注册了三个函数
,在dyld
进行动静态库加载过程时,当特定环境满足的条件下,这三个函数会调用执行。
2. map_images和load_images调用时机
从上面的流程中我们大概摸清了一个逻辑,就是dyld
进行动静态库加载,初始化主程序的时候,会加载libObjc.dylib
库,而objc
库会向dyld
中注册三个方法。那么这三个方法什么时候被调用呢?
dyld
不能编译运行,只能从libObjc.dylib
入手,同样通过bt
来查看运行堆栈。第三个方法是时卸载镜像文件的时候才会被调用,所以我们暂且只研究前面两个方法。我们分别在map_images
方法和load_images
方法中添加断点
,看谁先被执行!运行程序:
-
map_images优先被执行,查看运行堆栈,其流程为:
_dyld_objc_notify_register --> registerObjCNotifiers --> notifyBatchPartial --> map_images
-
继续运行程序,断点运行到load_images:
_dyld_objc_notify_register --> registerObjCNotifiers --> load_images
至此,可以得出结论,map_images的调用会先于load_images。即实现类后,才能调用+load方法
。
但是我们依然需要通过源码来验证一下!调用_dyld_objc_notify_register
之后,会执行registerObjCNotifiers
方法,在该方法中,会循环调用sNotifyObjCInit
方法,也就是load_images
方法。
load_images
方法调用位置找到了,map_images
在哪调用呢?进入notifyBatchPartial
函数,在这里成功找到了sNotifyObjCMapped
,也就是map_images
的调用的位置,见下图:
总结:dyld
进行动静态库加载,初始化主程序的时候,会加载libObjc.dylib
库,而objc
库会向dyld
中注册三个方法。这三个方法分别是:map_images
、load_images
、unmap_image
,并且map_images
会优先于load_images
方法的调用!
问题:我们上面摸清了dyld:main
做的一些主要工作,主程序的执行
过程,并且分析出_dyld_objc_notify_register
实现了libObjc.dylib
对dyld
的函数注册,以及函数的执行!但是依然有些问题没有解决,比如上面案例中c++
何时加载的,dyld
如何调用objc_init()
的?
3.doInitialization函数分析
虽然我们已经知道函数的注册过程,但是依然没有摸清objc_init()
被调用的时机。recursiveInitialization中
分析了notifySingle
之后,我们还有一个doInitialization
方法没有探究。doInitialization源码
见下图:
dyld
作为动态连接器,进行动态库的加载工作,libobjc.A.dylib库
也是它要加载的内容。进入doInitialization函数
的doImageInit
流程中。见下图:
怎么理解?看注释! libSystem initializer must run first
,这个注释提示的很明确,libSystem库
必须优先初始化!继续看doInitialization函数
的doModInitFunctions
的实现。见下图:
doModInitFunctions方法
加载了所有Cxx文件
。如何验证呢?bt
一下!依然结合上面的案例,在c++
函数中添加断点,查看运行堆栈信息,见下图:
所以c++
调用流程就是:
_dyld_start -> dyldbootstrap::start -> dyld::main -> dyld::initializeMainExecutable -> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> doInitialization -> doModInitFunctions -> c++
4.objc_init()调用时机
回调函数注册流程还没找到呢?但是从上面的分析,至少可以知道和libSystem库
有关!那么我们想要找的流程肯定在libSystem库
中。下载Libsystem-1292.60.1源码。大海捞针肯定不行,首先要找到个入口。回到我们最熟悉的libobjc.A.dylib源码
,在objc_init()
中添加断点,bt
查看运行堆栈信息,进行反向推导。见下图:
通过上图可以发现,_os_object_init
调用了_objc_init
,并且_os_object_init
来自于libdispatch.dylib
。前面已经分析了,libSystem
库需要优先被加载,同时上面的堆栈信息也得到了验证。下面在libSystem库
中全局搜索libSystem_initializer
。libSystem_initializer源码
实现:
根据上面的堆栈信息,libSystem_initializer->libdispatch_init
会进入流程libdispatch_init
。
extern void libdispatch_init(void); // from libdispatch.dylib
复制代码
查看源码,发现这部分来自libdispatch.dylib库
。
没啥好说的,下载libdispatch.dylib源码
,打开源码搜索libdispatch_init
。发现以下流程:
全局搜索_os_object_init()
,找到以下源码内容,第一行就调用了_objc_init();
方法。
OK
!闭环完成!回调函数的注册流程也打通!总结一下:
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)
这个过程完成了上面三个回调函数的注册!
所以可以简单的理解为sNotifySingle
这里是添加通知即addObserver
,_objc_init
中调用_dyld_objc_notify_register
相当于发送通知,即push
,而sNotifyObjcInit
相当于通知的处理函数,即selector
。
7.main函数
继续前面的案例,程序在调用load方法
,c++函数
后,step over
过掉断点,走完dyldbootstrap::start
后,会寻找main函数
。
lldb
使用register read读取寄存器
,发现rax = main
函数。
注意:
main是写定的函数,写入内存,读取到dyld;如果修改了main函数的名称,会报错。