iOS底层分析之应用程序加载流程

2022-01-14 15:05:32 浏览数 (1)

准备资料

  • dyld源码
  • Libsystem源码

整个编译过程大致分为:

  • 预编译(由Xcode完成)
  • 编译(由Xcode完成)
  • 汇编
  • 可执行文件

预编译

即编译之前要做的事情,通常来说,预编译分为:

  • 宏定义
  • 文件包含
  • 条件编译

宏定义

又叫宏替换,只做替换不做计算。宏定义的写法如下:

代码语言:javascript复制
#define 标识符 字符串

文件包含

顾名思义就是用来讲一个文件包含到另一个文件中的宏。在OC层面,我们通常使用

代码语言:javascript复制
#include:包含一个源文件代码

#import:包含一个源文件代码

@class:声明一个类,通常在.h文件里会用到,如何.m里去import它。

(滑动显示更多)

条件编译

代码语言:javascript复制
条件编译就是在编译之前预处理器根据预处理指令判断对应的条件,
如果条件满足就将对应的代码编译进去,否则代码就根本不进入编译环节。

条件编译举例:
1.#if 
2.#ifdef 判断某个宏是否被定义, 若已定义, 执行后面的语句
3.#ifndef 与#ifdef相反, 判断某个宏是否未被定义
4.#elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-if
5.#else 与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else
6.#endif #if, #ifdef, #ifndef这些条件命令的结束标志.
7.#if 与 #ifdef 的区别:#if是判断后面的条件语句是否成立,#ifdef是判断某个宏是否被定义过。要区分开

(滑动显示更多)

为了加快编译,避免多个文件使用同一个文件而导致多次引用相同文件的情况,苹果提供了预编译头的概念,也就是我们通常所使用.pch文件,在.pch里面定义、引用的文件、变量是全局的且只会编译一次,所以我们可以把常用的东西定义在里面。

可执行文件

动态库和静态库

  • 静态库格式:.a等
  • 动态库格式:.framework、.dylib、.tbd等

加载方式:

静态库是一个一个状态进内存,容易出现重复而浪费的情况;动态库是你有需要才会去加载,这样大大节省了空间。

加载过程:

  • app启动
  • 加载相应的库
  • 注册库的回调函数_dyld_objc_notify_register
  • 加载库的内存映射
  • 执行map_images、Load_images
  • 调用main函数

接下来我们通过源码分析看一下,main函数之前的流程走向.

dyld分析

(dyld又叫动态链接器)

dyld流程:

代码语言:javascript复制
dyld_start

dyldbootstrap::start

dyld::_main

        环境、平台、版本、主机信息等准备工作

        instantiateFromLoadedImage实例化主程序

        link主程序

        weakBind若引用绑定主程序

        notifyMonitoringDyldMain通知dyld可以进行main()函数调用

(滑动显示更多)

我们需要一个在main之前就执行的函数,暂时选定load函数吧:

我们需要一个在main之前就执行的函数,暂时选定load函数吧:

我们发现,最先执行到的是dyld里的_dyld_start,接下来我们下载dyld源码打开源码,搜索_dyld_start,我们会发现有好几个__dyld_start:定义,由于当前的运行设备是iPhone11,所以我们只需要看#if __arm64__这段:

代码语言:javascript复制
#if __arm64__
.text
.align 2
.globl __dyld_start
__dyld_start:
mov x28, sp
and     sp, x28, #~15 // force 16-byte alignment of stack
mov x0, #0
mov x1, #0
stp x1, x0, [sp, #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp, #16             // make room for local variables
...
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
...
#endif // __arm64__

(滑动显示更多)

我们发现注释里面有一个call指令,调用了dyldbootstrap的start函数,我们在dyld工程里全局搜索dyldbootstrap:

定位到start函数

代码语言:javascript复制
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
    ...
    //重定位dyld
    rebaseDyld(dyldsMachHeader);
    //将envp指针指向argv最后一个
    const char** envp = &argv[argc 1];
    //将apple指针指向argv末尾
    const char** apple = envp;
    while(*apple != NULL) {   apple; }
      apple;
    ///为堆栈设置随机值
    __guard_setup(apple);
    ...
    //已完成dyld引导,接下来执行main
    uintptr_t appsSlide = appsMachHeader->getSlide();
    return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

(滑动显示更多)

进入_main

由于_main函数有好几百行代码,如果我们每一行都去分析,会很耗费精力,我们可以结合最后的result返回值,以及我们一开始就知道的程序加载流程,来分析核心代码:

代码语言:javascript复制
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
int argc, const char* argv[], const char* envp[], const char* apple[], 
uintptr_t* startGlue){
    ...
    // 加载共享缓存
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
        if ( sSharedCacheOverrideDir)
            mapSharedCache();
#else
        mapSharedCache();
#endif
    }

    //实例化主程序
    /**  
    instantiateFromLoadedImage内部做了3件事情:
    判断machO是否兼容
    初始化ImageLoader
    加载初始化后的ImageLoader
    */
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
    ...
    //  加载插入的库,即动态库
    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);
    ...
    //链接动态库
    link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
    ...
    // <rdar://problem/12186933> do weak binding only after all inserted images linked
    // 弱引用绑定(在所有的iamge镜像文件链接之后,才会进行弱引用绑定符号表)
    sMainExecutable->weakBind(gLinkContext);
    ...
    // run all initializers
    // 运行所有初始化程序
    initializeMainExecutable();
    // 通知dyld进行main函数调用
    notifyMonitoringDyldMain();
    ...
    // main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
    // 通过LC_UNIXTHREAD,主程序找到main()函数入口
    result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
    ...
    return result;
}

