iOS底层-dyld加载流程

1,275 阅读9分钟

前言

写代码时一般都离不开main,我们知道main是程序的入口,那么系统在main之前做了什么呢,我们去探究下。

编译过程和库

  • 程序的编译过程为:预编译 -> 编译 -> 汇编 -> 链接 -> 可执行文件,如下图:

编译图解

截屏2021-07-12 11.23.50.png

代码查看

  • 也可以代码查看流程,以main.m为例子,命令为:clang -ccc-print-phases -framework Foundation main.m -o main
clang -ccc-print-phases -framework Foundation main.m -o main

-framework Foundation main.m -o main
   +- 0: input, "Foundation", object
   |           +- 1: input, "main.m", objective-c
   |        +- 2: preprocessor, {1}, objective-c-cpp-output
   |     +- 3: compiler, {2}, ir
   |  +- 4: backend, {3}, assembler
   |- 5: assembler, {4}, object
+- 6: linker, {0, 5}, image
7: bind-arch, "x86_64", {6}, image

核心步骤的意思如下:

  • preprocessor, {1}, objective-c-cpp-output:预处理,编译器前端
  • compiler, {2}, ir:编译器生成中间代码ir
  • backend, {3}, assemblerLLVM后端成成汇编
  • assembler, {4}, object:生存机器码
  • linker, {0, 5}, image:链接器
  • bind-arch, "x86_64", {6}, image:生成可执行的二进制文件image

静态库和动态库

应用程序的加载原理实质是库的二进制文件夹到到内存的过程,库通常分为两种:静态库动态库

静态库

  • 静他库在链接的过程就已经把内容链接到可执行文件中
  • 优点:链接完后,库文件就没有作用了,直接可以运行可执行文件。执行速度比较快
  • 缺点:静态库链接时,可能会被多次复制,导致包提及偏大。链接后,内容不可更改

动态库

  • 动态库在链接的过程不会把内容链接进去,而是在执行过程中会去找要链接的内存
  • 优点:由于链接时,重复的地方只被复制一次,占用的包体积小。链接后还可以通过运行时对内容进行替换
  • 缺点:运行时去找链接需要的内容会影响运行速度,如果链接后,环境缺少动态库,程序就会无法运行。

图示

截屏2021-07-12 13.53.39.png

动态链接器dyld

dyld简介: dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在app被编译打包成可执行文件Mach-O后,交由dyld负责余下的工作。

  • 所以App的启动流程如下: 截屏2021-07-12 14.36.19.png

启动入口

现在main上打断点运行,来到断点后,发现main之前有个start

截屏2021-07-12 14.52.08.png
但是看不到里面内容,然后下一个start符号断点,还是没有断住,说明最开始走的不是这个。我们知道+load函数在main函数之前调用,可以在+load方法处达断点,然后查看调用堆栈:

截屏2021-07-12 15.00.17.png

  • 这里我们得知:程序运行是从_dyld_start开始

源码分析

接下来,我们在源码dyld-852中分析

_dyld_start

  • dyld-852工程中搜索_dyld_start

截屏2021-07-12 15.48.43.png

  • 本文以arm64架构为例_dyld_start实质是调用dyldbootstrap::start方法,在C++中,::的前面是命名空间,后面是命名空间里的方法,类似类调用类方法。

dyldbootstrap::start

  • 这里是dyldbootstrap调用start,然后去搜索dyldbootstrap,然后找到start函数:

    截屏2021-07-12 15.56.27.png

    • 观察发现,start函数的核心是返回值为dyld::_maindyldmain第一个参数是macho_header,它是Mach-O的组成部分之一。
    • Mach-O是可执行文件的合集,它会将数据流分组,主要为三部分:Mach_header, Load Command, Data, 可以通过MachOView查看。
      • Mach_header:里会有Mach-OCPU信息,以及Load Command的信息
      • Load Command:包含Mach-O命令类型信息名称二进制的位置
      • Data:由Segment的数据组成,是Mach-O占比最多的部分,有代码有数据,比如符号表。Data共三个SegmentTEXTDATALINKEDIT。其中TEXTDATA对应一个或多个SectionLINKEDIT没有Section,需要配合LC_SYMTAB来解析symbol tablestring table。这些里面是Mach-O的主要数据
    • 详细参考Apple 操作系统可执行文件 Mach-O

