dyld应用程序加载

前言

之前的文章分析的是main函数之后的底层流程,那么main函数之前底层的流程是怎么样子的呢?我们这篇文章就得到了很好的分析,请往下看!

准备源码

应用程序加载原理

编译过程

编译过程
  • 源文件:.m/.h/.cpp等文件。
  • 预处理:在预处理的时候,注释被删除、条件编译被处理、头文件展开、宏被替换。
  • 编译:进行词法分析语法分析以及中间层IR文件,最后生成汇编文件.s文件。
  • 汇编:将.s文件转换成机器语言生成.o文件。
  • 链接:将所有的.o文件以及链接的第三方库,生成一个macho类型的可执行文件。

静态库与动态库

什么是库
库就是可执行的二进制文件,可以被系统加载到内存的。库也分为动态库静态库。它们之间的区别就是链接的区别

  • 动态库:程序编译不会链接到目标代码中,而是程序运行时才被载入。
  • 静态库:在链接阶段会将汇编生成的目标和引用库一起链接打包到可执行文件中。

动静态库链接流程

动态库链接流程

静态库的优缺点

  • 优点:静态库被打包到可执行文件(mach-o文件)中可以独立运行,不需要依赖外部环境。
  • 缺点:编译文件相对较大,影响编译速度。如果要添加新的静态库需要重新编译

动态库的优缺点

  • 优点:
    • 使得可执行文件变得更小,编译速度更快
    • 共享内容资源共享
    • 可以动态地进行添加,得到程序更新的目的。
  • 缺点:不能够独立运行,需要依赖外部环境。

dyld加载库到内存的原理图

动态库链接详细流程

dyld动态链接器,是苹果系统的重要组成部分。

dyld初探

既然是dyld加载的库,那么在加载完成后肯定会进入main函数,那么在main函数上打个断点。

引出dyld

通过打印堆栈信息,在main函数进来之前的确启动了dyld,但是startmain的过程也没有反馈出来,因为+load方法实在main函数之前调用的,呢么我们可以尝试在+load方法中打上断点并查看堆栈的信息。
+load方法断点

根据+load方法的堆栈信息打印,看出了基本的流程如下:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> initializeMainExecutable --> runInitializers --> processInitializers --> runInitializers -->recursiveInitialization --> notifySingle --> load_images -->+[ViewController load]

添加符号断点(_dyld_start)参看汇编信息:

_dyld_start断点调试

汇编中_dyld_start之后调用的是dyldbootstrap::start方法。在dyld源码中全局搜索dyldbootstrap::start发现没有实现的地方都是汇编:
_dyld_start汇编

在全局搜索dyldbootstrap
dyldbootstrap命名

dyldbootstrap是一个命名空间,在dyldInitialization.cpp文件中搜索start方法:

uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
                const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

    // Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
    //告知debugServer,dyld要启动
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

    //重新绑定dyld
    rebaseDyld(dyldsMachHeader);

    // kernel sets up env pointer to be just past end of agv array
    const char** envp = &argv[argc+1];
    
    // kernel sets up apple pointer to be just past end of envp array
    const char** apple = envp;
    while(*apple != NULL) { ++apple; }
    ++apple;

    // set up random value for stack canary
    //栈溢出的保护,保持栈平衡
    __guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
    // run all C++ initializers inside dyld
    runDyldInitializers(argc, argv, envp, apple);
#endif

    _subsystem_init(apple);
    //获取ASLR(偏移值)
    uintptr_t appsSlide = appsMachHeader->getSlide();
    //返回dyld调用main函数的返回值
    return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
  • 因为App一启动系统就会自动给App随机分配ASLR。dyld需要重定位因为它需要到当前进程中获取自己的信息。
  • 调用dyld::_main函数,获得返回结果。

dyld::_main

查看dyld::_main源码,发觉源码还是比较多的,有大约1000行。那么经过分拣得到如下的关键代码:

dyld环境配置

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* 
                apple[], uintptr_t* startGlue)
{
       .......
    //系统内核检测
    //Check and see if there are any kernel flags
    dyld3::BootArgs::setFlags(hexToUInt64(_simple_getenv(apple, "dyld_flags"), nullptr));
        ......
    //获取主程序的hash值
    uint8_t mainExecutableCDHashBuffer[20];
    // 主程序的hash值初始化是0
    const uint8_t* mainExecutableCDHash = nullptr;
    if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
           ...
      mainExecutableCDHash = mainExecutableCDHashBuffer;//赋值
    }
        //根据macho头文件配置CPU架构的信息,就是一些文件配置
    getHostInfo(mainExecutableMH, mainExecutableSlide);
        ......