(滑动显示更多)

接下来,我们探究一下initializeMainExecutable

代码语言:javascript复制
void initializeMainExecutable()
{
    // run initialzers for any inserted dylibs
    //拿到所有的镜像文件
    ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    if ( rootCount > 1 ) {
        //遍历所有镜像文件,并且执行Initializers初始化方法
        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]);
    ...
}

(滑动显示更多)

进入runInitializers

代码语言:javascript复制
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
    ...
    processInitializers(context, thisThread, timingInfo, up);
    context.notifyBatch(dyld_image_state_initialized, false);
    ...
}

(滑动显示更多)

点击进入processInitializers

代码语言:javascript复制
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
    ...
    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(const

代码语言:javascript复制
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,

  InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
...
try {
    //初始化依赖文件
    for(unsigned int i=0; i < libraryCount();   i) {
        ImageLoader* dependentImage = libImage(i);
        ...
    }
    ...
    ///让对象知道我们正在初始化,通知对象
    uint64_t t1 = mach_absolute_time();
    fState = dyld_image_state_dependents_initialized;
    oldState = fState;
    context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);

    //初始化镜像文件
    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);
    ...
}

(滑动显示更多)

点击notifySingle发现并没有找到函数定义,改为搜索context.notifySingle:

代码语言:javascript复制
gLinkContext.notifySingle = &notifySingle;

(滑动显示更多)

发现重定向到函数地址¬ifySingle,我们点击它:

代码语言:javascript复制
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)

{
    ...
    (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
    ...
}

(滑动显示更多)

搜索sNotifyObjCInit

我们发现,sNotifyObjCInit的赋值来源于

registerObjCNotifiers函数的第二个参数,

接下来我们搜一下registerObjCNotifiers看看哪里调用了:

代码语言:javascript复制
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);
}

(滑动显示更多)

由于我们在dyly源码工程里搜不到有哪个上层函数调用了_dyld_objc_notify_register,所以我们下一个符号断点看看哪里调用了:

运行

我们发现,_dyld_objc_notify_register由_objc_init调用。

至此,关于图片 dyld部分的代码已经分析完了,接下来进入libobjc工程,打开objc工程,由于我们前面分析到_dyld_objc_notify_register这个流程,我们在objc工程全局搜索一下_dyld_objc_notify_register:

代码语言:javascript复制
void _objc_init(void)
{
    ...
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    ...
}

(滑动显示更多)

发现在_objc_init会调用到_dyld_objc_notify_register这个函数,于是我们下断点,打印堆栈信息查看完整的流程走向:

按照先后顺序,_objc_init是在recursiveInitialization之后执行,而_dyld_objc_notify_register就像block回调一样,由notifySingle声明,最后_objc_init里调用。

当_objc_init一顿初始化操作之后,调用_dyld_objc_notify_register告诉dyld我已经初始化完毕了。

而中间的recursiveInitialization、doInitialization等我们没有办法从上一步的recursiveInitialization继续分析下去,所以我们这里采用逆向推导思维:由结果反推。

_objc_init里会调用_dyld_objc_notify_register,这个我们在前面已经分析了。

_objc_init由上层_os_object_init发起调用,_os_object_init存在于库里

  • libdispatch

打开libdispatch工程,搜索_objc_init

由此可以说明,objc工程里的_objc_init函数,的确是由libdispatch工程的_os_object_init函数发起的。

按照前面的思路,继续搜素libdispatch_init

