美文网首页
应用程序加载

应用程序加载

作者: 浅墨入画 | 来源:发表于2021-08-07 23:10 被阅读0次

应用程序的加载

本篇主要是分析dyld的加载流程,了解在main函数之前,底层还做了什么? 我们经常听到的二进制重排启动优化等字眼,就是在App的启动流程中做文章。

编译过程及库

在分析app启动之前,我们需要先了解iOSapp代码的编译过程以及动态库静态库

编译过程

编译过程如下所示

  • 源文件:载入.h、.m、.cpp等文件
  • 预处理:替换宏,删除注释,展开头文件,产生.i文件
  • 编译:将.i文件转换为汇编语言,产生.s文件
  • 汇编:将汇编文件转换为机器码文件,产生.o文件
  • 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件
编译过程
静态库和动态库

静态库:在链接阶段,会将可汇编生成的目标程序与引用的库一起链接打包到可执行文件当中。此时的静态库就不会再改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的

  • 优点:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖,直接就可以运行
  • 缺点:由于静态库会有两份,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大

动态库:程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入

  • 优势
  1. 减少打包之后app的大小:因为不需要拷贝至目标程序中,所以不会影响目标程序的体积,与静态库相比,减少了app的体积大小
  2. 共享内存,节约资源:同一份库可以被多个程序使用
  3. 通过更新动态库,达到更新程序的目的:由于运行时才载入的特性,可以随时对库进行替换,而不需要重新编译代码
  • 缺点:动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行

静态库与动态库图示

image.png
dyld加载流程分析

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的重要组成部分,在app被编译打包成可执行文件格式的Mach-O文件后,交由dyld负责连接,加载程序

dyld演变过程WWDC视频 -> WWDC2017:App Startup Time: Past, Present, and Future

App启动流程图

dyld加载流程

创建工程,ViewController中重写load方法,在main中加了一个C++方法,并且在main函数中打印,查看程序执行顺序

<!-- ViewController.m文件 -->
@implementation ViewController
+ (void)load{
    NSLog(@"%s",__func__);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}
@end

<!-- main.m文件 -->
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    NSLog(@"1223333");
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

__attribute__((constructor)) void kcFunc(){
    printf("来了 : %s \n",__func__);
}

// 控制台打印
2021-08-06 21:46:06.605315+0800 002-应用程加载分析[5305:346935] +[ViewController load]
来了 : kcFunc 
2021-08-06 21:51:43.385279+0800 002-应用程加载分析[5305:346935] 1223333

通过打印结果可以看出其顺序是load --> C++方法 --> mainmain作为入口函数为什么不是最先执行?根据这个问题,我们来探索main函数之前底层做了什么?

  • load方法处加一个断点,通过bt打印堆栈信息查看app启动是从哪里开始的。由于栈是先进后出,所以程序先执行的是_dyld_start
image.png
  • 需要去OpenSource下载一份dyld源码来进行分析,这里使用的是dyld-852.tar.gz版本
  • dyld-852源码中全局搜索_dyld_start,查找arm架构发现,是由汇编实现,通过汇编注释发现会调用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)方法,这是一个C++方法(以arm架构为例)
image.png
  • 源码中搜索dyldbootstrap找到命名作用空间,再在这个文件中查找start方法,其核心是返回值调用了dyld的_main函数,其中macho_header是Mach-O的头部,而dyld加载的文件就是Mach-O类型的,即Mach-O类型是可执行文件类型,由四部分组成:Mach-O头部、Load Command、section、Other Data,可以通过MachOView查看可执行文件信息
image.png

dyld流程中的main函数

进入dyld::_main的源码实现,发现代码特别长,可以根据_main函数的返回值进行反推。反推流程result -> sMainExecutable -> ``

在_main函数中主要做了以下几件事情:

  • 环境变量配置:根据环境变量设置相应的值以及获取当前运行架构
// 创建主程序cdHash的空间
    uint8_t mainExecutableCDHashBuffer[20];
        //从环境中获取主可执行文件的 cdHash
    const uint8_t* mainExecutableCDHash = nullptr;
    if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
        unsigned bufferLenUsed;
        if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
            mainExecutableCDHash = mainExecutableCDHashBuffer;
    }
        //配置信息,获取主程序的mach-o header、silder(ASLR的偏移值)
    getHostInfo(mainExecutableMH, mainExecutableSlide);
  • 共享缓存:检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如UIKit、CoreFoundation等
