逆向之旅--[基础]从源码看 dyld

前言

在 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),主要就是将 argcargv 等参数都存储下来给后续流程使用,同时设置后面需要调用的函数。
然后调用 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_OPTSDYLD_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 加载共享缓存库,加载共享缓存总共分三种情况:

  1. 仅加载到当前进程
  2. 第一次加载,则去做加载操作 mapCacheSystemWide
  3. 之前已经加载过了,则不做任何操作

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 这些比较细节的部分没有深入探讨,之后再写一篇文章专门讲一下这些细节。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335