dyld::_main

  • 进入_main,通过观察发现,_main返回一个result,我们可以从result着手进行分析,大致分为几个步骤:

环境变量配置

  • 配置环境,获取cpu等架构信息

截屏2021-07-14 08.56.48.png

截屏2021-07-14 08.57.37.png

共享缓存

  • 检查共享缓存是否开启,未开启则加载共享缓存 截屏2021-07-14 09.05.45.png

主程序初始化

  • 加载可执行文件,并生ImageLoaderMachO成实例 截屏2021-07-14 09.18.05.png

插入动态库

  • 遍历DYLD_INSERT_LIBRARIES(插入的动态库),并调用loadInsertedDylib加载

截屏2021-07-14 09.22.16.png

链接主程序

  • 调用link函数链接可执行文件 截屏2021-07-14 09.24.47.png

链接动态库

  • 链接插入的动态库 截屏2021-07-14 09.28.26.png

弱符号绑定

截屏2021-07-14 09.31.25.png

执行初始化方法

截屏2021-07-14 09.45.59.png

寻找main

  • Load Command中读取LC_MAIN,如果找到则使用libSystemHelper调用main(),如果没找到,dyld需要让start在程序中启动main() 截屏2021-07-14 09.48.08.png

下面将主要介绍主程序初始化执行初始化

dyld::_main的 主程序初始化

instantiateFromLoadedImage

  • 主程序的初始化是调用instantiateFromLoadedImage方法,进入方法查看:
//内核映射在主可执行文件之前,dyld得到控制。我们需要为已经映射在主可执行文件中的ImageLoader*创建一个ImageLoader
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
    ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
    addImage(image);
    return (ImageLoaderMachO*)image;
}
  • 该方法是通过instantiateMainExecutable创建一个ImageLoader实例,然后addImage,再强转成ImageLoaderMachO类型返回

instantiateMainExecutable

  • 进入instantiateMainExecutable方法:
// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
    sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
    bool compressed;
    unsigned int segCount;
    unsigned int libCount;
    const linkedit_data_command* codeSigCmd;
    const encryption_info_command* encryptCmd;
    // 确定这个macho文件是否具有经典的或压缩的LINKEDIT以及它的段数
    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
}
  • 这里的主要作用是创建主程序的映射,返回一个ImageLoader类型对象,也就是image镜像文件。sniffLoadCommands函数是确定machO文件的相关信息

dyld::_main的 执行初始化

initializeMainExecutable

  • 执行初始化是调用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]);
}
  • 先通过sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0])执行主可执行文件的一些依赖文件,都执行完后再去执行主可执行文件

runInitializers

  • 进入runInitializers
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);
}
  • 通过观察猜测核心代码是processInitializersnotifyBatch

processInitializers

  • 进入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.
	// 对images列表中的所有镜像递归init,构建一个未初始化向上依赖的新列表
	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);
}
  • 这里每个image都调用recursiveInitialization方法,
recursiveInitialization
  • 全局搜索定位到该函数的实现: 截屏2021-07-14 11.21.18.png
  • 这里面主要是两个函数,一个是notifySingle,另一个是doInitialization,先来看看第一个
notifySingle
  • 搜索notifySingle

截屏2021-07-14 11.38.34.png

  • 通过分析定位到方法的重点(*sNotifyObjCInit)(image->getRealPath(), image->machHeader()),再来看看sNotifyObjCInit,它是一个全局的_dyld_objc_notify_init类型变量:
static _dyld_objc_notify_init		sNotifyObjCInit;
  • 然后我们去看看它是在什么地方赋值的,全局搜索sNotifyObjCInit,于是定位到了被赋值的位置:
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;

	// call 'mapped' function with all images mapped so far
	try {
		notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
	}
	catch (const char* msg) {
		// ignore request to abort during registration
	}

	// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
	for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
		ImageLoader* image = *it;
		if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
			dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
			(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		}
	}
}
  • 也就是说调用registerObjCNotifiers方法时,sNotifyObjCInit会被赋值,再继续搜索registerObjCNotifiers
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,但此时dyld源码中并没有它的调用,再去objc源码中查看:
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}
  • 由此可见sNotifyObjCInit的赋值实质上是load_images,再来看看load_images截屏2021-07-14 15.05.08.png

  • 再进入call_load_methods方法,里面有个do-while循环调用call_load_methods

截屏2021-07-14 15.09.00.png

  • 分别进入call_load_methodscall_category_loads方法:
    • call_load_methods截屏2021-07-14 15.13.56.png
    • call_category_loads

截屏2021-07-14 18.39.20.png

  • 这里就能看到cls分类调用load方法,也就是load_images方法就是调用+load方法,刚好走完前面的堆栈信息: 截屏2021-07-12 15.00.17.png

总结:+load方法的流程为:_dyld_start -> dyldbootstrap::start -> dyld::_main -> initializeMainExecutable -> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> notifySingle -> sNotifyObjCInit -> load_images -> +load

  • 现在知道了+load的流程,但是上面提到的_objc_init是什么时候调用,接下来继续去探究
doInitialization
  • objc源码的_objc_init处打个断点,然后bt查看堆栈: 截屏2021-07-14 17.49.40.png
  • 在堆栈里发现了_objc_init的调用,doInitialization -> doModInitFunctions -> libSystem_initializer -> libdispatch_init -> _os_object_init -> _objc_init,下面今天反向推导
    1. 先看_os_object_init,它是在libdispatch源码中,
void
_os_object_init(void)
{
    _objc_init();
    ...
}

然后就找到_os_object_init调用_objc_init

  • 2. 再搜索libdispatch_init
void
libdispatch_init(void)
{
    ...
    _os_object_init();
    ...
}

找到了libdispatch_init调用_os_object_init,然后根据堆栈提示继续看

  • 3. libSystem_initializer是在libSystem源码中,在源码中搜索方法:
static void
libSystem_initializer(int argc,
		      const char* argv[],
		      const char* envp[],
		      const char* apple[],
		      const struct ProgramVars* vars)
{
    ...
    libdispatch_init();
    ...
}
  • 不出意外的在libSystem_initializer找到了libdispatch_init方法,再继续看堆栈,doModInitFunctionsdoInitialization都在dyld源码,我们继续去查看dyld源码:
    1. dyld源码搜索doModInitFunctions 截屏2021-07-14 18.17.43.png
      这里doModInitFunctions加载了所有C++的库
    1. 在搜索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);
}

然后在然后我们得知doInitialization调用了doModInitFunctions,在搜索的时候,发现这里这里调用doInitialization方法:

截屏2021-07-14 18.24.02.png 此时此刻主体流程形成了一个闭环。notifySingle是发送消息,registerObjCNotifiers是添加监听

总结
  • +load方法的流程为:_dyld_start -> dyldbootstrap::start -> dyld::_main -> initializeMainExecutable -> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> notifySingle -> sNotifyObjCInit -> load_images -> +load
  • _objc_init调用流程为:doInitialization -> doModInitFunctions -> libSystem_initializer -> libdispatch_init -> _os_object_init -> _objc_init

main函数

我们知道_dyld_start是个汇编函数以模拟器为例,然后再看看: 截屏2021-07-14 23.25.07.png

  • 分析汇编得到,rax中又main相关信息,怎么验证呢,我们知道load函数之后会走C++函数,在main中定义一个C++函数:
__attribute__((constructor)) void wushuang_func(){
    printf("来了 : %s \n",__func__);
}

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);
}

再来查看打印结果: 截屏2021-07-15 07.16.30.png
结果执行顺序为+load > C++ > main,然后在C++函数打个断点,根据汇编点击Step Over往下走走到_dyld_start,过掉dyldbootstrap::start,然后读取寄存器register read截屏2021-07-15 07.21.04.png
此刻也就印证了rax存的是main,而汇编最后走的是jmpq *%rax,也就是跳到main函数,此时就走完了从_dyld_start到main函数的全过程

dyld加载流程

截屏2021-07-15 11.33.25.png

总结

  • 本文通过粗略的分析dyld流程,主要传达一种探索思想,有时候分析问题到一定地方就卡住,常常拿他没办法,这个时候也可以试试反向推导,可能会获得意想不到的效果~