1、App 启动指的是什么?
从用户点击 App 开始到用户看到第一个界面,这称为一次 App 启动。
一般情况下,App 的启动分为冷启动和热启动。
-
冷启动是指, App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给它启动的情况。这是一次完整的启动过程。
-
热启动是指 ,App 在冷启动后用户将 App 退后台,在 App 的进程还在系统里的情况下,用户重新启动进入 App 的过程,这个过程做的事情非常少。
今天这篇文章,我们就探讨 App 的冷启动。
2、App 的拆包
在没有接触到 dyld 的时候,认为 App 启动就是把 Xcode 最后打包的 .ipa 包交给系统执行,系统自己调用代码启动 App。之后为了研究 App 是如何启动的,把 .ipa 拆包了(随意找了微信的包),如下图:
拆包之后,发现就是一些资源(包括:图片,json文件,html本地文件等)和一个显示 exec 的黑色文件,这个黑色文件就是代码打包后的东西,也就是 Mach-O 文件。
3、Mach-O
什么是Mach - O
维基百科的解释:Mach-O 为 Mach Object 文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。Mach-O 文件里面的内容,主要就是代码和数据:代码是函数的定义;数据是全局变量的定义,包括全局变量的初始值。
其实 .ipa 包里显示 exec 的黑色文件就是 Xcode 打包代码后形成的可执行的二进制文件。
Mach - O 是怎么被生成的
因为本文主要探索 App 的启动流程,Mach - O 的生成过程简单提一下。
开发者使用 Xcode 进行打包代码后形成 Mach - O 文件,其实打包的过程是使用 LLVM 编译器编译的。编译器会对每个工程文件进行编译,生成多个 Mach-O(可执行文件),之后由链接器会将项目中的多个 Mach-O 文件合并成一个。
戴铭大佬的这篇文章 中解释了编译的几个主要过程:
-
首先,你写好代码后,LLVM 会预处理你的代码,比如把宏嵌入到对应的位置。
-
预处理完后,LLVM 会对代码进行词法分析和语法分析,生成 AST 。AST 是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用 AST 能够更快速地进行静态检查,同时还能更快地生成 IR(中间表示)。
-
最后 AST 会生成 IR,IR 是一种更接近机器码的语言,区别在于和平台无关,通过 IR 可以生成多份适合不同平台的机器码。对于 iOS 系统,IR 生成的可执行文件就是 Mach-O。
下图展示了编译的主要过程。
Mach - O 文件是什么样的
找到一个 CoreFoundation 的 Mach - O ,使用 MachOView 打开如下:
能看到使用 Xcode 写的代码经过 LLVM 编译后变成了一个个数据段,分类别存储代码中的 类、方法、字符串 等,上图展示的是被引用类的指针地址和名称。
PS:扩展一下,有指针地址说明,类是在编译时生成的。类的实际地址为:ASLR + Offset。
4、App 启动流程
了解了 Mach - O 文件的概念,现在我们对 App 启动流程进行探索。
从调用栈中查看 dyld 的加载流程
既然是 App 启动,那么程序到 main.m 文件的 main 函数,肯定是启动完成了。
尝试1:把断点打在 main.m 的 main 函数
可惜什么也看不到,只是知道了在 libdyld.dylib 调用了 start 函数。
尝试2:把断点打在 objc-runtime-new.m 的随意一个函数里
打开上帝视角,其实应该断点到 +(void)load {} 方法里面去,分析完 dyld 就明白了
看见这个调用栈信息后,发现似乎是找对了,因为方法前缀都有 dyld 的前缀(这个是 C++ 的命名空间),有了眉目,那就从 __dyld_start 开始吧。
__dyld_start(dyld的入口)
点击这个调用栈,发现是一段汇编:
汇编中 call xxx 就会调用一个方法,能看到汇编后面的方法说明,其实是调用了 dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) 方法。
dyldbootstrap:: 为命名空间,commend + shift + o 搜索 dyldbootstrap ,看到上方注释 Code to bootstrap dyld into a runnable state.(引导dyld进入可运行状态的代码),随后在 dyldbootstrap 命名空间下方找到了 start 方法。
到此 App 启动第一步开始。
dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*)
start 源码如下:
//
// This is code to bootstrap dyld. This work in normally done for a program by dyld and crt.
// 这是引导dyld的代码。这项工作通常由dyld和crt为程序完成。
// In dyld we have to do this manually.
// 在dyld中,我们必须手动执行此操作。
//
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>
// 释放kdebug跟踪点,指示dyld引导已经启动
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
// 如果内核必须滑动dyld,我们需要修复加载敏感的位置,我们必须在使用任何全局变量之前完成此操作
rebaseDyld(dyldsMachHeader);
// kernel sets up env pointer to be just past end of agv array
// 内核将env指针设置为agv数组的刚刚结束
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
// 内核将苹果指针设置为刚好超过envp数组的末尾
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
_subsystem_init(apple);
// now that we are done bootstrapping dyld, call dyld's main
// 现在我们已经完成了对dyld的引导,调用dyld的main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
上方代码是对 dyld 的引导操作, 完成引导后调用了 dyld::_main 函数返回结果,并且也能从调用栈看到 dyld::_main 方法的调用。
start 函数中调用了 rebaseDyld(dyldsMachHeader); 这个函数就是在引导 dyld 时候确定 ASLR 。
ASLR :内核产生的随意性偏移地址,用来做地址定向攻击的防护。
dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*)
从 dyldbootstrap::start 函数的结尾能看到,调用了 _main 函数后就返回了,所以 dyld 的加载必定是在 dyld::_main 中。
_main 函数上方的注释,苹果也对 start 方法进行了注释, Returns address of main() in target program which __dyld_start jumps to.,意思是返回 __dyld_start 跳转到的目标程序中的 main() 地址。
有了这个前提,再看 _main 函数。(代码太多了,就不粘贴了,只放出主要代码)
小技巧:源码中注释的地方基本上都是重要信息。
//
// Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
// 返回 __dyld_start 跳转到的目标程序中的 main() 地址
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
// Grab the cdHash of the main executable from the environment
// 从环境中获取主可执行文件的cdHash
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
unsigned bufferLenUsed;
if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
}
// 获取主程序的架构
getHostInfo(mainExecutableMH, mainExecutableSlide);
// 保存主程序 Mach-O 的头
sMainExecutableMachHeader = mainExecutableMH;
// 保存主程序的 ASLR
sMainExecutableSlide = mainExecutableSlide;
// 设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
// 根据环境变量判断当前进程是否受限
configureProcessRestrictions(mainExecutableMH, envp);
// 检测进程环境变量
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
// 配置环境变量后就可以打印
if ( sEnv.DYLD_PRINT_OPTS )
//配置 DYLD_PRINT_OPTS 后可以打印 Mach-O 文件的地址
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
// 开始加载共享缓存
// 检查共享缓存库是否禁用,iOS不能禁用
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
// 加载共享缓存(UIKIT,Fundation)
mapSharedCache(mainExecutableSlide);
// instantiate ImageLoader for main executable
// 为主要可执行文件实例化ImageLoader
// 实例化主程序,从 Mach - O 头部 读取CPU架构等,判断兼容性
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);、
// load any inserted libraries
// 加载插入的动态库
// 根据 DYLD_INSERT_LIBRARIES 的值是否允许加载插入动态库
// (越狱的环境都是用这个,下载插件影响别人的程序)
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib); // 循环加载
}
// 开始链接主程序
// link main executable
gLinkContext.linkingMainExecutable = true;
// 链接动态库和第三方库,进行符号绑定
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
// link any inserted libraries
// 链接插入库
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
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();
}
}
// do weak binding only after all inserted images linked
// weak符号绑定,weakbind是比其他后绑定的意思,其他的绑定是在上方 link 完成时就绑定好了
sMainExecutable->weakBind(gLinkContext);
// 链接操作结束
gLinkContext.linkingMainExecutable = false;
// run all initializers
// 开始运行主程序了(load_images ,class_load)类的load方法就是这里调用的
initializeMainExecutable();
// notify any montoring proccesses that this process is about to enter main()
// 通知任何监视进程该进程即将进入main()
notifyMonitoringDyldMain();
// find entry point for main executable
// 查找主程序的入口点
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
// 调用 main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
//...
return result;
}
看到了上方主要代码流程后,这里需要提几个重要参数 :
-
macho_header* mainExecutableMH:mach-O 文件的 header ,每一个 mach-O 文件都会有,表示了 CPU 架构等一些运行环境的信息; -
uintptr_t mainExecutableSlide:滑块地址,其实就是ASLR的起始地址; -
sEnv.DYLD_PRINT_OPTS:这个参数配置后,控制台就能打印Mach-O的文件地址; -
sEnv.DYLD_INSERT_LIBRARIES:这个参数正向开发基本上用不到,但是如果要做逆向防护的时候,这个就是重要的参数,根据DYLD_INSERT_LIBRARIES的值, dyld 判断是否允许加载插入动态库,玩过越狱的朋友就知道,下载了一个插件所有的App都能使用,其实不是改变了App的东西,而是系统加载了插入库。
对于主线流程有了概念,接下来分析主线流程中一些重要代码。
checkSharedRegionDisable(const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide)
checkSharedRegionDisable :判断共享缓存是否能禁用。
这个方法不多说,本文主要探索 iOS 系统下 App 的启动流程,所以这里就只看一句话。
static void checkSharedRegionDisable(const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide)
{
#if TARGET_OS_OSX
// ...
#endif
// iOS cannot run without shared region
}
去掉 OSX方法,就只剩一个注释,这里就说明了,为什么 iOS 不能禁止共享缓存库了。
instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
// The kernel maps in main executable before dyld gets control. We need to
// make an ImageLoader* for the already mapped in main executable.
// 在dyld获得控制之前,内核在main可执行文件中映射。我们需要为主可执行文件中已经映射的对象创建一个ImageLoader*。
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);
//添加到 AllImages 环境变量中去
addImage(image);
return (ImageLoaderMachO*)image;
// }
// throw "main executable not a known format";
}
isCompatibleMachO以前判断了 CPU 架构是否兼容,现在去掉了。
创建是由 ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext); 这行代码去创建的,这里需要往下看一下,有一些有趣的参数需要了解一下。
instantiateMainExecutable
instantiateMainExecutable 源码:
// create image for main executable
// 为主要可执行文件创建映像
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
// ...
//通过这个方法去实例化
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// ...
}
sniffLoadCommands
sniffLoadCommands 源码:
这里代码过多,只说一些重要参数:
// determine if this mach-o file has classic or compressed LINKEDIT and number of segments it has
void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
const linkedit_data_command** codeSigCmd,
const encryption_info_command** encryptCmd)
{
//dlyd_info_only
*compressed = false;
//有最大限度
//最大 255
*segCount = 0;
//LC_LOAD_DYLD_LIB 最大4095个
*libCount = 0;
//代码签名
*codeSigCmd = NULL;
//加密信息,应用上传加壳
*encryptCmd = NULL;
// ...
// fSegmentsArrayCount is only 8-bits
if ( *segCount > 255 )
dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);
// fSegmentsArrayCount is only 8-bits
if ( *libCount > 4095 )
dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);
if ( needsAddedLibSystemDepency(*libCount, mh) )
*libCount = 1;
// dylibs that use LC_DYLD_CHAINED_FIXUPS have that load command removed when put in the dyld cache
if ( !*compressed && (mh->flags & MH_DYLIB_IN_CACHE) )
*compressed = true;
}
*compressed:从这里的代码能看到*compressed是标记LC_DYLD_INFO/LC_DYLD_INFO_ONLY中有没有取到cmdsize,确定mach - O文件的一些大小。
switch (cmd->cmd) {
case LC_DYLD_INFO:
case LC_DYLD_INFO_ONLY:
if ( cmd->cmdsize != sizeof(dyld_info_command) )
throw "malformed mach-o image: LC_DYLD_INFO size wrong";
dyldInfoCmd = (struct dyld_info_command*)cmd;
*compressed = true;
break;
// ...
}
MachOView 的图如下:
*segCount:LC_SEGMENT(或LC_SEGMENT_64)命令是最主要的加载命令,这条命令知道内核如何设置新运行的进程的内存空间,这些命令条数最大 255 个。
-
*libCount:动态库+三方库的总和,这些库最大允许加载 4095 个。因为是一个空工程,只有系统动态库,没有第三方库。 -
codeSigCmd:代码签名; -
encryptCmd:应用的加密信息,用于应用加壳。
到这里主程序 初始化 完成。
ImageLoader::link 链接器
主程序初始化完成之后,现在就开始 链接库 和 符号绑定 了。
戴铭大佬对于动态库链接的说明如下:
Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接,所以 Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。
使用 dyld 加载动态库,有两种方式:有程序启动加载时绑定和符号第一次被用到时绑定。为了减少启动时间,大部分动态库使用的都是符号第一次被用到时再绑定的方式。
加载过程开始会修正地址偏移,iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类,最后执行 load 方法和 Clang Attribute 的 constructor 修饰函数。
每个函数、全局变量和类都是通过符号的形式定义和使用的,当把目标文件链接成一个 Mach-O 文件时,链接器在目标文件和动态库之间对符号做解析处理。
源码如下:
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
// ...
uint64_t t0 = mach_absolute_time();
// ...
//根据LC_LOAD_DYLIB 告诉主程序动态库的里面的方法的实际地址
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
// 进行符号绑定,
this->recursiveRebaseWithAccounting(context);
// ...
}
-
recursiveLoadLibraries:(undefined) external _NSLog (from Foundation)上方解释说明到,动态库编译的时候并没有符号地址,只有链接库的目录,所以,dyld 在link的时候需要把被LLVM标识为undefined的这种未实现、非私有的符号通过动态库解析成真正的指针地址。 -
recursiveRebaseWithAccounting: 把解析后的指针地址和undefined逐个对应。
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 ) {
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]);
// ...
}
ImageLoader::runInitializers
initializeMainExecutable() 的核心函数调用了 processInitializers(),基本上没有处理其他东西,接着往下看。
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
// ...
processInitializers(context, thisThread, timingInfo, up);
// ..
}
ImageLoader::processInitializers
对 images 列表中的所有图像调用递归 init ,构建一个新的未初始化的向上依赖项列表。
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
// 对 images 列表中的所有图像调用递归 init ,构建一个新的未初始化的向上依赖项列表。
for (uintptr_t i=0; i < images.count; ++i) {
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
}
ImageLoader::recursiveInitialization
这里就开始实例化 images 了, 通过 notifySingle 回调。
这里主要处理以下 2 个事情:
notifySingle: 调用 runtime 中注册的通知doInitialization:实例化这些 image
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);
if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
// record termination order
if ( this->needsTermination() )
context.terminationRecorder(this);
// let objc know we are about to initialize this image
// 让 objc 知道我们将要初始化这个图
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
// 回调
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// initialize this image
// 实例化这些 image
bool hasInitializers = this->doInitialization(context);
//...
}
catch (const char* msg) {
// this image is not initialized
// ...
}
}
recursiveSpinUnLock();
}
notifySingle
这个方法就是 C++ 方法回调,通过调用栈知道是由 _objc_init() 函数下 _dyld_objc_notify_register 注册通知后,(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());回调的。
看到这里后,就明白了 dyld 和 runtime 是怎么完美的链接起来的。
sNotifyObjCMapped:调用 runtime 方法 map_images,处理由 dyld 映射的 image,计算class数量,根据总数调整各种表的大小;sNotifyObjCInit:调用 runtime 方法load_images,对类的初始化。
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
// ...
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
// ...
// 调用 runtime 方法
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
// ...
}
// ...
}
doInitialization
doInitialization 做一些初始化。
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
// 调用 全局 C++ 方法
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
两个主要方法如下:
-
doImageInit:逐个初始化image; -
doModInitFunctions: 调用全局 C++ 方法。这个方法目前正向开发不怎么用,但是到逆向上,一些代码的注入,都会在这里做。因为这个方法是在调用main()之前,95% 配置结束之后执行的。
doModInitFunctions
调用了 doModInitFunctions 后,发现到 _objc_init 时,调用栈中还有 libSystem_initializer、libdispatch_init、_os_object_init 没有看到在哪调用的,
继续看 doModInitFunctions 函数,发现在当前函数的最后调用了 libSystemInitialized。
bool haveLibSystemHelpersBefore = (dyld::gLibSystemHelpers != NULL);
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
bool haveLibSystemHelpersAfter = (dyld::gLibSystemHelpers != NULL);
if ( !haveLibSystemHelpersBefore && haveLibSystemHelpersAfter ) {
// now safe to use malloc() and other calls in libSystem.dylib
dyld::gProcessInfo->libSystemInitialized = true;
}
libSystem_initializer:在libSystem库中;libdispatch_init:在libdispatch库中;_os_object_init:在libdispatch库中。
libSystem_initializer
在 libSystem 库中找到了 libSystem_initializer ,发现 libdispatch_init() 的调用。
static void
libSystem_initializer(int argc,
const char* argv[],
const char* envp[],
const char* apple[],
const struct ProgramVars* vars)
{
// ...
_dyld_initializer();
_libSystem_ktrace_init_func(DYLD);
libdispatch_init();
// ...
errno = 0;
}
libdispatch_init()
看到 libdispatch_init() 调用了 _os_object_init(),继续追!!
void
libdispatch_init(void)
{
// ...
_os_object_init();
// ...
}
_os_object_init()
这里,终于找到了 _objc_init 的调用方法,完美的和调用栈匹配。
void
_os_object_init(void)
{
// ...
_objc_init();
// ...
}
_objc_init()
这个方法会在下一篇 类的加载 说,这里就不细说了,在这里就查找一下 + (void)load {} 方法的调用。
load_images 的源码:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// ...
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
从上述 runtime 源码中,看到调用了 call_load_methods(),源码如下:
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
看完这段代码, (*load_method)(cls, @selector(load)) 就是对各个类 load 方法的调用。
如果配置了
PrintLoading环境变量,那么就能打印所有使用load方法的类了。优化启动时间就会很方便。
(uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD()
接下来就调用 main() 函数了。
5、总结
上述就是 App 启动的时候 dyld 工作的流程,也能看到 dyld 和 Object-C runtime 链接的方式。
下图是 dyld 的加载流程总结图:
到这里,App启动流程之 dyld 探析就结束了,感谢阅读,如果对你有帮助,希望给个赞,如果有错误的地方,希望大佬指出,共同学习,一起进步,谢谢。