uintptr_t result = 0; // result就是main函数的地址
    sMainExecutableMachHeader = mainExecutableMH;//可执行文件的头文件
        sMainExecutableSlide = mainExecutableSlide;  //加载到进程系统自动提供ASLR虚拟内存偏移
        
    {
            __block bool platformFound = false;
            //验证主程序是什么架构的是arm64还是x86的是64位的还是32位的
            ((dyld3::MachOFile*)mainExecutableMH)->forEachSupportedPlatform
            (^(dyld3::Platform platform, uint32_t minOS, uint32_t sdk) {
                 
                    gProcessInfo->platform = (uint32_t)platform;
                    platformFound = true;
            });
    }
        
       ...
      //设置上下文 就会把信息保存起来,保存到 gLinkContext中
      setContext(mainExecutableMH, argc, argv, envp, apple);
      ...
      //文件是否是受限的,AFMI 苹果移动文件保护机制
      configureProcessRestrictions(mainExecutableMH, envp);
      ...
      // set again because envp and apple may have changed or moved
      //再次更新上下文信息
      setContext(mainExecutableMH, argc, argv, envp, apple);
      
      //环境变量的配置,xcode配置环境变量控制台可以打印信息
      if ( sEnv.DYLD_PRINT_OPTS )
            printOptions(argv);
      if ( sEnv.DYLD_PRINT_ENV ) 
            printEnvironmentVariables(envp);

}

其实就是一些读取macho的头文件信息,保存设置,以及环境变量的设置。明显就是准备工作。

加载共享缓存
共享缓存是iOS系统中重要的组成部分,共享缓存是系统级别的动态库。比如iOS开发中常用的UIKit或者CoreFoundation等等就是在共享缓存中加载进来的。自己创建的动态库或者第三方的动态库不会放在共享缓存中。

加载共享缓存

  • checkSharedRegionDisable方法是检测是否需要不同的架构是否需要共享缓存。
  • mapSharedCache是加载共享缓存。

checkSharedRegionDisable

checkSharedRegionDisable代码实现

checkSharedRegionDisable其实就是共享缓存检测结果信息的提示。最后的提示明确了iOS不能脱离共享缓存运行。

loadDyldCache
mapSharedCache加载共享缓存的源码实现中调用了loadDyldCache方法,明显这就是核心方法。那么查看loadDyldCache方法源码的实现如下:

加载共享缓存内部操作

加载共享缓存有三种情况:

  • 强制私有forcePrivate = YES表示强制私有,只加载到当前App进程中,不放在共享缓存中。
  • 缓存已经加载:如果依赖库在共享缓存中加载过,那么直接在共享缓存中直接用就可以了,无需重新加载进来。
  • 第一次加载:如果你依赖的库共享缓存中没有,它就会被加载到共享缓存中。

共享缓存总结:
通过源码的分析,对共享缓存的理解已经比较的清晰。系统缓存是系统级别的动态库,那么加载到共享缓存中的库只能是系统自带的库自定义的动态库或者第三方的动态库无法加载到共享缓存中的。这样的目的是让更多的进程在共享缓存中使用系统库,提高系统效率

dyld3或dyld2

dyld3有叫做闭包模式它的加载速度更快效率更高IOS11以后主程序都是用dyld3加载,IOS13以后动态库和三方库用dyld3加载。