// load shared cache
// 检查共享缓存是否开启,iOS中必须开启
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
// 检查共享缓存是否映射到了共享区域
        if ( sSharedCacheOverrideDir)
            mapSharedCache(mainExecutableSlide);
#else
        mapSharedCache(mainExecutableSlide);
#endif
  • 主程序的初始化:调用instantiateFromLoadedImage函数,实例化一个ImageLoader对象
// instantiate ImageLoader for main executable
// 主程序的初始化
// 加载可执行文件,生成一个ImageLoader实例对象
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
  • 插入动态库:遍历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);
}
// record count of inserted libraries so that a flat search will look at 
// inserted libraries, then main, then others.
// 插入库,然后是main,然后是其他
sInsertedDylibCount = sAllImages.size()-1;
  • link 主程序
// link主程序,链接主程序与动态库、插入动态库
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
  • link动态库
image.png
  • 弱符号绑定
image.png
  • 执行初始化方法
image.png
  • 寻找主程序入口即main函数:从Load Command读取LC_MAIN入口,如果没有,就读取LC_UNIXTHREAD,这样就来到了日常开发中熟悉的main函数
image.png
dyld加载流程

load的源码链为:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一个回调处理) --> load_images(libobjc.A.dylib)

整个加载过程是由dyld开始,最终到达libobjc结束,也就是说应用加载流程不只是dyld在工作,还有很多库在配合一起完成这个过程。

dyld流程图.png

dyld流程-主程序运行

主程序初始化initializeMainExecutable函数源码分析
  • initializeMainExecutable源码,主要是循环遍历,都会执行runInitializers方法
void initializeMainExecutable()
{
    // record that we've reached this step
    gLinkContext.startedInitializingMainExecutable = true;

    // run initialzers for any inserted dylibs
    ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    if ( rootCount > 1 ) {
        for(size_t i=1; i < rootCount; ++i) {
            sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
        }
    }
    
    // run initializers for main executable and everything it brings up 
    //运行主要可执行文件的初始化程序及其所带来的一切
    sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
    
    // register cxa_atexit() handler to run static terminators in all loaded images when this process exits
    //当此进程退出时,注册cxa_atexit() 处理程序以在所有加载的图像中运行静态终止符
    if ( gLibSystemHelpers != NULL ) 
        (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);

    // dump info if requested
    if ( sEnv.DYLD_PRINT_STATISTICS )
        ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
    if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
        ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
  • 全局搜索runInitializers找到如下源码,其核心代码是processInitializers函数的调用
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
    uint64_t t1 = mach_absolute_time();
    mach_port_t thisThread = mach_thread_self();
    ImageLoader::UninitedUpwards up;
    up.count = 1;
    up.imagesAndPaths[0] = { this, this->getPath() };
    //重点内容 
    processInitializers(context, thisThread, timingInfo, up);
    context.notifyBatch(dyld_image_state_initialized, false);
    mach_port_deallocate(mach_task_self(), thisThread);
    uint64_t t2 = mach_absolute_time();
    fgTotalInitTime += (t2 - t1);
}
  • 进入processInitializers函数的源码实现,其中对镜像列表调用recursiveInitialization函数进行递归实例化