代码语言:javascript复制
DISPATCH_EXPORT DISPATCH_NOTHROW
void
libdispatch_init(void)
{
    ...
    _os_object_init();
    ...
}

(滑动显示更多)

libdispatch_init由libSystem_initializer发起,搜索:

发现libSystem_initializer源自于Libsystem库。

  • Libsystem 源码下载

Libsystem工程里搜索libSystem_initializer:

代码语言:javascript复制
__attribute__((constructor))
static void
libSystem_initializer(int argc,
      const char* argv[],
      const char* envp[],
      const char* apple[],
      const struct ProgramVars* vars)
{
    ...
    libdispatch_init();
    ...
}

(滑动显示更多)

继续查看图片 发现ImageLoaderMachO::doModInitFunctions存在于dyld库,

于是我们回到dyld工程,搜索ImageLoaderMachO::doModInitFunctions:

代码语言:javascript复制
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
    Initializer func = (Initializer)((uint8_t*)this->machHeader()   funcOffset);
    ...
    func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
    ...
}

(滑动显示更多)

继续往上一层搜索

ImageLoaderMachO::doInitialization

代码语言:javascript复制
bool ImageLoaderMachO::doInitialization(const LinkContext& context){
    ...
    //点击doModInitFunctions,会跳到ImageLoaderMachO::doModInitFunctions
    doModInitFunctions(context);
    ...
}

(滑动显示更多)

搜索ImageLoader::recursiveInitialization

代码语言:javascript复制
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
  InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    //初始化镜像文件(点击doInitialization会跳转ImageLoaderMachO::doInitialization)
    bool hasInitializers = this->doInitialization(context);
}

(滑动显示更多)

接下来我们做一个测试:

ViewController.m里:

代码语言:javascript复制
@implementation ViewController
  (void)load{
    [super load];
    NSLog(@"我是ViewController load函数");
}
...
@end

(滑动显示更多)

main.m文件里:

代码语言:javascript复制
int main(int argc, char * argv[]) {
    NSLog(@"我是main.m        main函数");
    ...
}
///定义一个C  函数
__attribute__((constructor)) void SSJFun(){
    printf("n我是main.m        SSJFun函数n");
}

(滑动显示更多)

目的是看一下这三个的打印顺序,看运行结果:

我们发现,执行顺序load函数 > C 函数(SSJFun) > main函数,那么这是为什么呢?我们分析一下。

打开objc源码,嗖嗖load_images:

load_images 会调用所有load方法

代码语言:javascript复制
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    ...
    // Discover load methods
    {
        ...
        prepare_load_methods((const headerType *)mh);
    }
    ...
}

(滑动显示更多)

进入prepare_load_methods:

代码语言:javascript复制
void prepare_load_methods(const headerType *mhdr)
{
    ...
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i  ) {
        // 进入类的load方法
        schedule_class_load(remapClass(classlist[i]));
    }
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    ...
}

(滑动显示更多)

进入schedule_class_load:

代码语言:javascript复制
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ...
    if (cls->data()->flags & RW_LOADED) return;

    schedule_class_load(cls->getSuperclass());
    
    add_class_to_loadable_list(cls);
    
    cls->setInfo(RW_LOADED); 
}

/**
    我们发现schedule_class_load是一个递归调用函数,
    会沿着cls及其父类一层层执行add_class_to_loadable_list
*/

(滑动显示更多)

进入add_class_to_loadable_list:

代码语言:javascript复制
void add_class_to_loadable_list(Class cls)
{
    IMP method;
    ...
    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no  load method
    ...
    /**
        loadable_classes[loadable_classes_used]得到的是结构体,
        将类名和load方法IMP写到loadable_classes_used(下标)对应的结构体里
    */ 
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used  ;
}
/**
    我们发现,schedule_class_load在递归查找method的IMP,
    具体是什么方法的IMP我们还不得而知,继续往下看~
*/

(滑动显示更多)

进入getLoadMethod:

代码语言:javascript复制
IMP 
objc_class::getLoadMethod(){
    ...
    const method_list_t *mlist;
    ...
    mlist = ISA()->data()->ro()->baseMethods();
    if (mlist) {
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name());
            if (0 == strcmp(name, "load")) {
                return meth.imp(false);
            }
        }
    }
    return nil;
}
/**
    我们发现,getLoadMethod是寻找load方法的imp的过程,
*/

(滑动显示更多)

回到 -> prepare_load_methods函数 继续分析

