一、序言
作为iOSer,我们天天在xcode上写代码,然后打包发布或者测试,可曾想过代码是如何到内存种的?app是如何从点击图标到整个程序运行的呢?现在我们来探究一下。
二、库
什么是库?库就是能够被系统加载到内存中的可执行的二进制文件,库有这种形式: 静态库(.a
.o
等)和动态库(.framework
.so
.dylib
等)
1、动态库和静态库的区别
我们梳理一下库的加载过程,首先我们的代码(.h
.m
.cpp
.swift
等)经过预编译、编译成汇编,然后链接装载生成可执行文件,大致流程图如下:
1.1、静态库
链接时,静态库会被挨个挨个完整地装载到可执行文件中,被多次使用就有多份冗余拷贝,如图:
图中会很明显的有重复的库,这样就会浪费很多性能。
1.2、动态库
动态库在加载时才进行链接,比如加载到某个节点发现需要某一个库时并不是直接加载,而是共享这个动态库。相比于静态库,动态库会被共用,这样会节省更多的空间和时间。加载的大致流程如下所示:
三、链接器
库是如何加载到内存种的呢?这就需要一个非常重要的东西了dyld
。
1、App的启动入口
App是如何启动的呢?我们都知道所有程序的入口都是main
函数,那么我们来验证一下。首先创建一个空的iOS项目,并在main
函数上打个断点,执行程序,如图:
很容易发现在main
之前有个start_sim
,我们下个下个符号断点start
,再次运行程序,如图:
很遗憾还是在这里,好像失去了方向。但是经验告诉我们有个方法会在main
之前执行,对,就是load
方法,我们随便添加个load
并断点,如图:
运行起来,如图:
山穷水复疑无路,柳暗花明又一村。赶紧查看一下堆栈信息,如图:
好像这里才是我们程序的起点。
2、APP的启动流程
刚刚我们找到了APP的启动入口,那么从启动到main
函数这个中间经历了什么呢?干了什么事情呢?我们需要到dyld
的源码中寻找,打开dyld
的源码,搜索_dyld_start
(由于源码编译依赖太多就没有流程可以跟踪。还有因系统版本Xcode
打开dyld
源码闪退的问题,可以将dyld
的源码拖入到新建的工程中,不要copy
,可以避免闪退),如下所示:
全在深奥的汇编中,这不要紧,因为这里都写的注释。我们很容易发现所有的架构类型中都有call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
这个注释,直接搜索dyldbootstrap::
没有任何结果,这是c++
语法,要先找到命名空间dyldbootstrap
,如图:
然后再搜索start(
,如图所示:
这里返回的是个dyld::_main(
我们再次去找实现,同样先找命名空间,在找函数,如图:
13180
条结果,太多了,过滤一下,搜索namespace dyld {
,如图:
锐减到14
条结果,挨个搜索_main(
,找到了实现如下所示:
好长,收起来一看近1000
行代码,如图:
为了找准核心代码,我们可以直接渠道最后查看返回值,然后根据返回值找关键的东西,如图:
返回值是result
,那么找出result
赋值相关的代码,如图:
fake_main
返回值是0,如图:
所以可以忽略不看,那么result
的值很大可能与sMainExecutable
相关。接下来找sMainExecutable
相关的东西,如图:
这里的绑定可以说明sMainExecutable
就是我们需要找的代码。所以我们要看sMainExecutable
的初始化,如图:
然后进入到instantiateFromLoadedImage
方法中,如图:
大概意思是为已经映射到可执行文件中的对象创建一个ImageLoader*
,然后往下,如图:
加载动态库,然后如图:
link
主程序,再然后:
link
库。在所有镜像文件都链接完毕之后进行弱引用绑定符号表:
然后初始化执行主程序:
具体代码如图:
即拿到各个镜像文件,然后runInitializers
,如图:
其中processInitializers
代码如下:
第一个for
大概意思就是对镜像文件列表中的镜像文件递归初始化,第二个for
是继续初始化向上以来的文件。具体的初始在recursiveInitialization
中,继续进去查看,如图:
里面是个try catch
,那么我们从try中查找即可,如图:
再继续看notifySingle
,如图:
notifySingle
定义的是一个函数,接下来来就需要找到他赋值的地方,如图:
在然后实现的地方,如图:
接下来看1112
行的sNotifyObjCInit
,搜索到定义和赋值如下:
sNotifyObjCInit
的赋值源于registerObjCNotifiers
函数的第二个参数,所以继续搜索registerObjCNotifiers
的调用,如图:
继续搜索,如图:
貌似没有找到调用,线索断了?放松一下。
3、从_objc_init
开始的逆向推导
打开objc
源码搜索registerObjCNotifiers
,如图:
发现在_objc_init
有调用registerObjCNotifiers
,且objc
源码是可编译的,断点伺候,如图:
点击_os_object_init
,如图:
在libdispatch.dylib
库中的_os_object_init
有调用_objc_init
,所以我们去libdispatch.dylib
查找_os_object_init
,如图:
果真在_os_object_init
有调用_objc_init
,继续往下找,如图:
在同一个库中的libdispatch_init
中找到了_os_object_init
的调用。那么libdispatch_init
的调用是在哪里呢?当前库也搜索不到,所以我们回到objc
源码中,在控制台输入bt
查看堆栈信息,如图:
在libSystem.B.dylib
中有调用,那么我们去libSystem.B.dylib
查找libdispatch_init
,如图:
在libSystem
库的libSystem_initializer
调用了libdispatch_init
,继续搜索libSystem_initializer
并没有找到的调用,再看堆栈信息,在dyld
中的doModInitFunctions(ImageLoader::LinkContext const&)
调用,所以回到dyld
中搜索ImageLoaderMachO::doModInitFunctions
,如图:
根据注释判断出该方法是加载libSystem
的。其中func
为libSystem_initializer
,根据参数判断。然后查找doModInitFunctions
的调用,如图:
再查找doInitialization
的调用处,如图:
再次回到了recursiveInitialization
中,大概理清楚了通知注册的方法,由于目前xcode
直接打开dyld
库会闪退,影响部分分析,请各位大神多多指导。