// <rdar://problem/14412057> upward dylib initializers can be run too soon
// To handle dangling dylibs which are upward linked but not downward, all upward linked dylibs
// have their initialization postponed until after the recursion through downward dylibs
// has completed.
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
                                     InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
    uint32_t maxImageCount = context.imageCount()+2;
    ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
    ImageLoader::UninitedUpwards& ups = upsBuffer[0];
    ups.count = 0;
    // Calling recursive init on all images in images list, building a new list of
    // uninitialized upward dependencies.
    for (uintptr_t i=0; i < images.count; ++i) {
        images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
    }
    // If any upward dependencies remain, init them.
    if ( ups.count > 0 )
        processInitializers(context, thisThread, timingInfo, ups);
}
  • 全局搜索recursiveInitialization函数,其源码实现如下
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
                                          InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    recursive_lock lock_info(this_thread);
    recursiveSpinLock(lock_info);

    if ( fState < dyld_image_state_dependents_initialized-1 ) {
        uint8_t oldState = fState;
        // break cycles
        fState = dyld_image_state_dependents_initialized-1;
        try {
            // initialize lower level libraries first
            for(unsigned int i=0; i < libraryCount(); ++i) {
                ImageLoader* dependentImage = libImage(i);
                if ( dependentImage != NULL ) {
                    // don't try to initialize stuff "above" me yet
                    if ( libIsUpward(i) ) {
                        uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
                        uninitUps.count++;
                    }
                    else if ( dependentImage->fDepth >= fDepth ) {
                        dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
                    }
                }
            }
            
            // record termination order
            if ( this->needsTermination() )
                context.terminationRecorder(this);

            // let objc know we are about to initialize this image
            uint64_t t1 = mach_absolute_time();
            fState = dyld_image_state_dependents_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
            
            // initialize this image
            bool hasInitializers = this->doInitialization(context);

            // let anyone know we finished initializing this image
            fState = dyld_image_state_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_initialized, this, NULL);
            
            if ( hasInitializers ) {
                uint64_t t2 = mach_absolute_time();
                timingInfo.addTime(this->getShortName(), t2-t1);
            }
        }
        catch (const char* msg) {
            // this image is not initialized
            fState = oldState;
            recursiveSpinUnLock();
            throw;
        }
    }
    // 递归解锁
    recursiveSpinUnLock();
}

在这里,需要分成两部分探索,一部分是notifySingle函数,一部分是doInitialization函数。

notifySingle 函数
  • 全局搜索notifySingle(函数,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());这句
  • 全局搜索sNotifyObjCInit,发现没有找到实现,有赋值操作
  • 搜索registerObjCNotifiers在哪里调用了,发现在_dyld_objc_notify_register进行了调用。注意:_dyld_objc_notify_register的函数需要在libobjc源码中搜索
  • objc4-818源码中搜索_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是objc中的load_images,而load_images会调用所有的+load方法。所以综上所述,notifySingle是一个回调函数
doInitialization 函数
  • 走到objc的_objc_init函数,发现走不通了,我们回退到recursiveInitialization递归函数的源码实现,发现我们忽略了一个函数doInitialization
  • 进入doInitialization函数的源码实现,这里也需要分成两部分,一部分是doImageInit函数,一部分是doModInitFunctions函数
  • 进入doImageInit源码实现,其核心主要是for循环加载方法的调用,这里需要注意的一点是,libSystem的初始化必须先运行
  • 进入doModInitFunctions源码实现,这个方法中加载了所有Cxx文件
  • 可以通过测试程序的堆栈信息来验证,在C++方法处加一个断点,走到这里,还是没有找到_objc_init的调用?怎么办呢?放弃吗?当然不行,我们还可以通过_objc_init加一个符号断点来查看调用_objc_init前的堆栈信息,
  • _objc_init加一个符号断点,运行程序,查看_objc_init断住后的堆栈信息
  • libsystem中查找libSystem_initializer,查看其中的实现
  • 根据前面的堆栈信息,我们发现走的是libSystem_initializer中会调用libdispatch_init函数,而这个函数的源码是在libdispatch开源库中的,在libdispatch中搜索libdispatch_init
  • 进入_os_object_init源码实现,其源码实现调用了_objc_init函数

得出结论_objc_init的源码链:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

分析doInitialization(调用init方法)与context.notifySingle(单个通知注入)的关系

