前言
在 iOS 开发中,我们看到的程序入口都是 main.m 里面的 main 函数,因此我们一般会误以为程序是从这个函数开始执行的。但其实程序在执行 main 函数之前就已经做了相当多的事情,比如我们熟知的 +load
方法和 constructor
构造函数,那么 main 函数执行之前到底都发生了什么?
本文会循着调用堆栈的脉络,从源码出发,整理出程序执行的整体流程。
抛出问题
File -> New -> Project -> Single View App 创建一个工程,在 ViewController
类中添加 +load
方法并增加一个断点
运行程序,观察程序调用堆栈
调用堆栈
从程序调用堆栈可以看到,程序加载是通过 dyld 完成的,那么我们接下来就来看看 dyld 到底都做了什么。
dyld 简介
dyld(dynamic loader),动态链接器,广泛使用于 Apple 的各种操作系统中,作用是加载一个进程所需要的 image,dyld 是开源的。
我们的程序都不可避免会使用到系统动态库(UIKit/Foundation),不可能在每个程序加载时都去加载所有的系统动态库,为了优化程序启动速度和利用动态库缓存,iOS 系统采用了共享缓存(Shared Cache)技术。dyld 缓存在 iOS 系统中,默认在 /System/Library/Caches/com.apple.dyld/
目录下,设备在连接到 Xcode 时会自动提取系统库到 /Users/Vernon/Library/Developer/Xcode/iOS DeviceSupport/
目录下。
共享缓存在系统启动后被加载到内存中,当有新的程序加载时会先到共享缓存中查找。如果找到直接将共享缓存中的地址映射到目标进程的内存地址空间,提高了程序加载的效率。
加载流程
这里先给出 dyld 加载程序的流程,方便从整体把握 dyld 的脉络
加载流程
源码分析
1. 设置上下文信息、环境变量
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
...
// 1.1 设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
...
// 1.2 检查环境变量
checkEnvironmentVariables(envp);
...
// 1.3 获取当前运行架构的信息
getHostInfo(mainExecutableMH, mainExecutableSlide);
...
首先,调用 setContext
设置上下文信息(1.1),主要就是将 argc
、argv
等参数都存储下来给后续流程使用,同时设置后面需要调用的函数。
然后调用 checkEnvironmentVariables
,根据环境变量设置相应的值(1.2),checkEnvironmentVariables
方法会调用 processDyldEnvironmentVariable
处理并设置环境变量,我们看下 processDyldEnvironmentVariable
的部分代码
void processDyldEnvironmentVariable(const char* key, const char* value, const char* mainExecutableDir)
{
else if ( strcmp(key, "DYLD_INSERT_LIBRARIES") == 0 ) {
sEnv.DYLD_INSERT_LIBRARIES = parseColonList(value, NULL);
#if SUPPORT_ACCELERATE_TABLES
sDisableAcceleratorTables = true;
#endif
sEnv.hasOverride = true;
}
else if ( strcmp(key, "DYLD_PRINT_OPTS") == 0 ) {
sEnv.DYLD_PRINT_OPTS = true;
}
else if ( strcmp(key, "DYLD_PRINT_ENV") == 0 ) {
sEnv.DYLD_PRINT_ENV = true;
}
...
}
是不是觉得有点熟悉,DYLD_PRINT_OPTS
、DYLD_PRINT_ENV
不正是我们平时在 Xcode 中设置的环境变量吗?环境变量的注解可以查看这里。
然后调用 getHostInfo
获取当前运行架构的信息(1.3),代码如下:
static void getHostInfo(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide)
{
#if CPU_SUBTYPES_SUPPORTED
#if __ARM_ARCH_7K__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7K;
#elif __ARM_ARCH_7A__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7;
#elif __ARM_ARCH_6K__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V6;
#elif __ARM_ARCH_7F__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7F;
#elif __ARM_ARCH_7S__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7S;
...
2. 加载可执行文件,生成 ImageLoader 实例对象
调用 instantiateFromLoadedImage
函数来实例化一个 ImageLoader
对象,代码如下:
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";
}
instantiateFromLoadedImage
函数先调用 isCompatibleMachO
函数判断文件的架构是否和当前的架构兼容,然后调用 instantiateMainExecutable
函数来加载文件生成 image
实例,最后将 image
添加到全局的数组 sAllImages
中。接下来我们看看关键的 instantiateMainExecutable
函数:
// 从可执行文件创建 image
// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
// 获取 Load Command 的相关信息,并对其进行校验
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
}
instantiateMainExecutable
函数首先通过 sniffLoadCommands
函数来获取 Load Command 的相关信息,并对其进行校验。然后根据当前 Mach-O 是普通类型还是压缩的,使用不同的 ImageLoaderMachO
子类进行初始化。
最后将 image
添加到全局的数组 sAllImages
中,代码如下:
static std::vector<ImageLoader*> sAllImages;
static void addImage(ImageLoader* image)
{
// add to master list
allImagesLock();
sAllImages.push_back(image);
allImagesUnlock();
...
}
3. 加载共享缓存动态库
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
// 共享缓存
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
mapSharedCache();
}
bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
results->loadAddress = 0;
results->slide = 0;
results->errorMessage = nullptr;
if ( options.forcePrivate ) {
// 1. 加载到当前进程
// mmap cache into this process only
return mapCachePrivate(options, results);
}
else {
// fast path: when cache is already mapped into shared region
bool hasError = false;
if ( reuseExistingCache(options, results) ) {
// 3. 之前已经被加载过了 --> 不做处理
hasError = (results->errorMessage != nullptr);
} else {
// 2. 第一次加载 --> 加载
// slow path: this is first process to load cache
hasError = mapCacheSystemWide(options, results);
}
return hasError;
}
#endif
}
首先通过 checkSharedRegionDisable
函数检查共享缓存的禁用状态,将结果写入 gLinkContext.sharedRegionMode
中(iOS 下不会被禁用)。然后通过 mapSharedCache
--> loadDyldCache
加载共享缓存库,加载共享缓存总共分三种情况:
- 仅加载到当前进程
- 第一次加载,则去做加载操作
mapCacheSystemWide
- 之前已经加载过了,则不做任何操作
4. 加载所有插入的库
遍历 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);
}
DYLD_INSERT_LIBRARIES
环境变量也是很多越狱插件的原理所在了。
5. 链接主程序
// link main executable
gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
if ( mainExcutableAlreadyRebased ) {
// previous link() on main executable has already adjusted its internal pointers for ASLR
// work around that by rebasing by inverse amount
sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
}
#endif
// 链接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
// 链接插入的动态库
// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
// 注册符号插入
image->registerInterposing(gLinkContext);
}
}
6. 链接所有插入的库,进行符号替换
// 链接插入的动态库
// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
// 注册符号插入
image->registerInterposing(gLinkContext);
}
}
...
// 弱符号绑定
sMainExecutable->weakBind(gLinkContext);
gLinkContext.linkingMainExecutable = false;
对 sAllImages
(除了主程序)中的库调用 link
进行链接操作,然后调用 registerInterposing
注册符号替换。我们看下 link
函数:
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
(*context.setErrorStrings)(0, NULL, NULL, NULL);
uint64_t t0 = mach_absolute_time();
// 递归加载动态库
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
// we only do the loading step for preflights
if ( preflightOnly )
return;
uint64_t t1 = mach_absolute_time();
context.clearAllDepths();
// 对 image 进行排序
this->recursiveUpdateDepth(context.imageCount());
__block uint64_t t2, t3, t4, t5;
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
t2 = mach_absolute_time();
// 递归 rebase
this->recursiveRebaseWithAccounting(context);
context.notifyBatch(dyld_image_state_rebased, false);
t3 = mach_absolute_time();
if ( !context.linkingMainExecutable )
// 递归绑定符号表
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
t4 = mach_absolute_time();
if ( !context.linkingMainExecutable )
this->weakBind(context);
t5 = mach_absolute_time();
}
...
可以看到,link
函数中有各种 recursive
的函数,这就是所谓的递归进行符号绑定的过程。
link
函数执行完毕之后 ,会调用 weakBind
函数进行弱绑定 , 也就是说弱绑定一定发生在其他库链接完成之后 .
根据这里的代码可以知道,在 Mach-O 文件中向 __DATA,__interpose
中写要替换的函数和自定义的函数时,就能对懒加载和非懒加载表中的符号进行替换。
7. 调用初始化方法
接下来,_main
函数会调用 initializeMainExecutable
函数运行主程序,根据前面的函数调用栈,initializeMainExecutable
--> runInitializers
--> processInitializers
--> recursiveInitialization
--> notifySingle
,然后发现 commad 点击没办法继续跳转了,按照调用栈来说,下一步应该是调用 load_image
,但是全局搜索都找不到这个 load_image
函数,那么 load_image
到底在哪里呢?
我们看下 notifySingle
函数里有这么一句:
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
此处调用了 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;
...
可以看到,这个接口是提供给 objc runtime 调用的,我们下载 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();
cache_init();
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
load_images
函数就是在这里注册的,所以 dyld 的 sNotifyObjCInit
调用的就是 objc runtime 中的 load_images
函数,load_images
会调用所有的 +load
函数。
8. main 函数
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
从 Load Command 中读取 LC_MAIN
入口,然后跳到入口执行,终于来到了我们熟悉的 main
函数。
结语
限于篇幅,本文只是从源码的角度梳理了 dyld 的加载流程,像加载链接所有动态库、rebese、rebind 这些比较细节的部分没有深入探讨,之后再写一篇文章专门讲一下这些细节。
网友评论