我们都知道APP的入口函数是main(),而在main()函数调用之前,APP的加载过程是怎样的呢?接下来我们一起来分析APP的加载流程。
一、利用断点进行追踪
-
首先我们创建一个工程,什么代码都不写,在main()函数处进行断点,会看到情况如下图:
- 通过上图我们可以看到,在调用堆栈中,我们只看到了star和main,并开启了主线程,其它的什么都看不到。那要怎么才能看到调用堆栈详细点的信息了?我们都知道,有一个方法比main()函数调用更早,那就是load()函数,此时在控制器中写一个load函数,并断点运行,如下图:
- 通过上图,我们看到了比较详细的函数调用顺序,从第13行的_dyld_start到第3行的dyld:notifySingle,频率出现最多的就是这个dyld的家伙,那么dyld是什么?它在做什么?简单来说dyld是一个动态链接器,用来加载所有的库和可执行文件。接下来我们将通过图2的调用关系,去追踪dyld到底在什么?
二、 dyld加载流程分析
1. 首先下载dyld源码。
2. 打开dyld源码工程,根据图2的第12行dyldbootstrap:start为关键字搜索dyldbootstrap中调用的start方法,如下图:
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文件如下图:
- 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来查看:
-
3.4 上面那个黑不溜秋的就是macho文件,是一个可执行文件,我们来看下它加载的头部信息有哪些?这些信息将会被传到下一个函数中。这里简单说下Number of Load Commands数字为22,代表22个库文件,在LoadCommands有加载库的对应关系,Section中就是我们的数据DATA,包含了代码,常量等数据。
3.5 小结:star函数主要就是先读取macho文件的头部信息,设置虚拟地址偏移,这里的偏移主要用于重定向。接下来就是初始化macho文件,用于后续加载库文件和DATA数据,再运行C++的初始化器,最后进入dyly的主函数。
4. 接下来我们继续追踪,根据图2的调用堆栈,我们知道在dyldbootstrap:star方法中调用了dyld::_main方法,也就是我们上面说到的进入dyld的主程序,如下图:
- 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就会打印程序相关的目录、用户级别、插入的动态库、动态库的路径等,演示图下图:
- 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这个可执行文件的地址。
-
4.7 对macho文件进行实例化后,会看到一个checkSharedRegionDisable()的方法,这里是在加载共享缓存库。这个共享缓存库是个什么东西呢? 其实我们可以理解为是系统公用的动态库(苹果禁止第三方使用动态库)。如我们最常用的UIKit框架就在共享缓存库中,举个例子,微信、QQ、支付宝、天猫等APP都会使用到UIKit这个框架,如果每个应用都加载UIKit,势必会导致内存紧张。所以实际是这些APP都会共享一套UIKit框架,应用中用到了对应了UIKit框架中的方法,dyld就会去拿对应的资源供给这些APP使用。如下图展示了越狱手机中System的library中framework的库,也证明了这一点:
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. 初始化函数
8. 运行初始化程序:
-
8.1 递归:加载我们所需要的依赖的系统库和第三方库。
9. notifySingle函数,这是一个与运行时建立联系的关键函数:
- 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);
- 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方法,演示如图:
10. 在调用完notifySigin后,我们发现继续调用了doInitialization,doModInitFunctions会调用machO文件中_mod_init_func段的函数,也就是我们在文件中所定义的全局C++构造函数。
// let objc know we are about to initalize this image
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this);
// initialize this image
this->doInitialization(context);
-
10.1 所以通过上述代码的调用顺序我们知道先类文件load,再分类文件load,然后再是C++构造函数,最后就进入了我们的main主程序!演示如下:
通过上面的分析,我们从断点开始,查看方法的堆栈调用顺序,一步一步追踪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,希望我的文章对你有帮助。