老规矩,先说一下本篇文章说的内容。有两大部分:分别是MachO
文件和DYLD
。
主要要说的是DYLD
加载image(不是图片,是镜像)
的总体流程。由于这两部分都是概念性的知识且MachO部分的内容相对来讲不算太多,因此就将两部分知识点用这一篇文章归纳算咯🐶。编程
标题是属于第二大块DYLD里面的内容,若是是冲着标题来的能够直接跳到DYLD去看。bootstrap
Mach-O为Mach object文件格式的缩写,它是一种用于可执行文件、目标代码、动态库的文件格式。缓存
.o
.a | .dylib | xxx.framework/xxx
.dsym
能够经过终端file + 文件路径
来查看文件的类型信息。bash
苹果公司提出的一种程序代码,可以同时兼容多种架构的二进制文件。如果不了解什么是架构,看下面:
arm6四、arm64e、armv七、armv7s
这些就是不一样的架构,具体有什么区别与联系这里再也不多说。 架构
写过SDK的童鞋对此应该比较熟悉,在合并真机包和模拟器包的时候会使用如下指令:app
lipo -create [真机编译路径/xxx.framework/xxx]
[模拟器编译路径/xxx.framework/xxx] -output [合并后输出的文件路径]
复制代码
想起来了吧?这个命令就是为了合并不一样架构(真机/模拟器)
的二进制文件。ide
合并的想起来了,别急,下面还有一个拆分的命令:函数
// 从通用二进制文件拆分出不一样的架构
lipo [通用二进制文件路径] -thin [要拆的架构] -output [拆出的二进制输出的路径]
复制代码
首先来整一张图。
从图中能够看出,MachO文件能够分为Header、Load commands、Data
三部分。post
回到标题,"main函数是整个程序的入口"
这句话想必你们从开始学习编程的那一刻就听到过了吧!可是有没有想过main函数为何是程序的入口?main函数在哪里调用的呢?以及main函数调用以前作了哪些事呢?性能
想知道上面问题的答案,慢慢往下看,dyld。
dyld(the dynamic link editor)是苹果的动态连接器,是苹果操做系统 一个重要组成部分,在系统内核作好程序准备工做以后,交由dyld负责余下的工做。并且它是开源的,任何人能够经过苹果官网下载它的源码来阅读理解它的运做方式,了解系统加载动态库的细节。dyld源码。
咱们知道,每个类都有一个load方法,而且这个load方法的调用时机特别的早,比程序入口main函数调用的还要早。
包括上篇文章的代码注入之因此写到load方法中也是由于这个缘由,在真正的代码逻辑执行以前就交换了某些类的方法,则在代码逻辑执行过程使用的方法就都是交换过之后的方法了。
为了探究dyld,咱们先在任意类的load方法上打一个断点,运行,能够看到在load方法执行前有9个调用堆栈。
点击调用堆栈中的第一个_dyld_start
可查看到汇编代码。注意断点的前一行dyldbootstrap::start
,和左边调用堆栈中的第二步相同,说明就是执行了这句代码跳到了堆栈第二步。(查询汇编bl
指令的含义就可验证这个猜测)。
另外若是对C++语言有所了解的话,应该就知道dyldbootstrap
是C++中的一个命名空间,start是这个命名空间的一个函数。
接下来咱们就在下载的dyld源码中全局搜索这个命名空间dyldbootstrap
而且查找里面是否是有一个start函数。
接下来咱们回过头看刚刚的调用堆栈,第三步是dyld::_main
,仔细一看这不正是咱们start函数最后返回值处调用的一个方法吗?
看到dyld::_main
,好了,main
函数找到了,main
函数调用以前也没干啥事啊?
哈哈😂,先别急说我是标题党,这个_main
并不是程序入口main
。跳进方法实现一看,这个方法实现600多行,还真很多。
_main
函数的代码实现吧。
Step1: 设置运行环境。
主要是设置主程序的运行参数、环境变量等。
将参数mainExecutableMH
赋值给了sMainExecutableMachHeader
,这是一个macho_header
结构体,表示的是当前主程序的MachO头部信息
。
// 将主程序的MachO头部信息赋值给sMainExecutableMachHeader
sMainExecutableMachHeader = mainExecutableMH; // MH = MachOHeader
// 同理,保存主程序的内存地址偏移值
sMainExecutableSlide = mainExecutableSlide;
复制代码
接着调用setContext()设置上下文信息,包括一些回调函数、参数、标志信息等。设置的回调函数都是dyld模块自身实现的,如loadLibrary()函数实际调用的是libraryLocator(),负责加载动态库。
// 设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
复制代码
配置进程是否受限、检查环境变量
// 配置进程是否受限
configureProcessRestrictions(mainExecutableMH, envp);
··· ···
// 检查环境变量
checkEnvironmentVariables(envp);
// 在Scheme中配置对于的环境变量可打印对应环境变量的值
// 配置相应的环境变量(DYLD_PRINT_OPTS/DYLD_PRINT_ENV)便可打印对应的信息
if (sEnv.DYLD_PRINT_OPTS) {
printOptions(argv);
}
if ( sEnv.DYLD_PRINT_ENV ) {
printEnvironmentVariables(envp);
}
复制代码
Step2: 加载共享缓存
这里要说一下iOS里的共享库、动态库和静态库的区别。
共享库(真正的动态库)
。"文件夹"
,把某些功能及所用到的资源文件所有放到这个"文件夹"
里面了,在程序编译连接期间静态库的代码会被编译到主程序的MachO文件中。通过上面的解释,想必已经知道加载共享缓存
是作什么的了吧?我认为就是读取共享库的缓存区,将App使用到的系统共享库没加载到内存的加载到内存,已经加载的记录一下内存地址。
// 检查共享缓存是不是禁用状态
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
··· ···
// 映射/加载共享缓存
mapSharedCache();
复制代码
检查共享缓存禁用状态那个方法跳进去看源码能够知道,iOS系统共享缓存没法被禁用。
Step3: 实例化主程序
听名字就知道这一步是作什么的。操做系统自己也是一个应用,只不过这个应用是用来管理其余应用的。既然是应用,那么这个应用内确定会有变量/对象,很明显,这一步就是经过主程序的相关信息建立一个主程序对象。此时的App相对于操做系统来讲就是其中的一个变量。
// 实例化主程序,建立主程序对象sMainExecutable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH,
mainExecutableSlide, sExecPath);
复制代码
跳到这个方法实现里面能够发现,这个方法就是建立来一个ImageLoader类型的对象image(这里不是图片,是指主程序的镜像)
而且添加到了某个地方保存起来。
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path) {
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
// 建立image对象,image指的是主程序
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
// 添加主程序镜像
addImage(image);
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}
复制代码
Step4: 加载插入的库
这里插入的库是dyld源码中注释的直译,我认为这里"插入的库"
应该指的就是动态库,包括App开发中用到的动态库以及咱们代码注入
时注入的动态库。
从代码上看是遍历DYLD_INSERT_LIBRARIES
环境变量指向的那个连续的空间,取出所有的插入的库,依次加载。
// load any inserted libraries
// 加载插入的库到内存。 所谓插入的库,其实就是非共享的动态库,由于静态库在编译期间就会变成主程序的一部分
if( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
// 加载所有的动态库
loadInsertedDylib(*lib);
}
// record count of inserted libraries so that a flat search will look at
// inserted libraries, then main, then others.
sInsertedDylibCount = sAllImages.size()-1;
复制代码
点进loadInsertedDylib(*lib);
方法发现里面又调用了一个load(path, context, cacheIndex);
方法,在这个方法中一层层的调用了loadPhase0、loadPhase一、loadPhase二、loadPhase三、loadPhase四、loadPhase五、loadPhase6
等方法,具体实现没有详细研究,大概看了一下发现有些对加载的库进行一些签名验证、cryptid判断等操做。(有兴趣的能够本身研究研究源码)
Step5: 连接主程序和插入的库
// 连接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
··· ···
// 连接插入的库
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
// 连接插入的库
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
复制代码
连接主程序和插入的库是分两次调用的,在AllImages中第一个image是主程序的image,后面才是插入的库的image。
经过link
函数连接主程序和插入的库,在link
函数中会递归的将当前image进行符号绑定。注意:这里符号绑定只会绑定nolazy的库,对于lazy标记的库会在运行时动态进行绑定连接。
Step6: 初始化主程序
前面那些步骤已经将须要配置和加载的东西都完成了,接下来就须要初始化咱们的主程序,相似于建立对象的alloc
已经执行完了,接下来就是init
了。
// 初始化主程序
initializeMainExecutable();
复制代码
点进去查看方法实现以下:
void initializeMainExecutable() {
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
// 先初始化所有的插入库
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
// 从1开始,由于第0个是主程序的image
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]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
// 若是配置了这两个环境变量,则会打印想对应的状态信息
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
复制代码
其实看到这个initializeMainExecutable()
方法,就会发现这个方法就是最开始那个调用堆栈里面第四步,也就是说调用堆栈的后续步骤须要从这个函数继续追踪了。那么接下来就开始追踪吧。
找到堆栈下一步runInitializers
,恰好代码中就有这个函数的调用
processInitializers
recursiveInitialization
image
,在
doInitialization
方法前会发送
state
为
dyld_image_state_dependents_initialized
的通知,这个通知会调用
libobjc
的
load_images
,最后去依次调用各个OC类的load方法以及分类的load方法。(这里就是load方法的调用时机)。
接下来咱们跳进notifySingle
里面看看,哎?又跳到声明了,老方法
load_images
,可是咱们找遍了也没找到哪里有调用
load_images
。
load_images
,运行
load_images
是
libobjc
里面的,所以咱们能够下载
runtime源码
load_images
是
_objc_init
函数里面的,所以咱们在runtime源码里面全局搜索
_objc_init
。
load_images
是
_dyld_objc_notify_register
函数的第二个参数,很明显
_dyld_objc_notify_register
是dyld库里面的方法。在dyld源码里面搜索
_dyld_objc_notify_register
。
registerObjCNotifiers
方法实现,能够看到
objc
传过来的
load_images
在dyld中赋值给力
sNotifyObjCInit
,而咱们上面的那个回调函数就是
sNotifyObjCInit
。说明那一步回调实质上调用的就是
objc
里面的
load_images
函数。
load_images
函数到底长啥样,在
objc
源码中搜索
load_images
找到方法实现。
call_load_methods()
函数,函数实现以下
void call_load_methods(void) {
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
// 循环调用
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
复制代码
这里的call_class_loads();
方法内部就是调用每一个类的load
方法。
找到了load方法的调用时机,还没完,再回到刚开始的_main
函数中,初始化主程序这一步算是分析完了,可是下面还有代码,继续分析。
// find entry point for main executable
// 找到主可执行程序的入口点
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
if (result != 0) {
// main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
if ((gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9))
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
*startGlue = 0;
}
复制代码
getEntryFromLC_MAIN()
方法找到了主可执行程序的入口了,也就是那个程序的入口main()
函数,非刚刚分析的_main()
函数。
又通过一系列的配置,最后_main()
函数的返回值处返回了咱们找到的程序入口main()
函数。
又到了总结的时刻,总结了如下六点......
哈哈,写这篇文章快要累死我了,可是收获也是不少的,学到了App加载过程当中经历了哪些步骤,后续文章咱们可能就会利用这些东西的加载顺序的不一样来破解或者防御App。
真正的总结
MachO文件
dyld,main函数以前都作了什么?
_dyld_start
开始执行,进入_main
函数。doModInitFunctions
函数,会调用带有__attribute__((constructor))
的C函数。_main()
调用结束返回程序入口main()
函数,开始进入主程序的main()
函数。