系统如何选择dyld版本
main函数实现的6841行有以下的代码操作:

  //判断是否使用闭包模式也是dyld3的模式启动 ClosureMode::on 用dyld3 否则使用dyld2
  if ( sClosureMode == ClosureMode::Off ) {
    //dyld2
    if ( gLinkContext.verboseWarnings )
            dyld::log("dyld: not using closures\n");
  } else {
    //dyld3  DYLD_LAUNCH_MODE_USING_CLOSURE 用闭包模式
    sLaunchModeUsed = DYLD_LAUNCH_MODE_USING_CLOSURE;
    const dyld3::closure::LaunchClosure* mainClosure = nullptr;
    dyld3::closure::LoadedFileInfo mainFileInfo;
    //主程序中的info与header
    mainFileInfo.fileContent = mainExecutableMH;
    mainFileInfo.path = sExecPath;
    ...
    // 首先到共享缓存中去找是否有dyld3的mainClosure
    if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
            mainClosure = sSharedCacheLoadInfo.loadAddress->findClosure(sExecPath);
            ...
    }
 
   ...
    //如果共享缓存中有,然后去验证closure是否是有效的
    if ( (mainClosure != nullptr) && !closureValid(mainClosure, mainFileInfo, 
    、mainExecutableCDHash, true, envp) ) {
            mainClosure = nullptr;
            sLaunchModeUsed &= ~DYLD_LAUNCH_MODE_CLOSURE_FROM_OS;
    }
    
    bool allowClosureRebuilds = false;
    if ( sClosureMode == ClosureMode::On ) {
            allowClosureRebuilds = true;
    } 
    ...
    
    //如果没有在共享缓存中找到有效的closure 此时就会自动创建一个closure
    if ( (mainClosure == nullptr) && allowClosureRebuilds ) {
        ...
        if ( mainClosure == nullptr ) { 
        // 创建一个mainClosure
        mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, 
        bootToken);
        if ( mainClosure != nullptr )
                sLaunchModeUsed |= DYLD_LAUNCH_MODE_BUILT_CLOSURE_AT_LAUNCH;
        }
    }
   
    // try using launch closure
    // dyld3 开始启动
    if ( mainClosure != nullptr ) {
        CRSetCrashLogMessage("dyld3: launch started");
        ...
        //启动 launchWithClosure
        bool launched = launchWithClosure(mainClosure, 
        sSharedCacheLoadInfo.loadAddress,(dyld3::MachOLoaded*)mainExecutableMH,...);
         //启动失败                                                              
        if ( !launched && closureOutOfDate && allowClosureRebuilds ) {
                // closure is out of date, build new one
                // 如果启动失败 重新去创建mainClosure
                mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, 
                envp, bootToken);
                if ( mainClosure != nullptr ) {
                    ...
                    //dyld3再次启动
                    launched = launchWithClosure(mainClosure,  sSharedCacheLoadInfo.loadAddress,
                    (dyld3::MachOLoaded*)mainExecutableMH,...);
                }
            }

            if ( launched ) {
                    gLinkContext.startedInitializingMainExecutable = true;
                    if (sSkipMain)
                    //启动成功直接返回main函数的地址
                    result = (uintptr_t)&fake_main;
                    return result;
            }
            else {  
            //启动失败,打印异常信息   
            }
    }
}
  • dyld3在启动过程中会尝试多次的启动,这是系统的容错操作。一般情况下不会出现启动失败的情况。
  • 如果不启用dyld3就会选择启动dyld2,请往下看
// could not use closure info, launch old way
    // 用dyld2的模式,不用dyld3
    sLaunchModeUsed = 0;
    // install gdb notifier
    stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
    stateToHandlers(dyld_image_state_mapped, sSingleHandlers)->push_back(updateAllImages);
    // make initial allocations large enough that it is unlikely to need to be re-alloced
    sImageRoots.reserve(16);
    sAddImageCallbacks.reserve(4);
    sRemoveImageCallbacks.reserve(4);
    sAddLoadImageCallbacks.reserve(4);
    sImageFilesNeedingTermination.reserve(16);
    sImageFilesNeedingDOFUnregistration.reserve(8);

#if !TARGET_OS_SIMULATOR
#ifdef WAIT_FOR_SYSTEM_ORDER_HANDSHAKE
      file generation process
    WAIT_FOR_SYSTEM_ORDER_HANDSHAKE(dyld::gProcessInfo->systemOrderFlag);
#endif
#endif
    try {
        // add dyld itself to UUID list
        addDyldImageToUUIDList();
                ......
 }

注意:因为dyld2已经逐渐被dyld3取代,那么我们主要是研究dyld3的加载过程,dyld2就不需要了。

dyld3启动流程总结

  • 创建dyld3的实例mainClosure
  • 再去共享缓存中查找有效的mainClosure,如果有直接启动。
  • 如果没有,创建mainClosure,赋值给空的mainClosure
  • 启动mainClosure,启动dyld3
  • 启动成功以后,主程序启动成功,result就是main函数的地址,返回到dyldbootstrap::start方法,然后进入mian函数。

实例化主程序

