iOS程序启动->dyld加载->runtime初始化(初识)

程序的开始main函数与Coding生涯的开始hello World!.png

iOS开发中,main函数是我们熟知的程序启动入口,但实际上并非真正意义上的入口,因为在我们运行程序,再到main方法被调用之间,程序已经做了许许多多的事情,比如我们熟知的runtime的初始化就发生在main函数调用前,还有程序动态库的加载链接也发生在这阶段,本文主要对从程序启动到main函数中发生的主要事情进行简单介绍。

其实简单总结起来就是:

系统先读取App的可执行文件(Mach-O文件),从里面获得dyld的路径,然后加载dyld,dyld去初始化运行环境,开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。最后dyld返回main函数地址,main函数被调用,我们便来到了熟悉的程序入口。

下面我们将结合代码对整个过程进行分析:

dyld加载

这里先说下Mach-O文件。

Mach-O文件格式是 OS X 与 iOS 系统上的可执行文件格式,像我们编译过程产生的.O文件,以及程序的可执行文件,动态库等都是Mach-O文件。它的结构如下:

mach-o文件.jpg

有如下几个部分组成:

Header:保存了一些基本信息,包括了该文件运行的平台、文件类型、LoadCommands的个数等等。

LoadCommands:可以理解为加载命令,在加载Mach-O文件时会使用这里的数据来确定内存的分布以及相关的加载命令。比如我们的main函数的加载地址,程序所需的dyld的文件路径,以及相关依赖库的文件路径。

Data: 这里包含了具体的代码、数据等等。

我们可以通过Mach-O文件查看器MachOView查看一个测试项目(这里放上地址)编译后的可执行文件内容:

Mach-O文件内容.png

这里可以看到,程序需要的dyld的路径在LC_LOAD_DYLINKER命令里,一般都是在/usr/lib/dyld 路径下。这里的LC_MAIN指的是程序main函数加载地址,下面还有写LC_LOAD_DYLIB指向的都是程序依赖库加载信息,如果我们程序里使用到了AFNetworking,这里就会多一条名为LC_LOAD_DYLIB(AFNetworking)的命令,如下图

三方库.png

这里可以看到一些我们比较常用的三方库:AFNetworking,IQKeyboard等。

系统加载程序可执行文件后,通过分析文件来获得dyld所在路径来加载dyld,然后就将后面的事情甩给dyld了。

从dyld开始

dyld: (the dynamic link editor)动态链接器,其源码是开源的

ImageLoader: 用于辅助加载特定可执行文件格式的类,程序中对应实例可简称为image(如程序可执行文件,Framework库,bundle文件)。

dyld接手后得做很多事情,主要负责初始化程序环境,将可执行文件以及相应的依赖库与插入库加载进内存生成对应的ImageLoader类的image(镜像文件)对象,对这些image进行链接,调用各image的初始化方法等等(注:这里多数事情都是递归的,从底向上的方法调用),其中runtime也是在这个过程中被初始化,这些事情大多数在dyld:_mian方法中被发生,我们可以看段简洁的代码:

dyld::_main函数代码.png

这里的_main函数是dyld的函数,并非我们程序里的main函数。

1.sMainExecutable = instantiateFromLoadedImage(....)与loadInsertedDylib(...)

这一步dyld将我们可执行文件以及插入的lib加载进内存,生成对应的image。

sMainExecutable对应着我们的可执行文件,里面包含了我们项目中所有新建的类。

InsertDylib一些插入的库,他们配置在全局的环境变量sEnv中,我们可以在项目中设置环境变量DYLD_PRINT_ENV为1来打印该sEnv的值。

环境变量设置.png

运行程序Log如下:

打印出插入库的log.png

可以看到插入的库为:libBacktraceRecording.dyliblibViewDebuggerSupport.

有时我们会在三方App的Mach-O文件中通过修改DYLD_INSERT_LIBRARIES的值来加入我们自己的动态库,从而注入代码,hook别人的App(相关资料)。

2.link(sMainExecutable,...)和link(image,....)

