iOS应用程序加载大致流程分析

前言

今天我们重点来分析一下,iOS App运行时,在main()方法执行之前,程序到底做了哪些事?

准备工作

示例,新建一个iOS应用工程,查看方法加载的顺序

__attribute__((constructor)) void lg_cFunction() {
//    printf();
    NSLog(@"%s -- 来了", __func__);
}

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        NSLog(@"%s -- 来了", __func__);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
---------------------------------分割线---------------------------------
@implementation ViewController

+ (void)load {
    NSLog(@"%s -- 来了", __func__);
}
@end

运行后

2020-09-28 11:06:39.756959+0800 Test1[49592:4931930] +[ViewController load] -- 来了
2020-09-28 11:06:39.757473+0800 Test1[49592:4931930] lg_cFunction -- 来了
2020-09-28 11:06:39.757671+0800 Test1[49592:4931930] main -- 来了
2020-09-28 11:06:39.800886+0800 Test1[49592:4931930] result is 0

发现当前3个方法的调用顺序是load --> lg_cFunction(c++方法) -->main入口,why?

编译过程

带着上述方法调用顺序的疑问,我们先来大致了解下,App编译的一个过程:


大致流程是:
源文件(.h .m .cpp)-->预编译(检查语法)-->编译(转化为汇编)-->汇编(生成机器码文件) -->链接(也包括一些库的链接)-->生成可执行文件(在生成的.app中右键打开包文件,里头的exec)
其中链接这一步苹果系统使用的就是dyld库来完成的。那什么是dyld呢?

dyld动态链接器

dyld 是英文 the dynamic link editor 的简写,翻译过来就是动态链接器,是苹果操作系统的一个重要的组成部分。在 iOS/Mac OSX 系统中,仅有很少量的进程只需要内核就能完成加载,基本上所有的进程都是动态链接的,所以 Mach-O 镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容的填补,这个填补工作就是由 动态链接器dyld来完成的,也就是符号绑定

找入口

你想知道系统调用load之前走了什么流程?很简单,在load方法里打断点,然后lldb bt指令查看调用堆栈信息。

指令名称 释义
bt 查看调用堆栈信息,加all可打印多有thread的堆栈


上图红框处可知,最开始是从_dyld_start_开始的,我们全局搜索_dyld_start_,寻找入口。
_dyld_start_.png

看注释,我们注意到会调用dyldbootstrap::start
image.png

再次全局搜索dyldbootstrap,找到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>
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

    // if kernel had to slide dyld, we need to fix up load sensitive locations
    // we have to do this before using any global variables
    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

    // now that we are done bootstrapping dyld, call dyld's main
    uintptr_t appsSlide = appsMachHeader->getSlide();
    return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

最终调用dyld::_main,这个就是我们要找的入口!

dyld::_main流程大致分析

这个函数实现代码量巨大,我们一步步看。

  1. 首先看函数的返回值result,如下图:

    再搜索result的赋值地方,

上图是初始化

上图是一个if特殊情况条件里的返回,不考虑。


上图是个宏编译的if条件的,不考虑。


上图也是一个if条件中的赋值,并return,不作考虑。

同理,不考虑。


首先3是宏条件编译,不考虑,然后1和2是主要赋值的地方,都用到一个共同的变量sMainExecutable,应该是关键。

也是if条件里的,不考虑。

最终发现,赋值result的都是通过变量sMainExecutable,那我们再搜索sMainExecutable的赋值情况:

sMainExecutable.png

第一个就找到了,通过方法instantiateFromLoadedImage,再看注释// instantiate ImageLoader for main executable -->为主可执行文件实例化ImageLoader,接着我们重点看看instantiateFromLoadedImage函数

// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
    // try mach-o loader
    if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
        ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
        addImage(image);
        return (ImageLoaderMachO*)image;
    }
    
    throw "main executable not a known format";
}

关键代码是ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);

// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
    //dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
    //  sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
    bool compressed;
    unsigned int segCount;
    unsigned int libCount;
    const linkedit_data_command* codeSigCmd;
    const encryption_info_command* encryptCmd;
    sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
    // instantiate concrete class based on content of load commands
    if ( compressed ) 
        return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
    else
#if SUPPORT_CLASSIC_MACHO
        return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
        throw "missing LC_DYLD_INFO load command";
#endif
}

首先sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);

sniffLoadCommands.png

根据注释,应该是定义mach-o文件的格式,定义什么格式呢?
这时需要用到查看mach-o文件的软件MachOView,举个例子来看看:
先找到工程的.app文件所在位置:

再右键显示包内容:

选择exec可执行文件

拖入到MachOView中:

所以,sniffLoadCommands定义的就是mach-o里的区间Load Commands里的格式。
格式定义完成后,接着进行初始化instantiateMainExecutable

// instantiate concrete class based on content of load commands
    if ( compressed ) 
        return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
    else
#if SUPPORT_CLASSIC_MACHO
        return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
        throw "missing LC_DYLD_INFO load command";
#endif

不论是压缩模式ImageLoaderMachOCompressed,还是标准模式ImageLoaderMachOClassic,最终都是生成ImageLoader对象,完成一个初始化sMainExecutable的过程。

至此,我们通过返回值result反向搜索赋值,找到sMainExecutable的初始化流程(第6577行),那么在sMainExecutable的初始化前,又走了哪些流程?我们一点点的往下看:














上述所有图,大致描述了dyld::_main的大致流程:

  1. 环境变量配置
  2. 共享缓存处理
  3. 主程序表初始化
  4. 插入动态库
  5. 链接主程序表
  6. 链接动态库
  7. 弱符号绑定
  8. 初始化所有
  9. 主程序入口处理
第8步初始化流程详细分析

大家肯定想知道:我们平时写的对象到底是如何初始化的呢,说白了就是我们之前讨论的_objc_init是在哪里触发被调用的呢?带着这个问题,我们首先看看initializeMainExecutable源码:

再看看runInitializers源码:

继续processInitializers

继续recursiveInitialization


很明显,这里面,需要分成两部分探索,一部分是notifySingle函数,一部分是doInitialization函数

首先探索notifySingle函数
小技巧-->全局搜索notifySingle(函数

红框里是核心代码
再搜索sNotifyObjCInit,看看哪里赋值处理


上图赋值的是在函数registerObjCNotifiers,再搜索其被调用的地方

是在 _dyld_objc_notify_register进行了调用,但是_dyld_objc_notify_register的函数需要在libobjc源码中搜索

终于,是在 _objc_init中,这不正是我们最开始要找的问题所在吗,哈哈!
_objc_init源码中调用了_dyld_objc_notify_register,并传入了参数load_images,那么sNotifyObjCInit = load_images,而load_images中会调用所有的+load方法。

整个load方法的调用链路就是:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一个回调处理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)

load方法的调用栈信息

load调用栈

前面的流程跟我们之前分析的一样:_dyld_start-->dyld::main-->initialzeMainExecutetable()初始化主程序表-->dyld::notifySingle回调结果-->load_images加载文件,紧接着就调用了load方法,具体调用的位置如下图:
load调用

cxx方法调用栈信息

同理,在cxx方法处打断点,查看调用栈:

cxx调用

load不同的是,在recursiveInitialization之后,



和load不同的是,在doInitialization里触发的cxx方法。那我们具体看看doInitialization的流程是如何处理的。

先看doImageInit

再看doModInitFunctions,和doImageInit差不多的流程

接着搜索LC_SEGMENT_COMMAND

以64位为例,看看LC_SEGMENT_64

看来是_TEXT区间相关,我们查看mach-o符号文件:
_TEXT.png

所以doModInitFunctions就是在编译调用上图红框处的区间里所有的函数,其中就包含cxx函数的调用触发。

cxx函数调用链
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::doModInitFunctions-->func()

那么回到之前的问题,objc_init是在哪里被调用呢?在initializeMainExecutable没找到答案,那么接下来,只能使用终极大招了-->符号断点

符号断点查看objc_init


前面的流程和cxx函数调用大致相同,在第3步时,会调用libSystem库,再去到libdispatch库,然后触发_os_object_init-->_objc_init,请看下列图:



上图可知:objc_init调用链如下:

  1. _dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::doModInitFunctions
  2. libSystem_initlializer-->libdispatch_init-->_os_object_init-->_objc_init
  3. _objc_init-->注册观察者_dyld_objc_notify_register(&map_images, load_images, unmap_image)-->第2个参数load_images == sNotifyObjCInit,然后再在dyld加载的ImageLoader::recursiveInitialization这一步里notifySingle-->sNotifyObjCInit触发回调,让_objc_init和dyld加载过程形成一个闭环。
main入口

上面我们知道了loadcxx函数的调用链,还剩下main()了,它是在哪里被调用的呢?智能在cxx函数里打断点,然后打开汇编模式,跟着断点一步步看了。


然后按住按键control,点击step over

上图红框里发现,其实和之前分析的流程基本一致,还是回到了_dyld_start,看来我们只有回到最初的汇编代码里,去寻找main入口了:

在第3步也是_dyld_start的最后,找到了main(),此时才调用,当然比loadcxx函数的调用时机都晚!

总结

借用Style_月月iOS-底层原理 15:dyld加载流程的dyld加载流程图:

dyld加载流程

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