通过分析源码,发现dyld3dyld2其实加载流程是一样的,不一样的是dyld3闭包模式image在源码中经常出现,image并不是图片的意思,而是镜像文件镜像文件就是从磁盘映射到内存的macho文件。可以理解为只要是加载到内存macho文件就叫镜像文件

实例化主程序

实例化主程序就是把需要的主程序的部分信息加载到内存中,通过instantiateMainExecutable方法返回ImageLoader类型的实例对象,然后对主程序进行签名
添加镜像文件

将实例化的image添加到镜像文件的数组中,值得注意的点是主程序的镜像文件是第一个添加到镜像文件数组中的

ImageLoaderMachO::instantiateMainExecutable

读取mach-o Header信息

此方法其实就是读取mach-o镜像文件)的Header信息。
加载macho文件中command信息,以及校验。sniffLoadCommands中代码取部分重要的探究。(laodCommand记录着动态库的加载顺序,有那些动态库需要加载等等信息)

由上图面代码分析可知道:

  • sniffLoadCommands中加载segmentcommod信息,以及一些校验
  • segment段的个数最大是256个。
  • command的个数最大是4096个。
  • 确保必须依赖了libSystem库。

使用machOView查看mach-o文件

查看mach-文件

macho文件主要是3块内容HeaderCommodsData。需要细致的mach-o结构自己自行操作就要可以了哦!

插入动态库

插入动态库

通过loadInsertedDylib插入动态库,此时有的动态库数量是所有的镜像文件减1。(因为第一个动态库为主程序)

链接主程序

链接主程序

link方法中调用了imageLoader::link方法,ImageLoader负责加载image文件(主程序,动态库)每个image对应一个ImageLoader类的实例。
ImageLoader::link方法实现

link的主要流程如下:

  • 递归加载所有的动态库
  • 递归image重定位。
  • 递归绑定非懒加载
  • 弱绑定

recursiveLoadLibraries递归动态库,代码实现

void ImageLoader::recursiveLoadLibraries(const LinkContext& context, bool 
preflightOnly, const RPathChain& loaderRPaths, const char* loadPath){
    ...
    // get list of libraries this image needs
    //获取当前的image依赖的动态库
    DependentLibraryInfo libraryInfos[fLibraryCount]; 
    this->doGetDependentLibraries(libraryInfos);

    // get list of rpaths that this image adds
    //获取当前的image依赖的动态库的文件路径
    std::vector<const char*> rpathsFromThisImage;
    this->getRPaths(context, rpathsFromThisImage);
    const RPathChain thisRPaths(&loaderRPaths, &rpathsFromThisImage);

    // 加载image依赖的动态库
    for(unsigned int i=0; i < fLibraryCount; ++i){
      ...
      dependentLib = context.loadLibrary(requiredLibInfo.name, true, this->getPath(),
      &thisRPaths, cacheIndex);
      // 保存加载的动态库
      setLibImage(i, dependentLib, depLibReExported, requiredLibInfo.upward);    
      ...
    `}`

    //告诉image依赖的动态库去加载各自需要的动态库
    for(unsigned int i=0; i < libraryCount(); ++i) {
            ImageLoader* dependentImage = libImage(i);
            if ( dependentImage != NULL ) {
                 dependentImage->recursiveLoadLibraries(context, preflightOnly,
                 thisRPaths, libraryInfos[i].name);
            }
    }
}

因为recursiveLoadLibraries方法的实现代码比较多,我们就提取了比较重要的代码进行了分析,得出recursiveLoadLibraries方法只要做以下的步骤:

  • 获取当前image依赖的动态库和动态库的文件路径
  • 加载image依赖的动态库,并进行了保存
  • 告诉image依赖的动态库去加载各自需要的动态库。(dyld是系统级别的,里面有image数组)

链接动态库

链接动态库

链接动态库其实跟链接主程序的逻辑基本一样。
注意:循环取image(镜像文件)的时候要从第1个开始取,因为第0个是主程序。那么我们就在下面验证一下!
主程序image位置验证

好明显image list中第0个就是主程序。

弱绑定主程序

弱绑定主程序

注意:之前说链接主程序的时候不是进行了弱绑定吗?因为在链接主程序中linkingMainExecutable = true,所以link里面的弱绑定在主程序时是不调用的,等动态库的都进行了弱绑定,最后对主程序进行弱绑定

运行初始化方法

运行所有的初始化image
  • initializeMainExecutable方法就是初始化image的方法。
  • initializeMainExecutable作为单独一部分进行研究。(请往下看)

返回main函数

返回main函数地址

最后是获取main函数地址,返回给外面,这样我们就能回到main函数里面了。

dyld加载流程总结

dyld加载流程:dyld::_main --> 配置环境变量 --> 加载共享缓存 --> 实例化主程序 --> 插入动态库 -->链接主程序 --> 链接动态库 --> 弱绑定主程序 --> 运行初始化方法 --> 返回main函数(其实标题就是流程了)

initializeMainExecutable -- 初始化image

initializeMainExecutable初始化方法的实现

注意:在initializeMainExecutable始化方法中,运行动态库的初始化方法,运行主程序的初始化方法。

runInitializers

runInitializers方法实现

进入processInitializers方法
processInitializers方法实现

进入recursiveInitialization方法(ImageLoader.cpp)

void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
                                          InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
……
    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
            //先初始化下级lib
            for(unsigned int i=0; i < libraryCount(); ++i) {
                ImageLoader* dependentImage = libImage(i);
                if ( dependentImage != NULL ) {
……
                    else if ( dependentImage->fDepth >= fDepth ) {
                        //依赖文件递归初始化
                        dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
                    }
                }
            }       
……
            fState = dyld_image_state_dependents_initialized;
            oldState = fState;
            //这里调用传递的状态是dyld_image_state_dependents_initialized,image传递的是自己。也就是最后调用了自己的+load。从libobjc.A.dylib开始调用。
            context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
            
            // initialize this image
            //初始化镜像文件,调用c++构造函数。libSystem的libSystem_initializer就是在这里调用的。会调用到objc_init中。_dyld_objc_notify_register 中会调用自身的+load方法,然后c++构造函数。
            //1.调用libSystem_initializer->objc_init 注册回调。
            //2._dyld_objc_notify_register中调用 map_images,load_images,这里是首先初始化一些系统库,调用系统库的load_images。比如libdispatch.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib。
            //3.自身的c++构造函数
            bool hasInitializers = this->doInitialization(context);

            // let anyone know we finished initializing this image
            fState = dyld_image_state_initialized;
            oldState = fState;
            //这里调用不到+load方法。 notifySingle内部fState==dyld_image_state_dependents_initialized 才调用+load。
            context.notifySingle(dyld_image_state_initialized, this, NULL);
……
        }
