先看一个断点
App Launch 以及 dyld
自行下载参考dyld源码,下面只是我的简要记录
dyldbootstrap::start
{
rebaseDyld 符号便宜aslr address layout random
__guard_setup 栈溢出保护
dyld::_main
}
rebaseDyld {
遍历所有固定的 chains 然后 rebase dyld
所有基于修正链的映像的基地址为零,因此slide == 加载地址
// now that rebasing done, initialize mach/syscall layer
mach_init(); // from libc.a
}
重点分析的方法
dyld::_main分析
初始化程序运行环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38//获取主程序的macho_header结构以及主程序的slide偏移值
sMainExecutableMachHeader = mainExecutableMH;
sMainExecutableSlide = mainExecutableSlide;
//获取主程序路径
// Pickup the pointer to the exec path.
sExecPath = _simple_getenv(apple, "executable_path");
if (!sExecPath) sExecPath = apple[0];
if ( sExecPath[0] != '/' ) {
// have relative path, use cwd to make absolute
....
}
//获取进程名称
// Remember short name of process for later logging
sExecShortName = ::strrchr(sExecPath, '/');
if ( sExecShortName != NULL )
++sExecShortName;
else
sExecShortName = sExecPath;
//配置进程受限模式
configureProcessRestrictions(mainExecutableMH, envp);
//检测环境变量
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
`所谓的启动优化就是在这里,添加dyly参数,进行打印的`
//判断是否设置了sEnv.DYLD_PRINT_OPTS以及sEnv.DYLD_PRINT_ENV,分别打印argv参数和envp环境变量
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
//获取当前程序架构
getHostInfo(mainExecutableMH, mainExecutableSlide);加载共享缓存 shared cache
mapSharedCache();实例化主程序,并赋值给ImageLoader::LinkContext
1
2
3
4
5
6
7
8//
try {
// add dyld itself to UUID list
addDyldImageToUUIDList();
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
}加载插入的动态库++++++++++++++++++++
1
2
3
4
5// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}链接主程序++++++++++++++
1
2
3gLinkContext.linkingMainExecutable = true;
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();链接插入的动态库++++++++
1
2
3
4
5
6
7
8
9...
ImageLoader::applyInterposingToDyldCache(gLinkContext);
// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
}
}在链接所有插入的image后,执行弱绑定++++++++++++++++++++++++++++++
1
2
3
4
5// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);
gLinkContext.linkingMainExecutable = false;
sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);执行所有的初始化方法+++++++++++++++++++++
1
2// run all initializers
initializeMainExecutable();查找主程序的入口点并返回
1
2
3
4
5// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
return result;
}
总结dyld::_main主要做了以下操作(就不一一分析了):
主程序运行环境初始化及配置,拿到Mach-O头文件 (macho_header里面包含整个Mach-O文件信息其中包括所有链入的动态库信息)
加载共享缓存 shared cache
实例化主程序,并赋值给ImageLoader::LinkContext
加载所有插入的动态库,将可执行文件以及相应的依赖库与插入库加载进内存生成对应的ImageLoader类的image(镜像文件)对象
链接主程序(必须先链接主程序后才能插入)
链接所有的动态库ImageLoader的image(镜像文件)对象,并注册插入的信息,方便后续进行绑定
在链接完所有插入的动态库镜像文件之后执行弱绑定
执行所有动态库image的初始化方法initializeMainExecutable
查找主程序的入口点LC_MAIN并返回result结果,结束整个_dyld_start流程,进入我们App的main()函数!
这里解释一下共享缓存机制:网上自己查询 dyld1.0版本 - dyld3 版本, 当前使用dyld3版本
dyld加载时,为了优化程序启动,在dyld::_main中启用了共享缓存(shared cache)技术。共享缓存会在进程启动时被dyld映射到内存中,之后,当任何Mach-O映像加载时,dyld首先会检查该Mach-O映像与所需的动态库是否在共享缓存中,如果存在,则直接将它在共享内存中的内存地址映射到进程的内存地址空间。在程序依赖的系统动态库很多的情况下,这种做法对程序启动性能会有明显提升。
分析一下_main的第8步,initializeMainExecutable() 倒数第二步
自行根据下面的方法阅读源码initializeMainExecutable -> runInitializers -> processInitializers -> recursiveInitialization -> notifySingle
最后我想说的就是这个notifySingle,
1 | context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo); |
找到一个关键的函数指针* sNotifyObjCInit, 全局搜索找到赋值
1 | void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped) |
全局搜索,看看registerObjCNotifiers这个方法会被谁调用,找到调用的地方_dyld_objc_notify_register函数
不用找了,在objc的源码里面,
_dyld_objc_notify_register
是_objc_init
进行调用的。而_objc_init函数则是Runtime的入口函数!
打开Objc源码,搜索_objc_init
1 | /*********************************************************************** |
看 _dyld_objc_notify_register
注释
1 | * _objc_init |
_objc_init的调用时机是在其他动态库加载之前由libSystem系统库先调用的。
那么到现在就很明确了,其实在dyld::_main主程序的第8步,初始化所有动态库及主程序的时候之前,就先注册了load_images的回调,之后在Runtime调用load_images加载完所有load方法之后,就会回调到dyld::_main的initializeMainExecutable()内部执行回调。
map_images
自行下载objc源码, 找打 _objc_init
方法
1 | /*********************************************************************** |
map_images直接返回了map_images_nolock,直接看实现
1 | void |
总结map_images_nolock的流程就是:
- 判断firstTime,firstTime为YES,则执行环境初始化的准备,为NO就不执行
- 计算class数量,根据总数调整各种表的大小并做了GC相关逻辑处理(不支持GC则打印提示信息)
- 判断firstTime,firstTime为YES,执行各种表初始化操作,为NO则不执行
- 执行
_read_images
进行读取,然后将firstTime置为NO,就不再进入上面的逻辑了,下次进入map_images_nolock就开始直接_read_images
接下来我们重点分析_read_images
1 | /*********************************************************************** |
_read_images的实现主要分为以下步骤:
- 重新初始化TaggedPointer环境
- 开始遍历头文件,进行类与元类的读取操作并标记(旧类改动后会生成新的类,并重映射到新的类上)
- 读取@selector方法
- 读取协议protocol
- 处理分类category,并rebuild重建这个类的方法列表method list
两个表,一个叫gdb_objc_realized_classes
用来存放已命名的类的列表,另一个叫allocatedClasses
用来存放已分配的所有类(和元类)
逐步分析 readClass、@selector、 protocol、 category
从源码中可以看出, readClass 方法有返回值,并且包含三种逻辑处理:
- 找不到该类的父类,可能是弱绑定,直接返回nil;
- 找到类了,判断这个类是否是一个future的类(可以理解为需要实现的一个类,也可以理解为这个类是否有变化),如果有变化则创建新类,并把旧类的数据拷贝一份然后赋值给新类newCls,然后调用addRemappedClass进行重映射,用新的类替换掉旧的类,并返回新类newCls的地址
- 找到类了,如果类没有任何变化,则不进行任何操作,直接返回class
从readClass的底层实现部分做个延伸思考:日常开发中,对于已经启动完成的工程项目,如果我们未修改任何类的数据,那么再次点击运行会很快完成,但是一旦我们在对这些类进行修改后,在读取这些类的信息(包括类本身的信息以及下面我们要继续分析的协议protocol、分类category、方法selector),就需要对该类的数据进行更新,这个更新实际上是新建一个类,然后拷贝旧类的数据赋值给新类,然后重映射并用新类替换掉新类,这里面的拷贝以及读写过程其实是相当耗时的!这是类信息改动之后项目再次Run运行起来会比较慢的原因之一。
已经读取完成的类,会被存放到了这个表gdb_objc_realized_classes
里面!
分析源码注释及源码得出,addClassTableEntry
里面会把这个读取完成的类直接先添加到allocatedClasses
表里面,然后再判断addMeta
是否为YES,然后会把当前这个类的元类metaClass也添加到allocatedClasses
这个表里面。
@selector
点击sel_registerNameNoLock,找到__sel_registerName,在它里面找到关键代码
逻辑其实就是:把方法名插入并存储到namedSelectors这个表里面.
protocol
找到关键函数readProtocol,进入发现其实读取protocol的操作是把protocol存进协议表protocol_map。
category
分类category的读取,里面主要做了下面这些步骤:
- 从头文件中获取所有的分类列表catlist,然后循环遍历这个列表
- 在循环中,判断当前分类cat所属的类是否存在,如果不存在则把这个分类置为空catlist[i] = nil; 如果这个分类所属的类存在,那么开始下面两个步骤:
- 第一个步骤:判断这个分类cat中是否有实例方法instanceMethods,协议protocols以及属性实例instanceProperties,如果有,那么进入remethodizeClass,重新rebuild当前类cls的方法列表
- 第二个步骤:继续判断这个分类cat中是否有类方法classMethods,协议protocols以及类属性_classProperties,然后重新rebuild当前类所对应元类cls->ISA()的方法列表。
loadimage
调用load方法,父类-子类-所有分类
总结
_objc_init 是系统库,很早就已经 由libSystem系统库先调用的,初始化了很多环境 其中有_dyld_objc_notify_register(&map_images, load_images, unmap_image);
dyld 初始化进程环境,链接动态库,进行 _dyld_objc_notify_register 注册
参考
https://www.jianshu.com/p/ea680941e084