dyld加载应用启动原理详解
Qinz
我们都知道APP的入口函数是main(),而在main()函数调用之前,APP的加载过程是怎么的呢?接下来我们一起来分析APP的加载流程。
一、利用断点进行追踪
首先我们创立一个工程,什么代码都不写,在main()函数处进行断点,会看到情况如下图:
01
- 通过上图我们可以看到,在调用堆栈中,我们只看到了star和main,并开启了主线程,其它的什么都看不到。那要怎样才能看到调用堆栈详细点的信息了?我们都知道,有一个方法比main()函数调用更早,那就是load()函数,此时在控制器中写一个load函数,并断点运行,如下图:
02
- 通过上图,我们看到了比较详细的函数调用顺序,从第13行的_dyld_start到第3行的dyld:notifySingle,频率出现最多的就是这个dyld的家伙,那么dyld是什么?它在做什么?简单来说dyld是一个动态链接器,用来加载所有的库和可执行文件。接下来我们将通过图2的调用关系,去追踪dyld究竟在什么?
二、 dyld加载流程分析
1. 首先下载dyld源码。
2. 打开dyld源码工程,根据图2的第12行dyldbootstrap:start为关键字搜索dyldbootstrap中调用的start方法,如下图:
03
3. 该方法源码如下,接下来我们对该方法的重点部分进行分析:
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], intptr_t slide){ // 读取macho文件的头部信息 const struct macho_header* dyldsMachHeader = (const struct macho_header*)(((char*)&_mh_dylinker_header)+slide); // 滑块,设置偏移量,用于重定位 if ( slide != 0 ) { rebaseDyld(dyldsMachHeader, slide); } uintptr_t appsSlide = 0; // 针对偏移异常的监测 dyld_exceptions_init(dyldsMachHeader, slide); // 初始化machO文件 mach_init(); // 设置分段保护,这里的分段下面会详情,属于machO文件格式 segmentProtectDyld(dyldsMachHeader, slide); //环境变量指针 const char** envp = &argv[argc+1]; // 环境变量指针结束的设置 const char** apple = envp; while(*apple != NULL) { ++apple; } ++apple; // 在dyld中运行所有c++初始化器 runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple); // 假如主可执行文件被链接-pie,那么随机分配它的加载地址 if ( appsMachHeader->flags & MH_PIE ) appsMachHeader = randomizeExecutableLoadAddress(appsMachHeader, envp, &appsSlide); // 传入头文件信息,偏移量等。调用dyld的自己的main函数(这里并不是APP的main函数)。 return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple);}
3.1 函数的参数中我们看到有一个macho_header的参数,这是一个什么东西呢?Mach-O其实是Mach Object文件格式的缩写,是mac以及iOS中的可执行文件格式,并且有自己的文件格式目录,苹果给出的mach文件如下图:
04
- 3.2 首先我们点击进入macho_header这个结构体看它的定义如下:
struct mach_header_64 { uint32_t magic; /* 区分系统架构版本 */ cpu_type_t cputype; /*CPU类型 */ cpu_subtype_t cpusubtype; /* CPU具体类型 */ uint32_t filetype; /* 文件类型 */ uint32_t ncmds; /* loadcommands 条数,即依赖库数量*/ uint32_t sizeofcmds; /* 依赖库大小 */ uint32_t flags; /* 标志位 */ uint32_t reserved; /* 保留字段,暂没有用到*/};
3.3 这里macho_header就是读取macho文件的头部信息,header里面会包含该二进制文件的少量信息:如字节顺序、架构类型、加载指令的数量等。可以用来快速确认少量信息,比方当前文件用于32位还是64位、文件的类型等。那么macho文件在哪里可以找到了呢?如下图,我们找到macho,并用MachOView来查看:
05
3.4 上面那个黑不溜秋的就是macho文件,是一个可执行文件,我们来看下它加载的头部信息有哪些?这些信息将会被传到下一个函数中。这里简单说下Number of Load Commands数字为22,代表22个库文件,在LoadCommands有加载库的对应关系,Section中就是我们的数据DATA,包含了代码,常量等数据。
06
3.5 小结:star函数主要就是先读取macho文件的头部信息,设置虚拟地址偏移,这里的偏移主要用于重定向。接下来就是初始化macho文件,用于后续加载库文件和DATA数据,再运行C++的初始化器,最后进入dyly的主函数。
4. 接下来我们继续追踪,根据图2的调用堆栈,我们知道在dyldbootstrap:star方法中调用了dyld::_main方法,也就是我们上面说到的进入dyld的主程序,如下图:
07
- 4.1 我们进入方法继续追踪,截取部分源如下图,我们发现这里有几个if判断,此处是在设置环境变量,也就是假如设置了这些环境变量,Xcode就会在控制台打印相关的详细信息:
if ( sProcessIsRestricted ) pruneEnvironmentVariables(envp, &apple); else checkEnvironmentVariables(envp, ignoreEnvironmentVariables); if ( sEnv.DYLD_PRINT_OPTS ) printOptions(argv); if ( sEnv.DYLD_PRINT_ENV ) printEnvironmentVariables(envp); getHostInfo();
- 4.2 当我们设置了相关的环境变量,此时Xcode就会打印程序相关的目录、客户级别、插入的动态库、动态库的路径等,演示图下图:
08
- 4.3 设置环境变量之后,接下来会调用getHostInfo()来获取machO头部获取当前运行架构的信息,函数代码如下:
static void getHostInfo(){#if 1 struct host_basic_info info; mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT; mach_port_t hostPort = mach_host_self(); kern_return_t result = host_info(hostPort, HOST_BASIC_INFO, (host_info_t)&info, &count); if ( result != KERN_SUCCESS ) throw "host_info() failed"; sHostCPU = info.cpu_type; sHostCPUsubtype = info.cpu_subtype;#else size_t valSize = sizeof(sHostCPU); if (sysctlbyname ("hw.cputype", &sHostCPU, &valSize, NULL, 0) != 0) throw "sysctlbyname(hw.cputype) failed"; valSize = sizeof(sHostCPUsubtype); if (sysctlbyname ("hw.cpusubtype", &sHostCPUsubtype, &valSize, NULL, 0) != 0) throw "sysctlbyname(hw.cpusubtype) failed";#endif}
- 4.4 接着往下看,这里会对macho文件进行实例化:
try { // 实例化主程序,也就是machO这个可执行文件 sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); sMainExecutable->setNeverUnload(); gLinkContext.mainExecutable = sMainExecutable; gLinkContext.processIsRestricted = sProcessIsRestricted; // 加载共享缓存库 checkSharedRegionDisable(); #if DYLD_SHARED_CACHE_SUPPORT if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) mapSharedCache(); #endif
- 4.5 进入实例化主程序代码如下,加载完毕后会返回一个ImageLoader镜像加载类,这是一个笼统类,用于加载特定可执行文件格式的类,对于程序中需要的依赖库、插入库,会创立一个对应的image对象,对这些image进行链接,调用各image的初始化方法等等,包括对runtime的初始化。
{ // isCompatibleMachO 是检查mach-o的subtype能否是当前cpu可以支持 if ( isCompatibleMachO((const uint8_t*)mh, path) ) { ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);//将image增加到imagelist。所以我们在Xcode使用image list命令查看的第一个便是我们的machO addImage(image); return image; } throw "main executable not a known format";}
4.6 使用image list命令演示如下图,看到的第一个0x000000010401c000地址就是macho这个可执行文件的地址。
09
- 4.7 对macho文件进行实例化后,会看到一个checkSharedRegionDisable()的方法,这里是在加载共享缓存库。这个共享缓存库是个什么东西呢? 其实我们可以了解为是系统公用的动态库(苹果禁止第三方使用动态库)。如我们最常用的UIKit框架就在共享缓存库中,举个例子,微信、QQ、支付宝、天猫等APP都会使用到UIKit这个框架,假如每个应用都加载UIKit,势必会导致内存紧张。所以实际是这些APP都会共享一套UIKit框架,应用中用到了对应了UIKit框架中的方法,dyld就会去拿对应的资源供给这些APP使用。
5. 插入库:我们继续看该方法中的剩余源码,这里将会加载所有插入库,逆向中的代码注入就是在这一步完成的,framework的详细代码注入流程请看我的这篇文章。这里有一个sAllImages.size()-1的操作,实际上是排除了主程序。
// 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. sInsertedDylibCount = sAllImages.size()-1;
6. 链接主程序:内部通过imageLoader的实例对象去调用link方法,递归加载所依赖的系统库和第三方库。
// link main executable gLinkContext.linkingMainExecutable = true; link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL)); gLinkContext.linkingMainExecutable = false; if ( sMainExecutable->forceFlat() ) { gLinkContext.bindFlat = true; gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding; } result = (uintptr_t)sMainExecutable->getMain();
7. 初始化函数
10
8. 运行初始化程序:
11
8.1 递归:加载我们所需要的依赖的系统库和第三方库。
12
9. notifySingle函数,这是一个与运行时建立联络的关键函数:
13
- 9.1 我们发现notifySingle这个函数中调用了load_images方法,点进去发现这是一个函数指针,里面并没有找到load_images的调用,通过对dyld文件的全局搜索,也没有发现。所以此时我们推断它是在运行时调用的,正好objc运行时代码也是开源的,接下来我们下载objc源码进行分析。
void (*notifySingle)(dyld_image_states, const ImageLoader* image);
- 9.2 在objc_init中我们会发现调用,这里load_images。
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
14
- 9.3 在load_images中完成call_load_methods的调用,这里就是加载所有类文件及分类文件的load方法:
load_images(const char *path __unused, const struct mach_header *mh){ // 假如这里没有+load方法,则返回时不带锁 if (!hasLoadMethods((const headerType *)mh)) return; recursive_mutex_locker_t lock(loadMethodLock); // 发现load方法 { mutex_locker_t lock2(runtimeLock); prepare_load_methods((const headerType *)mh); } // 加载所有load方法 call_load_methods();}
- 9.4 call_load_methods方法调用,在call_load_methods中,通过doWhile循环来调用call_class_loads加载每个类的load方法,而后再加载分类的loads方法。
void call_load_methods(void){ static bool loading = NO; bool more_categories; loadMethodLock.assertLocked(); // Re-entrant calls do nothing; the outermost call will finish the job. if (loading) return; loading = YES; void *pool = objc_autoreleasePoolPush(); do { // 1. 循环调用所有类文件的laod方法 while (loadable_classes_used > 0) { call_class_loads(); } // 2.调用所有分类方法 more_categories = call_category_loads(); // 3. Run more +loads if there are classes OR more untried categories } while (loadable_classes_used > 0 || more_categories); objc_autoreleasePoolPop(pool); loading = NO;}
9.5 根据上面的调用顺序,我们知道是先加载类文件中的load方法,而后再加载分类文件中的load方法,演示如图:
15
10. 在调用完notifySigin后,我们发现继续调用了doInitialization,doModInitFunctions会调用machO文件中_mod_init_func段的函数,也就是我们在文件中所定义的全局C++构造函数。
// let objc know we are about to initalize this imagefState = dyld_image_state_dependents_initialized;oldState = fState;context.notifySingle(dyld_image_state_dependents_initialized, this);// initialize this imagethis->doInitialization(context);
10.1 所以通过上述代码的调用顺序我们知道先类文件load,再分类文件load,而后再是C++构造函数,最后就进入了我们的main主程序!演示如下:
16
通过上面的分析,我们从断点开始,查看方法的堆栈调用顺序,一步一步追踪dyld的加载流程,也就将main函数调用前的神秘面纱揭露无疑,你也可以根据上述的步骤自己动手追踪APP的加载过程,这样会更加印象深刻!
总结:main()函数调用之前,其实是做了很多准备工作,主要是dyld这个动态链接器在负责,核心流程如下:
1. 程序执行从_dyld_star开始
- 1.1. 读取macho文件信息,设置虚拟地址偏移量,用于重定向。
- 1.2. 调用dyld::_main方法进入macho文件的主程序。
2. 配置少量环境变量
- 2.1. 设置的环境变量方便我们打印出更多的信息。
- 2.1. 调用getHostInfo()来获取machO头部获取当前运行架构的信息。
3. 实例化主程序,即macho可执行文件。
4. 加载共享缓存库。
5. 插入动态缓存库。
6. 链接主程序。
7. 初始化函数。
- 7.1. 经过一系列的初始化函数最终调用notifSingle函数。
- 7.2. 此回调是被运行时_objc_init初始化时赋值的一个函数load_images
- 7.3. load_images里面执行call_load_methods函数,循环调用所用类以及分类的load方法。
- 7.4. doModInitFunctions函数,内部会调用全局C++对象的构造函数,即_ _ attribute_ _((constructor))这样的函数。
8. 返回主程序的入口函数,开始进入主程序的main()函数。
我是Qinz,希望我的文章对你有帮助。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » dyld加载应用启动原理详解