对上面生成的Image进行进行链接。其主要做的事有对image进行load(加载),rebase(基地址复位),bind(外部符号绑定),我们可以查看源码:

link方法.png

recursiveLoadLibraries(context, preflightOnly, loaderRPaths)

递归加载所有依赖库进内存。

recursiveRebase(context)

递归对自己以及依赖库进行复基位操作。在以前,程序每次加载其在内存中的堆栈基地址都是一样的,这意味着你的方法,变量等地址每次都一样的,这使得程序很不安全,后面就出现ASLR(Address space layout randomization,地址空间配置随机加载),程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行计算修复才能正常访问。

recursiveBind(context, forceLazysBound, neverUnload)

对库中所有nolazy的符号进行bind,一般的情况下多数符号都是lazybind的,他们在第一次使用的时候才进行bind。

3.initializeMainExecutable()

这一步主要是调用所有image的Initalizer方法进行初始化。这里的Initalizers方法并非名为Initalizers的方法,而是C++静态对象初始化构造器,atribute((constructor))进行修饰的方法,在LmageLoader类中initializer函数指针所指向该初始化方法的地址。

initiallizer函数指针.jpg

我们可以在程序中设置环境变量DYLD_PRINT_INITIALIZERS为1来打印出程序的各种依赖库的initializer方法:

可以打印出调用了Initalizers的image的.png

运行程序,系统Log打印如下:

lnitializer调用log.png

(由于打印的比较长,这样就截取开头的log)可以看到每个依赖库对应着一个初始化方法,名称各有不同。

这里最开始调用的libSystem.dylib的initializer function比较特殊,因为runtime初始化就在这一阶段,而这个方法其实很简单,我们可以在这里看到init.c源码,主要方法如下:

libSystem_initializer方法.jpg

其中libdispatch_init里调用了到了runtime初始化方法_objc_init.我们可以、在程序中打个符号断点来验证:

_objc_init符号断点.png

运行程序,然后断点命中,我们来看下调用栈:

objc_init调用栈.png

这里可以看到_objc_init调用的顺序,先libSystem_initializer调用libdispatch_init再到_objc_init初始化runtime。

runtime初始化后不会闲着,在_objc_init中注册了几个通知,从dyld这里接手了几个活,其中包括负责初始化相应依赖库里的类结构,调用依赖库里所有的laod方法。

就拿sMainExcuatable来说,它的initializer方法是最后调用的,当initializer方法被调用前dyld会通知runtime进行类结构初始化,然后再通知调用load方法,这些目前还发生在main函数前,但由于lazy bind机制,依赖库多数都是在使用时才进行bind,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行的。

main函数被调用

当所有的依赖库库的lnitializer都调用完后,dyld::main函数会返回程序的main函数地址,main函数被调用,从而代码来到了我们熟悉的程序入口。

main函数入口.png

结语

这里只是简单了概括了从程序启动->dyld加载依赖库->runtime初始化->main 的过程。但这阶段还有很多事情未讲,如果想深入了解还得结合源码来学习,这里我已经将dyld和runtime源码都放在这了,大家可直接下载,也可以从opensource-apple下载。

再唠嗑会

dyld源码前前后后读个大概懂,花了我3个多礼拜的空闲时间,由于C和C++基础并不是很好,所以特意跑回学校买了几本书补了下基础,不过读源码的这段时间还是挺累的。

为什么要去读源码,主要是看别人的文章时并不能很好解决我的某些疑问,而且只有真正去认识源码,去亲身体会才能加深对它的理解。

学习的旅途虽然颇累,但一路下来收获颇多。加油!

前行路,路漫漫,一人一酒似逍遥。

一张图.jpg

参考资料

1.Mach-O 可执行文件

2.dylib动态库加载过程分析

3.iOS 程序 main 函数之前发生了什么

4.今日头条iOS客户端启动速度优化

5.App 启动时间:过去,现在和未来

6.优化 App 的启动时间

7.dyld在hook方面的小东西

喜欢的话点个喜欢呗^_^

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

推荐阅读更多精彩内容