在前面的文章中介绍过,app启动过程中,首先是操作系统内核进行一些处理,比如新建进程,分配内存等。在iOS/Mac OS系统中,操作系统内核是XNU。在XNU完成相关的工作后,会将控制权交给dyld。dyld,即动态链接器,用于加载动态库。dyld是运行在用户态的,从XNU到dyld,完成了一次内核态到用户态的切换。那么,后续dyld做了哪些事情呢?幸运的是,dyld是开源的,我们通过分析dyld的源码,来看一下dyld在app启动过程中做了哪些工作。
dyld入口
在之前的文章中介绍过,dyld入口函数是__dyld_start,我们看一下__dyld_start里面做了那些操作。dyld中的部分源码是汇编语言,__dyld_start源码就是汇编。__dyld_start部分代码如下:
__dyld_start:
// 这里调用了dyldbootstrap::start()函数,此函数会完成动态库加载过程,并返回主程序main函数入口
bl __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
mov x16,x0 // save entry point address in x16
ldr x1, [sp]
cmp x1, #0
// LC_MAIN case, set up stack for call to main()
Lnew: mov lr, x1 // simulate return address into _start in libdyld.dylib
ldr x0, [x28, #8] // main param1 = argc
add x1, x28, #16 // main param2 = argv
add x2, x1, x0, lsl #3
add x2, x2, #8 // main param3 = &env[0]
mov x3, x2
__dyld_start内部调用了dyldbootstrap::start()函数,看一下dyldbootstrap::start()内部的实现:
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[],
intptr_t slide, const struct macho_header* dyldsMachHeader,
uintptr_t* startGlue)
{
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
if ( slide != 0 ) {
rebaseDyld(dyldsMachHeader, slide);
}
// 调用dyld中的_main()函数,_main()函数返回主程序的main函数入口,也就是我们App的main函数地址
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
查找App main函数地址的操作主要是在_main函数中,_main函数中做了较多的操作,看一下_main()函数是如何实现的。
_main()函数
_main()函数中代码比较多,做的事情也比较多。主要完成了上下文的建立,主程序初始化成ImageLoader对象,加载共享的系统动态库,加载依赖的动态库,链接动态库,初始化主程序,返回主程序main()函数地址。接下来分别看一下每个功能的具体实现。
instantiateFromLoadedImage
instantiateFromLoadedImage()函数主要是将主程序Mach-O文件转变成了一个ImageLoader对象,用于后续的链接过程。ImageLoader是一个抽象类,和其相关的类有ImageLoaderMachO,ImageLoaderMachO是ImageLoader的子类,ImageLoaderMachO又有两个子类,分别是ImageLoaderMachOCompressed和ImageLoaderMachOClassic。这几个类之间的关系如下:
在app启动过程中,主程序和其相关的动态库,最后都被转化成了一个ImageLoader对象。看一下instantiateFromLoadedImage中做的操作。
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// 检测mach-o header的cputype与cpusubtype是否与当前系统兼容
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
}
isCompatibleMachO主要是检测mach-o文件的cputype和cpusubtype是否与当前系统兼容,之后调用了instantiateMainExecutable()函数,看一下instantiateMainExecutable()函数的实现:
// 初始化ImageLoader
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
bool compressed;
unsigned int segCount;
unsigned int libCount;
// sniffLoadCommands主要获取加载命令中compressed的值(压缩还是传统)以及segment的数量、libCount(需要加载的动态库的数量)
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
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()函数根据Mach-O文件是否被压缩过,分别调用了ImageLoaderMachOCompressed::instantiateMainExecutable()和ImageLoaderMachOClassic::instantiateMainExecutable()。现在的Mach-O文件都是被压缩过的,因此我们只看一下ImageLoaderMachOCompressed::instantiateMainExecutable的实现。
// 根据macho_header,返回一个ImageLoaderMachOCompressed对象
ImageLoaderMachOCompressed* ImageLoaderMachOCompressed::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path,
unsigned int segCount, unsigned int libCount, const LinkContext& context)
{
ImageLoaderMachOCompressed* image = ImageLoaderMachOCompressed::instantiateStart(mh, path, segCount, libCount);
image->setSlide(slide);
image->disableCoverageCheck();
image->instantiateFinish(context);
image->setMapped(context);
return image;
}
通过这样一系列的操作,最终,一个Mach-O文件被转变为了一个ImageLoaderMachOCompressed对象。
mapSharedCache
mapSharedCache()负责将系统中的共享动态库加载进内存空间,比如UIKit就是动态共享库,这也是不同的App之间能够实现动态库共享的机制。不同App间访问的共享库最终都映射到了同一块物理内存,从而实现了共享动态库。
在Mac OS系统中,动态库共享缓存以文件的形式存放在/var/db/dyld目录下,更新共享缓存的程序是update_dyld_shared_cache,该程序位于 /usr/bin 目录下。update_dyld_shared_cache通常只在系统的安装器安装软件与系统更新时调用。接下来看一下mapSharedCache()的内部实现逻辑。
mapSharedCache()中的代码比较多,我们只看部分代码:
// 将本地共享的动态库加载到内存空间,这也是不同app实现动态库共享的机制
// 常见的如UIKit、Foundation都是共享库
static void mapSharedCache()
{
// _shared_region_***函数,最终调用的都是内核方法
if ( _shared_region_check_np(&cacheBaseAddress) == 0 ) {
// 共享库已经被映射到内存中
sSharedCache = (dyld_cache_header*)cacheBaseAddress;
if ( strcmp(sSharedCache->magic, magic) != 0 ) {
// 已经映射到内存中的共享库不能被识别
sSharedCache = NULL;
if ( gLinkContext.verboseMapping ) {
return;
}
}
}
else {
// 共享库没有加载到内存中,进行加载
// 获取共享库文件的句柄,然后进行读取解析
int fd = openSharedCacheFile();
if ( fd != -1 ) {
if ( goodCache ) {
// 做一个随机的地址偏移
cacheSlide = pickCacheSlide(mappingCount, mappings);
//使用_shared_region_map_and_slide_np方法将共享文件映射到内存,_shared_region_map_and_slide_np
// 内部实际上是做了一个系统调用
if (_shared_region_map_and_slide_np(fd, mappingCount, mappings, cacheSlide, slideInfo, slideInfoSize) == 0) {
// successfully mapped cache into shared region
sSharedCache = (dyld_cache_header*)mappings[0].sfm_address;
sSharedCacheSlide = cacheSlide;
}
}
}
}
}
mapSharedCache()中调用了内核中的一些方法,最终实际上是做了系统调用。mapSharedCache()的主要逻辑就是:先判断共享动态库是否已经映射到内存中了,如果已经存在,则直接返回;否则打开缓存文件,并将共享动态库映射到内存中。
loadInsertedDylib
共享动态库映射到内存后,dyld会把app 环境变量DYLD_INSERT_LIBRARIES中的动态库调用loadInsertedDylib()函数进行加载。可以在xcode中设置环境变量,打印出app启动过程中的DYLD_INSERT_LIBRARIES环境变量,这里看一下我们开发的app的DYLD_INSERT_LIBRARIES环境变量:
DYLD_INSERT_LIBRARIES=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libBacktraceRecording.dylib:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libMainThreadChecker.dylib:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
看一下loadInsertedDylib中的实现逻辑:
static void loadInsertedDylib(const char* path)
{
// loadInsertedDylib方法中主要调用了load方法
ImageLoader* image = NULL;
try {
LoadContext context;
context.useSearchPaths = false;
context.useFallbackPaths = false;
context.useLdLibraryPath = false;
image = load(path, context, cacheIndex);
}
}
loadInsertedDylib()函数中主要是调用了load()函数,看一下load()函数的实现:
// load函数是一系列查找动态库的入口
ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex)
{
// 根据路径进行一系列的路径搜索、cache查找等
ImageLoader* image = loadPhase0(path, orgPath, context, cacheIndex, NULL);
if ( image != NULL ) {
CRSetCrashLogMessage2(NULL);
return image;
}
// 查找失败,再次查找
image = loadPhase0(path, orgPath, context, cacheIndex, &exceptions);
if ( (image == NULL) && cacheablePath(path) && !context.dontLoad ) {
if ( (myerr == ENOENT) || (myerr == 0) )
{
// 从缓存里面找
if ( findInSharedCacheImage(resolvedPath, false, NULL, &mhInCache, &pathInCache, &slideInCache) ) {
struct stat stat_buf;
try {
image = ImageLoaderMachO::instantiateFromCache(mhInCache, pathInCache, slideInCache, stat_buf, gLinkContext);
image = checkandAddImage(image, context);
}
}
}
}
}
load()函数是查找动态库的入口,在load()函数中,会调用loadPhase0,loadPhase1,loadPhase2,loadPhase3,loadPhase4,loadPhase5,loadPhase6,对动态库进行查找。最终在loadPhase6中,对mach-o文件进行解析,并最终转成一个ImageLoader对象。看一下loadPhase6中的实现逻辑:
// 进行文件读取和mach-o文件解析,最后调用ImageLoaderMachO::instantiateFromFile生成ImageLoader对象
static ImageLoader* loadPhase6(int fd, const struct stat& stat_buf, const char* path, const LoadContext& context)
{
uint64_t fileOffset = 0;
uint64_t fileLength = stat_buf.st_size;
// 最小的mach-o文件大小是4K
if ( fileLength < 4096 ) {
if ( pread(fd, firstPages, fileLength, 0) != (ssize_t)fileLength )
throwf("pread of short file failed: %d", errno);
shortPage = true;
}
else {
if ( pread(fd, firstPages, 4096, 0) != 4096 )
throwf("pread of first 4K failed: %d", errno);
}
// 是否兼容,主要是判断cpuType和cpusubType
if ( isCompatibleMachO(firstPages, path) ) {
// 只有MH_BUNDLE、MH_DYLIB、MH_EXECUTE 可以被动态的加载
const mach_header* mh = (mach_header*)firstPages;
switch ( mh->filetype ) {
case MH_EXECUTE:
case MH_DYLIB:
case MH_BUNDLE:
break;
default:
throw "mach-o, but wrong filetype";
}
// 使用instantiateFromFile生成一个ImageLoaderMachO对象
ImageLoader* image = ImageLoaderMachO::instantiateFromFile(path, fd, firstPages, headerAndLoadCommandsSize, fileOffset, fileLength, stat_buf, gLinkContext);
return checkandAddImage(image, context);
}
}
loadPhase6中使用了ImageLoaderMachO::instantiateFromFile()函数来生成ImageLoader对象,ImageLoaderMachO::instantiateFromFile()的实现和上面提到的instantiateMainExecutable实现逻辑类似,也是先判断mach-o文件是否被压缩过,然后根据是否被压缩,生成不同的ImageLoader对象,这里不做过多的介绍。
link
在将主程序以及其环境变量中的相关动态库都转成ImageLoader对象之后,dyld会将这些ImageLoader链接起来,链接使用的是ImageLoader自身的link()函数。看一下具体的代码实现:
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
// 递归加载所有依赖库
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
// 递归修正自己和依赖库的基地址,因为ASLR的原因,需要根据随机slide修正基地址
this->recursiveRebase(context);
// recursiveBind对于noLazy的符号进行绑定,lazy的符号会在运行时动态绑定
this->recursiveBind(context, forceLazysBound, neverUnload);
}
link()函数中主要做了以下的工作:
1. recursiveLoadLibraries递归加载所有的依赖库
2. recursiveRebase递归修正自己和依赖库的基址
3. recursiveBind递归进行符号绑定
在递归加载所有的依赖库的过程中,加载的方法是调用loadLibrary()函数,实际最终调用的还是load()方法。经过link()之后,主程序以及相关依赖库的地址得到了修正,达到了进程可用的目的。
initializeMainExecutable
link()函数执行完毕后,会调用initializeMainExecutable()函数,可以将该函数理解为一个初始化函数。实际上,一个app启动的过程中,除了dyld做一些工作外,还有一个重要的角色,就是runtime,而且runtime和dyld是紧密联系的。runtime里面注册了一些dyld的回调通知,这些通知是在runtime初始化的时候注册的。其中有一个通知是,当有新的镜像加载时,会执行runtime中的load-images()函数。接下来看一些runtime中的源码,分析一下load-images()函数做了哪些操作。
void load_images(const char *path __unused, const struct mach_header *mh)
{
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
load_images()中首先调用了prapare_load_methods()函数,接着调用了call_load_methods()函数。看一下parpare_load_methods()的实现:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
_getObjc2NonlazyClassList获取到了所有类的列表,而remapClass是取得该类对应的指针,然后调用了schedule_class_load()函数,看一下schedule_class_load的实现:
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
分析这段代码,可以知道,在将子类添加到加载列表之前,其父类一定会优先加载到列表中。这也是为何父类的+load方法在子类的+load方法之前调用的根本原因。
然后我们在看一下call_load_methods()函数的实现:
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
while (loadable_classes_used > 0) {
call_class_loads();
}
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
call_load_methods中主要调用了call_class_loads()函数,看一下call_class_loads的实现:
static void call_class_loads(void)
{
int i;
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
if (classes) free(classes);
}
其主要逻辑就是从待加载的类列表loadable_classes中寻找对应的类,然后找到@selector(load)的实现并执行。
getThreadPC
getThreadPC是ImageLoaderMachO中的方法,主要功能是获取app main函数的地址,看一下其实现逻辑:
void* ImageLoaderMachO::getThreadPC() const
{
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
// 遍历loadCommand,加载loadCommand中的'LC_MAIN'所指向的偏移地址
if ( cmd->cmd == LC_MAIN ) {
entry_point_command* mainCmd = (entry_point_command*)cmd;
// 偏移量 + header所占的字节数,就是main的入口
void* entry = (void*)(mainCmd->entryoff + (char*)fMachOData);
if ( this->containsAddress(entry) )
return entry;
else
throw "LC_MAIN entryoff is out of range";
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
return NULL;
}
该函数的主要逻辑就是遍历loadCommand,找到’LC_MAIN’指令,得到该指令所指向的便宜地址,经过处理后,就得到了main函数的地址,将此地址返回给__dyld_start。__dyld_start中将main函数地址保存在寄存器后,跳转到对应的地址,开始执行main函数,至此,一个app的启动流程正式完成。
总结
在上面,已经将_main函数中的每个流程中的关键函数都介绍完了,最后,我们看一下_main函数的实现:
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;
// 处理环境变量,用于打印
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
try {
// 将主程序转变为一个ImageLoader对象
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
// 将共享库加载到内存中
mapSharedCache();
}
// 加载环境变量DYLD_INSERT_LIBRARIES中的动态库,使用loadInsertedDylib进行加载
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// 链接
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
// 初始化
initializeMainExecutable();
// 寻找main函数入口
result = (uintptr_t)sMainExecutable->getThreadPC();
}
return result;
}
本篇文章介绍了从dyld处理主程序Mach-O开始,一直到寻找到主程序Mach-O main函数地址的整个流程。需要注意的是,这也仅仅是一个大概流程的介绍,实际上,除了文章中所写的这些,源码中还有非常多的细节处理,以及一些没有介绍到的知识点。无论是XNU,还是dyld,阅读其源码都是一个巨大的工程,需要在日后不断的学习、回顾。
完。