应用程序加载
应用程序的加载
本篇主要是分析dyld的加载流程
,了解在main函数之前,底层还做了什么? 我们经常听到的二进制重排
、启动优化
等字眼,就是在App的启动流程
中做文章。
编译过程及库
在分析app启动之前,我们需要先了解iOSapp代码的编译过程
以及动态库
和静态库
。
编译过程
编译过程如下所示
-
源文件
:载入.h、.m、.cpp等文件 -
预处理
:替换宏,删除注释,展开头文件,产生.i文件 -
编译
:将.i文件转换为汇编语言,产生.s文件 -
汇编
:将汇编文件转换为机器码文件,产生.o文件 -
链接
:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件
静态库和动态库
静态库
:在链接阶段,会将可汇编生成的目标程序与引用的库一起链接打包到可执行文件当中。此时的静态库就不会再改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的
-
优点
:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖,直接就可以运行 -
缺点
:由于静态库会有两份,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大
动态库
:程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入
-
优势
:
- 减少打包之后app的大小:因为不需要拷贝至目标程序中,所以不会影响目标程序的体积,与静态库相比,减少了app的体积大小
- 共享内存,节约资源:同一份库可以被多个程序使用
- 通过更新动态库,达到更新程序的目的:由于运行时才载入的特性,可以随时对库进行替换,而不需要重新编译代码
-
缺点
:动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行
静态库与动态库图示
image.pngdyld加载流程分析
dyld
(the dynamic link editor)是苹果的动态链接器
,是苹果操作系统的重要组成部分,在app被编译打包成可执行文件格式的Mach-O文件
后,交由dyld负责连接,加载程序
dyld演变过程WWDC视频 -> WWDC2017:App Startup Time: Past, Present, and Future
App启动流程图dyld加载流程
创建工程,ViewController
中重写load方法
,在main
中加了一个C++方法,并且在main函数中打印,查看程序执行顺序
<!-- ViewController.m文件 -->
@implementation ViewController
+ (void)load{
NSLog(@"%s",__func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
@end
<!-- main.m文件 -->
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
NSLog(@"1223333");
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
__attribute__((constructor)) void kcFunc(){
printf("来了 : %s \n",__func__);
}
// 控制台打印
2021-08-06 21:46:06.605315+0800 002-应用程加载分析[5305:346935] +[ViewController load]
来了 : kcFunc
2021-08-06 21:51:43.385279+0800 002-应用程加载分析[5305:346935] 1223333
通过打印结果可以看出其顺序是load --> C++方法 --> main
,main
作为入口函数为什么不是最先执行?根据这个问题,我们来探索main函数之前底层做了什么?
-
load
方法处加一个断点,通过bt
打印堆栈信息查看app启动是从哪里开始的。由于栈是先进后出,所以程序先执行的是_dyld_start
- 需要去
OpenSource
下载一份dyld源码来进行分析,这里使用的是dyld-852.tar.gz
版本 - 在
dyld-852源码
中全局搜索_dyld_start
,查找arm架构发现,是由汇编实现,通过汇编注释发现会调用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
方法,这是一个C++方法(以arm架构为例)
- 源码中搜索
dyldbootstrap
找到命名作用空间,再在这个文件中查找start
方法,其核心是返回值调用了dyld的_main函数
,其中macho_header是Mach-O的头部,而dyld加载的文件就是Mach-O类型的,即Mach-O类型是可执行文件类型,由四部分组成:Mach-O头部、Load Command、section、Other Data
,可以通过MachOView查看可执行文件信息
dyld流程中的main函数
进入dyld::_main
的源码实现,发现代码特别长,可以根据_main函数的返回值进行反推。反推流程result
-> sMainExecutable
-> ``
在_main函数中主要做了以下几件事情:
-
环境变量配置
:根据环境变量设置相应的值以及获取当前运行架构
// 创建主程序cdHash的空间
uint8_t mainExecutableCDHashBuffer[20];
//从环境中获取主可执行文件的 cdHash
const uint8_t* mainExecutableCDHash = nullptr;
if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
unsigned bufferLenUsed;
if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
}
//配置信息,获取主程序的mach-o header、silder(ASLR的偏移值)
getHostInfo(mainExecutableMH, mainExecutableSlide);
-
共享缓存
:检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如UIKit、CoreFoundation等
// load shared cache
// 检查共享缓存是否开启,iOS中必须开启
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
// 检查共享缓存是否映射到了共享区域
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
mapSharedCache(mainExecutableSlide);
#endif
-
主程序的初始化
:调用instantiateFromLoadedImage
函数,实例化一个ImageLoader
对象
// instantiate ImageLoader for main executable
// 主程序的初始化
// 加载可执行文件,生成一个ImageLoader实例对象
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
-
插入动态库
:遍历DYLD_INSERT_LIBRARIES
环境变量,调用loadInsertedDylib
加载
// 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.
// 插入库,然后是main,然后是其他
sInsertedDylibCount = sAllImages.size()-1;
link 主程序
// link主程序,链接主程序与动态库、插入动态库
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
link动态库
弱符号绑定
执行初始化方法
-
寻找主程序入口即main函数
:从Load Command
读取LC_MAIN
入口,如果没有,就读取LC_UNIXTHREAD
,这样就来到了日常开发中熟悉的main函数
了
dyld加载流程
load的源码链为:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一个回调处理) --> load_images(libobjc.A.dylib)
整个加载过程是由dyld
开始,最终到达libobjc
结束,也就是说应用加载流程不只是dyld在工作,还有很多库在配合一起完成这个过程。
dyld流程-主程序运行
主程序初始化initializeMainExecutable
函数源码分析
- 进
initializeMainExecutable
源码,主要是循环遍历,都会执行runInitializers
方法
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
//当此进程退出时,注册cxa_atexit() 处理程序以在所有加载的图像中运行静态终止符
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]);
}
- 全局搜索
runInitializers
找到如下源码,其核心代码是processInitializers
函数的调用
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);
}
- 进入
processInitializers
函数的源码实现,其中对镜像列表调用recursiveInitialization
函数进行递归实例化
// <rdar://problem/14412057> upward dylib initializers can be run too soon
// To handle dangling dylibs which are upward linked but not downward, all upward linked dylibs
// have their initialization postponed until after the recursion through downward dylibs
// has completed.
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
函数,其源码实现如下
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();
}
在这里,需要分成两部分探索,一部分是notifySingle
函数,一部分是doInitialization
函数。
notifySingle 函数
- 全局搜索
notifySingle
(函数,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
这句 - 全局搜索
sNotifyObjCInit
,发现没有找到实现,有赋值操作
- 搜索
registerObjCNotifiers
在哪里调用了,发现在_dyld_objc_notify_register
进行了调用。注意:_dyld_objc_notify_register
的函数需要在libobjc源码
中搜索 - 在
objc4-818
源码中搜索_dyld_objc_notify_register
,发现在_objc_init
源码中调用了该方法,并传入了参数,所以sNotifyObjCInit
的赋值的就是objc中的load_images
,而load_images会调用所有的+load方法。所以综上所述,notifySingle
是一个回调函数
doInitialization 函数
- 走到objc的
_objc_init
函数,发现走不通了,我们回退到recursiveInitialization
递归函数的源码实现,发现我们忽略了一个函数doInitialization
- 进入
doInitialization
函数的源码实现,这里也需要分成两部分,一部分是doImageInit
函数,一部分是doModInitFunctions
函数 - 进入
doImageInit
源码实现,其核心主要是for循环加载方法的调用,这里需要注意的一点是,libSystem的初始化必须先运行
- 进入
doModInitFunctions
源码实现,这个方法中加载了所有Cxx
文件 - 可以通过测试程序的堆栈信息来验证,在C++方法处加一个断点,走到这里,还是没有找到_objc_init的调用?怎么办呢?放弃吗?当然不行,我们还可以通过_objc_init加一个符号断点来查看调用
_objc_init
前的堆栈信息, -
_objc_init
加一个符号断点,运行程序,查看_objc_init
断住后的堆栈信息 - 在
libsystem
中查找libSystem_initializer
,查看其中的实现 - 根据前面的堆栈信息,我们发现走的是
libSystem_initializer
中会调用libdispatch_init
函数,而这个函数的源码是在libdispatch
开源库中的,在libdispatch中搜索libdispatch_init
- 进入_os_object_init源码实现,其源码实现调用了
_objc_init
函数
得出结论_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)
分析doInitialization(调用init方法)与context.notifySingle(单个通知注入)的关系
通过调用链路,此函数里面找不到load_images
函数了,应为load_images已经不是dyld中的函数了,是属于libobjc
库的函数。在此函数中发现了关键的回调(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
,但是在回调之前判断了sNotifyObjCInit
是否为空,就证明一定有给sNotifyObjCInit变量赋值的地方
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<dyld_image_state_change_handler>* 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<dyld_image_state_change_handler>::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 ) {
// <rdar://problem/7008875> Save load addr + UUID for images from outside the shared cache
// <rdar://problem/50432671> 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
,找到了函数registerObjCNotifiers
- 通过
registerObjCNotifiers
函数得知,有一个调用者调用了这个函数,并且传递的第二个参数是init,全局搜索这个调用者
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
函数为注册通知,通过全局查找,没有注册的地方,这里通过对工程下符号断点的方式看一下是哪个库调用的这个方法