通过调用链路,此函数里面找不到load_images函数了,应为load_images已经不是dyld中的函数了,是属于libobjc库的函数。在此函数中发现了关键的回调(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());,但是在回调之前判断了sNotifyObjCInit是否为空,就证明一定有给sNotifyObjCInit变量赋值的地方

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
    //dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());
    std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sSingleHandlers);
    if ( handlers != NULL ) {
        dyld_image_info info;
        info.imageLoadAddress   = image->machHeader();
        info.imageFilePath      = image->getRealPath();
        info.imageFileModDate   = image->lastModified();
        for (std::vector<dyld_image_state_change_handler>::iterator it = handlers->begin(); it != handlers->end(); ++it) {
            const char* result = (*it)(state, 1, &info);
            if ( (result != NULL) && (state == dyld_image_state_mapped) ) {
                //fprintf(stderr, "  image rejected by handler=%p\n", *it);
                // make copy of thrown string so that later catch clauses can free it
                const char* str = strdup(result);
                throw str;
            }
        }
    }
    if ( state == dyld_image_state_mapped ) {
        // <rdar://problem/7008875> Save load addr + UUID for images from outside the shared cache
        // <rdar://problem/50432671> Include UUIDs for shared cache dylibs in all image info when using private mapped shared caches
        if (!image->inSharedCache()
            || (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion)) {
            dyld_uuid_info info;
            if ( image->getUUID(info.imageUUID) ) {
                info.imageLoadAddress = image->machHeader();
                addNonSharedCacheImageUUID(info);
            }
        }
    }
    if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
        uint64_t t0 = mach_absolute_time();
        dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
        (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
        uint64_t t1 = mach_absolute_time();
        uint64_t t2 = mach_absolute_time();
        uint64_t timeInObjC = t1-t0;
        uint64_t emptyTime = (t2-t1)*100;
        if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
            timingInfo->addTime(image->getShortName(), timeInObjC);
        }
    }
    // mach message csdlc about dynamically unloaded images
    if ( image->addFuncNotified() && (state == dyld_image_state_terminated) ) {
        notifyKernel(*image, false);
        const struct mach_header* loadAddress[] = { image->machHeader() };
        const char* loadPath[] = { image->getPath() };
        notifyMonitoringDyld(true, 1, loadAddress, loadPath);
    }
}
  • 在本文件中搜索sNotifyObjCInit,找到了函数registerObjCNotifiers
  • 通过registerObjCNotifiers函数得知,有一个调用者调用了这个函数,并且传递的第二个参数是init,全局搜索这个调用者
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    dyld::registerObjCNotifiers(mapped, init, unmapped);
}
  • _dyld_objc_notify_register函数为注册通知,通过全局查找,没有注册的地方,这里通过对工程下符号断点的方式看一下是哪个库调用的这个方法

相关文章

  • OC底层原理10—应用程序加载

    之前分析了消息的发送、转发,那么消息是如何加载到类中的呢?研究类的加载,就要先研究下应用程序的加载。在应用程序加载...

  • iOS底层原理 12 : 应用程序的加载

    一、应用程序的加载 APP加载过程:程序启动依次加载dyld、libSystem、libdispathc.dyld...

  • iOS dyld

    一、应用程序加载原理 在分析dyld加载应用程序之前,先清楚以下基本概念。库:可执行的二进制文件,可以被系统加载到...

  • iOS引用程序加载流程-dyld

    一、应用程序加载原理 在分析dyld加载应用程序之前,先清楚以下基本概念。库:可执行的二进制文件,可以被系统加载到...

  • iOS 类的加载

    一、 应用程序加载 系统调用exec()会让我们的应用程序映射到新的地址空间 然后通过dyld进行加载、链接、初始...

  • 应用程序的加载分析

    应用程序的加载分析 作为一个开发者,对于iOS应用程序启动过程有很多疑问,本篇就应用程序是如何加载的,做相关分析 ...

  • 应用程序加载

    1、问题引入 创建一个程序main.m代码: ViewController.m代码: 运行查看打印顺序 2、开始分...

  • 应用程序加载

    应用程序加载原理库:可执行的二进制文件,加载到内存文件类型:静态库 .a动态库 .so .dll两者是链接的区别 ...

  • 应用程序加载

    应用程序的加载 本篇主要是分析dyld的加载流程,了解在main函数之前,底层还做了什么? 我们经常听到的二进制重...

  • 应用程序加载

    做了这么久的ioser,你真的了解我们应用程序加载的一个主流程么?我们做的app是怎么运行起来的呢?下面我们探索下...

网友评论

      本文标题:应用程序加载

      本文链接:https://www.haomeiwen.com/subject/stuivltx.html