iOS-16.程序启动流程

606 阅读7分钟

ios底层文章汇总

1 引入

image.png 新建工程,在viewContoll的+load函数中处设置断点,运行程序,可以看到程序的调用堆栈为下图所示 可知道_dyld_start的源码位于dylib

断点调试可知程序的执行过程是:load-->c++函数-->main c++函数:

__attribute__((constructor)) void kcFunc(){
    printf("来了 : %s \n",__func__);
}

为啥程序的执行过程为load>c++函数>main?\color{#FF0000}{为啥程序的执行过程为load -> c++函数 -> main?}

2 程序编译过程

静态库

在编译阶段,会将汇编生成的目标与引用的库一起链接打包到可执行文件当中,静态库直接拷贝一份复制到目标程序里

  • 静态库的链接是在编译期完成的,编译完成后,目标程序没有外部依赖,直接就可以运行运行速度快
  • 使可执行文件变大,每个引用位置都有一份静态库的拷贝。
  • 一个静态库更新了,所有使用它的应用程序都需要重新编译、发布

动态库

链接时,目标程序只会存储指向动态库的引用,程序运行时,用一份共享库的实例加载进来。eg:UIKit,Foundation,CoreFoundation,libdispatch,libSystem,libobjc

  • 相对于静态库减少APP的大小
  • 所有引用动态库的文件共享一份动态库,节约资源和空间
  • 可以通过更新动态库,达到更新程序的目的
  • 但动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行

程序编译过程

源文件:.h、.m、.mm、.cpp等文件

预处理:宏替换,删除注释,展开头文件,生成.i文件

编译:将.i文件转换为汇编语言,生成.s文件

汇编:将汇编文件转换为目标文件,生成.o文件

链接:对.o文件中引用其他库的地方进行链接,生成可执行文件

链接部分在ios中是由dyld负责

3 动态链接器:dyld(the dynamic link editor)

APP的加载过程中,在app被编译打包成可执行文件格式的Mach-O文件后,交由dyld负责连接,加载程序:

3.1 dyld源码找到入口

全局搜索_dyld_start:

  • dyldStartup.s 文件中找到了入口

  • 看汇编注释,可知调用dyldbootstrap 命名空间下的start 方法call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)

3.2 dyldbootstrap::start

全局搜索dyldbootstrap找到命名空间,再查找start方法,关注重点返回值 调用了dyld的main函数

dyldbootstrap::start中,主要过程为:

  • 使用全局变量之前,对dyld进行rebase操作,以修复为 real pointer 来运行;
  • 设置参数和环境变量;
  • 读取 app二进制文件 Mach-O 的header 得到偏移量 appSlide,然后调用dyld 命名空间下的_main 方法。
macho_header是Mach-O的头部
    
dyld加载的文件就是Mach-O类型文件
    
Mach-O类型是`可执行文件类型`,由四部分组成:`Mach-O头部``Load Command``section``Other Data`

3.3 分析 dyld::_main的思路

  • 查看返回值result,在上面什么时候赋值的

  • 找到result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN()result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();

  • 搜索sMainExecutable,什么时候初始化的?

  • 找到sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath)

  • 查看instantiateFromLoadedImage源码 ,通过调用ImageLoaderMachO::instantiateMainExecutable方法创建一个ImageLoader实例对象

  • 进入instantiateMainExecutable源码:为主程序创建映像,返回一个ImageLoader类型的image对象,其中sniffLoadCommands函数获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验


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* 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
}

3.3 dyld::_main主要功能

  • 3.3.1 环境的配置:根据环境变量设置相应的值以及获取当前运行架构,为可执行文件的加载做准备工作; checkEnvironmentVariables(envp);

defaultUninitializedFalbackPaths(envp);

  • 3.3.2 共享缓存:映射共享缓存到当前进程的逻辑内存空间;

checkSharedRegionDisable验证共享缓存的路径