代码语言:javascript复制
void prepare_load_methods(const headerType *mhdr)
{
    ...
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i  ) {
        // 进入类的load方法
        schedule_class_load(remapClass(classlist[i]));
    }
    /**
        经过上面几个函数的分析,我们可以得出结论:
        上面这段for循环,目的是把所有主类的继承链关系类的load方法装载到loadable_classes里
    */

    //接下来看看分类做了什么
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    ...
    add_category_to_loadable_list(cat);
}

(滑动显示更多)

进入add_category_to_loadable_list:

代码语言:javascript复制
void add_category_to_loadable_list(Category cat)
{
    IMP method;
    ...
    method = _category_getLoadMethod(cat);
    if (!method) return;
    ...
    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used  ;
}

// 里面就不一一分析了,跟前面的类一样,
// 也是寻找load方法的IMP,并且写入loadable_categories的过程。

(滑动显示更多)

继续回到load_images函数:

代码语言:javascript复制
load_images(const char *path __unused, const struct mach_header *mh)
{
    ...
    // 添加类的load方法   到loadable_classes、
    // 添加分类的load方法 到loadable_categories
    prepare_load_methods((const headerType *)mh);

    // -> 接下来分析这个函数
    call_load_methods();
}

(滑动显示更多)

进入call_load_methods:

代码语言:javascript复制
void call_load_methods(void)
    ...
    do {
        // loadable_classes_used已经在prepare_load_methods函数里赋值,统计load的个数,也可以认为是下标吧
        while (loadable_classes_used > 0) {
            //这里面就是调用Class的load方法
            call_class_loads();
        }
        //这里面就是调用category的load方法
        more_categories = call_category_loads();
    } while (loadable_classes_used > 0  ||  more_categories);
    ...
}

(滑动显示更多)

到此,我们可以得出一个结论,load_images函数会调用:

  • 所有非懒加载Class的load方法
  • 所有非懒加载category的load方法

为什么C 方法会自动调用,什么时候调用?

我们先在SSJFun方法打个断点,控制台bt一下看看堆栈信息:

我们发现,SSJFun是由dyld_sim的上层函数doModInitFunctions调用的,

而doModInitFunctions是由doInitialization调用的。

打开dyld源码,搜索doInitialization,我们找到这段代码:

代码语言:javascript复制
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
  InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
  {
      ...
      context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
      bool hasInitializers = this->doInitialization(context);
      ...
  }
  /**
  notifySingle在前面已经分析过类,它最终会调用_dyld_objc_notify_register的第二个参数,也就是load_images函数;
  load_images则会调用load方法;
  notifySingle在doInitialization之前先执行,
  所以`load方法`在`SSJFun方法`之前调用就很明白了。
  */

(滑动显示更多)

为什么main函数最后执行?

我们知道,最先执行的是_dyld_start,dyld源码搜素_dyld_start:

发现_dyld_start最后会调起main()函数;

回到工程,打开DeBug调试:

我们发现_dyld_start执行完之后,确实会执行main函数,再一次证明了main函数是在dyld之后执行。

registerObjCNotifiers里初始化的函数,分别在哪里调用呢?

我们知道,_objc_init里进行一系列初始化后,会调用_dyld_objc_notify_register函数,继而会进入dyld的registerObjCNotifiers:

代码语言:javascript复制
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
    sNotifyObjCMapped = mapped;

    sNotifyObjCInit = init;

    sNotifyObjCUnmapped = unmapped;
    ...
}

(滑动显示更多)

找寻sNotifyObjCMapped调用

全局搜索sNotifyObjCMapped,找到它的调用函数notifyBatchPartial,搜索notifyBatchPartial找到它的上层调用:

找寻sNotifyObjCInit调用

全局搜索sNotifyObjCInit,找到它的调用函数notifySingle,搜索notifySingle找到上层调用:

代码语言:javascript复制
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
  InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    ...
    // 有依赖才会调用
    /** 举个例子,比如是A库依赖于B库:
    第一次recursiveInitialization调用,由于A库的初始化是在doInitialization完成,
    所以第一次进来的时候A库为空,自然不存在所谓的依赖库,第一个notifySingle不执行
    */
    context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
    // 初始化
    bool hasInitializers = this->doInitialization(context);
    ...
    context.notifySingle(dyld_image_state_initialized, this, NULL);
    ...
}

(滑动显示更多)

这边做个总结,应用程序从启动到objc_init:

代码:

链接:pan.baidu.com/s/1Bse22q_f…

密码:du3f(包含Demo、dyld源码、libdispatch源码、Libsystem源码、objc4源码)

原文链接:https://juejin.cn/post/6989269646247460872

0 人点赞