……
    }
    recursiveSpinUnLock();
}

通过上面的代码可以得出:

  • 需要初始化的动态库image是从libImage()中获取,而libImage()的数据是在链接动态库的时recursiveLoadLibraries中的setLibImage保存的image
  • 系统会根据每个库的依赖深度去初始化,深度值最大优先去初始化,每次初始化都会有一个image文件。
    -image都会调用 context.notifySingle方法去调用load_images调用load方法。
  • doInitialization是初始化没有依赖的库。
  • context.notifySingle(dyld_image_state_initialized, this, NULL) 实际上作用不大,notifySingle方法中并没有判断,有可能是在objc注册回调时里面根据dyld_image_state_initialized状态去调用系统库
    进入notifySingle方法(dyld2.cpp)
//调用到objc里面去
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
……

    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 是在 registerObjCNotifiers 中赋值的。这里执行会跑到objc的load_images中
        (*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);
        }
    }
……
}

(*sNotifyObjCInit)(image->getRealPath(), image->machHeader())这是函数调用,而且参数是跟image有关的,现在只要搞清楚在哪里赋值就可以了。全局搜索sNotifyObjCInit,源码如下:

sNotifyObjCInit赋值

registerObjCNotifiers方法中对sNotifyObjCInit进行赋值,全局搜索registerObjCNotifiers, 源码如下:
registerObjCNotifiers调用地方

全局搜索_dyld_objc_notify_register,发现dyld源码中并没有调用的这个方法的地方。在项目工程中添加_dyld_objc_notify_register符号断点,我的项目工程跑运行的是Mac用的是dyld2,如果运行的是IOS用的就是dyld3。
真机添加符号断点查看堆栈

通过上面真机的堆栈信息可以发现:

  • doInitialization --> doModInitFunctions --> libSystem_initializer --> libdispatch_init --> _os_object_init --> _objc_init --> _dyld_objc_notify_register --> registerObjCNotifiers
  • libSystem_initializer方法在libSystem系统库中。
  • libdispatch_init_os_object_init方法在libdispatch系统库中
  • _objc_init方法在libobjc系统库中,objc源码库。