mapSharedCache();

  • 3.3.3 将dyld本身添加到UUID表 :addDyldImageToUUIDList();

  • 3.3.4 初始化主程序:实例化主程序instantiateFromLoadedImage;

  • 3.3.5 插入动态库: 遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载

  • 3.3.6 链接主程序

  • 3.3.7 链接插入的动态库

  • 3.3.8 执行弱符号绑定(weakBind)

  • 3.3.9 执行初始化方法;

  • 3.3.10 查找程序入口 从Load Command读取LC_MAIN入口,如果没有,就读取LC_UNIXTHREAD。返回main( )--> notifyMonitoringDyldMain().

4 初始化主程序 initializeMainExecutable:为主程序实例化为ImageLoader对象

这一步主要是调用所有image的Initalizer方法进行初始化。先为所有插入并链接完成的动态库执行初始化操作,再为主程序可执行文件执行初始化操作

具体流程为: runInitializers --> processInitializers --> recursiveInitialization

ImageLoader::recursiveInitialization

全局搜索notifySingle:分析源码可知,通过 notifySingle 函数对sNotifyObjCInit 进行函数调用

获取镜像文件的真实地址 *sNotifyObjCInit)(image->getRealPath(), image->machHeader() ,而 sNotifyObjCInit通过 registerObjCNotifiers 进行赋值

继而找到registerObjCNotifiers 的 调用函数 _dyld_objc_notify_register

_dyld_objc_notify_register 函数是供 objc runtime 使用的,objc4-781的源码libobjc.A.dylib 库里_objc_init函数中找到其调用

搜索_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit赋值的就是objc中的load_images,而load_images会调用所有的+load方法,notifySingle是一个回调函数

load函数加载

下面我们进入load_images的源码看看其实现,以此来证明load_images中调用了所有的load函数

  • 通过objc源码中_objc_init源码实现,进入load_images的源码实现

image.png

  • 进入call_load_methods源码实现,可以发现其核心是通过do-while循环调用+load方法

image.png

  • 进入call_class_loads源码实现,了解到这里调用的load方法证实我们前文提及的类的load方法

call_class_loads源码实现

所以,load_images调用了所有的load函数,以上的源码分析过程正好对应堆栈的打印信息

堆栈信息

image.png

sMainExcuatable的initializer方法被调用前dyld会通知runtime进行类结构初始化,然后再通知调用load方法,这些目前还发生在main函数前,所有的依赖库的lnitializer都调用完后,dyld::main 函数会返回程序的main()函数地址,main函数被调用,从而代码来到了我们熟悉的程序入口

但由于OC的懒加载机制,依赖库多数都是在使用时才进行bind,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才加载

那么 _objc_init 又是如何被调用的呢?-->doInitialization

回退到recursiveInitialization递归函数的源码实现,发现还有一个初始化镜像函数doInitialization

  • doInitialization源码

  • doInitialization源码有两个函数调用:doImageInitdoModInitFunctions

  • doImageInit源码:循环加载方法的调用

  • doModInitFunctions源码:加载了所有的Cxx文件

在 doModInitFunctions之后 会 先执行libSystem_initializer,保证系统库优先初始化完毕,在这里初始化libdispatch_init,进而在_os_object_init 中 调用 _objc_init

5 总结:_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)

符号断点_objc_init查看调用栈:

image.png

为啥程序的执行过程为load>cXX函数>main?\color{#FF0000}{为啥程序的执行过程为load -> cXX函数 -> main?}

+load方法调用: load_images --> call_load_methods -- call_class_loads[load_method]

image.png

Cxx函数调用: doInitialization --> doModInitFunctions 调用所有的CXX函数

在CXX函数中断点,bt打印堆栈

image.png

main 函数是固定的格式,被编译到内存里的,在dyld_start源码中可查看到走完dyldbootstrap::start后才调用main

image.png

所以,综上所述,最终dyld加载流程,如下图所示,图中也诠释了前文中的问题:为什么是load-->Cxx-->main的调用顺序

image.png