dyld加载应用启动原理详解

作者 : 开心源码 本文共7958个字,预计阅读时间需要20分钟 发布时间: 2022-05-12 共156人阅读

Qinz

我们都知道APP的入口函数是main(),而在main()函数调用之前,APP的加载过程是怎么的呢?接下来我们一起来分析APP的加载流程。

一、利用断点进行追踪

  • 首先我们创立一个工程,什么代码都不写,在main()函数处进行断点,会看到情况如下图:

    01

  1. 通过上图我们可以看到,在调用堆栈中,我们只看到了star和main,并开启了主线程,其它的什么都看不到。那要怎样才能看到调用堆栈详细点的信息了?我们都知道,有一个方法比main()函数调用更早,那就是load()函数,此时在控制器中写一个load函数,并断点运行,如下图:

02

  1. 通过上图,我们看到了比较详细的函数调用顺序,从第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加载应用启动原理详解

发表回复