疑问:_objc_init方来到之后到_dyld_objc_notify_register,那么_objc_init方法里面会不会调用_dyld_objc_notify_register方法呢?

进入_objc_init方法(objc4-812源码中找到)

_objc_init方法实现

sNotifyObjCInit = load_images其实就是调用load_images方法,下面探究下load_images方法。

进入load_images方法

load_images方法

进入call_load_methods
call_load_methods方法

进入call_class_load方法
call_class_load

call_class_load方法里面其实就是调用了load方法。

进入call_category_loads方法

call_category_loads方法

call_category_loads方法内部也调用了load方法。

总结:

  • 分类都会调用load方法,从这里调用顺序可得出这样一个结论:
    • 类的load比分类的load方法先调用,类中load方法调用完才开始调用
      分类的load方法。
    • 类中的load方法按编译先后顺序,谁先编译谁的load方法先调用。
    • 分类中的的load方法按编译先后顺序,谁先编译谁的load方法先调用。
      注意:这是常见的面试题哦!!!!

_objc_init流程

_objc_initobjc系统库中,除了是大家最熟悉的源码库还有就是_objc_init起到承上启下的作用。所以从_objc_init反推整个流程。调用_objc_init方法的是_os_object_init方法,在libdispatch源码库中全局搜索_os_object_init

进入_os_object_init方法

_os_object_init方法

_os_object_init方法确实调用_objc_init方法。_os_object_init方法是被libdispatch_init调用,继续验证。

进入libdispatch_init方法

libdispatch_init方法

libdispatch_init方法确实调用_os_object_init方法。libdispatch_init被libSystem_initializer调用,libSystem_initializer方法是在libSystem系统库中。

进入libSystem_initializer方法

libSystem_initializer方法实现

libSystem_initializer方法确实调用libdispatch_init方法。libSystem_initializer方法是被doModInitFunctions调用, doModInitFunctions方法是在dyld源码库中的。

进入doModInitFunctions中(dyld源码)

doModInitFunctions方法

libSystem的初始化程序,必须最先运行,其它在它后面执行,doModInitFunctions 调用所有的C++函数。

在main函数下面添加一个全局的C++函数,断点看下堆栈信息:

添加C++函数查看堆栈信息

堆栈信息显示是doModInitFunctions方法调用了全局的c++方法,是在load方法之后。在doModInitFunctions方法之前调用的是doInitialization方法,然后我们以此类推进行下一步。

进入doInitialization方法

doInitialization方法

证明doModInitFunctions方法是被doInitialization调用,那么doInitialization之前已经引申出来了,只是没讲到。

doInitialization方法的调用

doInitialization的调用

recursiveInitialization调用doInitialization方法,又会到了递归的方法里,完美的串联起来。

总结

  • load方法的调用流程:
    _dyld_start --> dyldbootstrap::start --> dyld::_main --> intializeMainExecutable --> runInitializers --> processInitializers --> runInitializers -->recursiveInitialization --> notifySingle --> load_images -->+[ViewController load]
  • _objc_init方法的调用流程:
    doInitialization --> doModInitFunctions --> libSystem_initializer --> libdispatch_init--> _os_object_init --> _objc_init --> _dyld_objc_notify_register-->registerObjCNotifiers

这两个调用流程通过doInitializationnotifySingle完美形成一个完整的闭环

main函数入口

上面说到C++函数是在main函数之前,在+load函数之后,那么来验证一波!!

load函数验证

根据上面可以得出+load函数的确实在C++函数之前,那么打开汇编断点可以看到:
打开汇编断点

发现+load的方法是在C++之前,这是肯定的了。然后我们让断点一步一步往下走,看看什么时候进入main函数:
准备进入main函数

dyldbootstrap::start返回了main函数的地址,读取x86_64寄存器发现第一个寄存器rax存放的是main函数的地址,最后一步跳转到main函数。

注意:项目中生成的main.m文件不能随意地修改,因为底层都是写死了main,你修改之后就会找不到然后报错

修应该main.m文件名称报错

dyld加载流程图

dyld加载流程图

总结

dyld我总结了两天的时间,学习的过程中是非常枯燥的,需要把持住!!坚持下来了,就可以把握住dyld的加载流程,先苦后甜肯定是有道理的哦!!加油!!!

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

推荐阅读更多精彩内容