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

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

和谐学习!不急不躁!!我是你们的老朋友小青龙~

前面的文章,我们分析了alloc、类的结构、及消息发送的底层流程等等。
那么我们的代码如何加载到内存中的呢?这就是今天要探究的内容。

准备资料

image.png

整个编译过程大致分为:

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

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

  • 宏定义
  • 文件包含
  • 条件编译 宏定义
    又叫宏替换,只做替换不做计算。宏定义的写法如下:
#define 标识符 字符串
复制代码

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

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

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

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

条件编译

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

条件编译举例:
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里面定义、引用的文件、变量是全局的且只会编译一次,所以我们可以把常用的东西定义在里面。

可执行文件

image.png

动态库和静态库

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

加载方式: image.png

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

加载过程:

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

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

dyld分析

(dyld又叫动态链接器)

dyld流程:

dyld_start

dyldbootstrap::start

dyld::_main

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

        instantiateFromLoadedImage实例化主程序

        link主程序

        weakBind若引用绑定主程序

        notifyMonitoringDyldMain通知dyld可以进行main()函数调用
复制代码

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

image.png 我们发现,最先执行到的是dyld里的_dyld_start,接下来我们下载dyld源码

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

#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指令,调用了dyldbootstrapstart函数,我们在dyld工程里全局搜索dyldbootstrap:

image.png 定位到start函数

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返回值,以及我们一开始就知道的程序加载流程,来分析核心代码:

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

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

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

点击进入processInitializers

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

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

gLinkContext.notifySingle = &notifySingle;
复制代码

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

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)

{
    ...
    (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
    ...
}
复制代码

搜索sNotifyObjCInit

image.png

我们发现,sNotifyObjCInit的赋值来源于registerObjCNotifiers函数的第二个参数,接下来我们搜一下registerObjCNotifiers看看哪里调用了:

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,所以我们下一个符号断点看看哪里调用了:

image.png 运行

image.png 我们发现,_dyld_objc_notify_register_objc_init调用。

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

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

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

image.png 按照先后顺序,_objc_init是在recursiveInitialization之后执行,
_dyld_objc_notify_register就像block回调一样, 由notifySingle声明,最后_objc_init里调用。
_objc_init一顿初始化操作之后,调用_dyld_objc_notify_register告诉dyld我已经初始化完毕了。

而中间的recursiveInitializationdoInitialization等我们没有办法从上一步的recursiveInitialization继续分析下去,所以我们这里采用逆向推导思维:由结果反推
_objc_init里会调用_dyld_objc_notify_register,这个我们在前面已经分析了。

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

打开libdispatch工程,搜索_objc_init

image.png

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

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

DISPATCH_EXPORT DISPATCH_NOTHROW
void
libdispatch_init(void)
{
    ...
    _os_object_init();
    ...
}
复制代码

libdispatch_initlibSystem_initializer发起,搜索: image.png 发现libSystem_initializer源自于Libsystem库。

Libsystem工程里搜索libSystem_initializer

__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

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

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

搜索ImageLoader::recursiveInitialization

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里:

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

main.m文件里:

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

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

image.png 我们发现,执行顺序load函数 > C++函数(SSJFun) > main函数,那么这是为什么呢?我们分析一下。
打开objc源码,嗖嗖load_images

load_images 会调用所有load方法

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

进入prepare_load_methods

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

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

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

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函数 继续分析

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

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函数:

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

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一下看看堆栈信息:

image.png 我们发现,SSJFun是由dyld_sim的上层函数doModInitFunctions调用的,而doModInitFunctions是由doInitialization调用的。
打开dyld源码,搜索doInitialization,我们找到这段代码:

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

image.png 发现_dyld_start最后会调起main()函数;
回到工程,打开DeBug调试:

image.png

image.png

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

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

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

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找到上层调用:

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);
    ...
}
复制代码

image.png




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

WX20210721-005832@2x.png

代码:

链接:pan.baidu.com/s/1Bse22q_f…
密码:du3f
(包含Demo、dyld源码、libdispatch源码、Libsystem源码、objc4源码)

分类:
iOS
标签: