本篇文章将探究app
应用程序是如何加载的?
我们平时都认为main
是程序的入口,但是实际上是这样子的吗?不是的话,应用在冷启动后main
之前做了什么呢,我们去探索下流程。
一、启动流程初步探索
新建iOS
工程,在ViewController.m
中添加一个load
方法并打断点,程序main.m
中添加一个C++
函数kcFunc
。
ViewControler
代码:
@interface ViewController ()
@end
@implementation ViewController
+ (void)load{
NSLog(@"%s", __func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"viewDidLoad --- ");
}
@end
main.m
代码:
// 在`main`函数之前,执行一些我们的设置,比如状态打印
__attribute__((constructor)) void kcFunc(){
printf("C... %s \n",__func__);
}
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"main ...");
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
- llvm打印:
2023-02-02 14:21:51.278198+0800 DyldTest[17170:4322854] +[ViewController load]
C... kcFunc
2023-02-02 14:21:51.280613+0800 DyldTest[17170:4322854] main ...
2023-02-02 14:21:52.835821+0800 DyldTest[17170:4322854] viewDidLoad ---
- 结论:
-
__attribute__((constructor))
是在main
函数之前执行一个函数,便于我们做一些准备工作。可以查阅了GNU的文档印证了我的想法。 -
运行程序,根据终端打印结果,可发现程序首先运行了
load
方法,然后调用了C++
函数,最后才进入到main
方法中。
在分析启动流程之前,先学习一些概念。
二、应用程序编译过程
静态库
在链接阶段,会将汇编生成的目标程序与引用的库一起链接打包到可执行文件当中。此时的静态库就不会再改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的。例如:.a
、.lib
- 优点:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖,直接就可以运行
- 缺点:由于静态库可能会有两份,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大
动态库
程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入。例如:.so
、.framwork
、.dll
- 优点:减少打包之后
app
的大小,共享内存,节约资源,更新动态库,达到更新程序 - 缺点:动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行。
编译过程
程序的编译过程:预编译 -> 编译 -> 汇编 -> 链接 -> 可执行文件
- 源文件:载入
.h、.m、.cpp
等文件 - 预处理:替换宏,删除注释,展开头文件,产生
.i
文件 - 编译:经过词法分析、语法分析、语义分析后,将
.i
文件转换为汇编语言,产生.s
文件 - 汇编:将汇编文件转换为机器码文件,产生
.o
文件 - 链接:对
.o
文件中引用其他库的地方进行引用,生成Mach-O
格式的可执行文件 经过上述步骤,我们开发的代码会生成一个Mach-O
格式的可执行文件。详情可阅读这里。
动态链接器dyld
dyld(the dynamic link editor)
是苹果的动态链接器,是苹果操作系统的重要组成部分,在app
被编译打包成可执行文件格式的Mach-O
文件后,交由dyld
负责连接,并加载程序。
dyld
的作用:加载各个库,也就是image
镜像文件,由dyld
从内存中读到表中,加载主程序,link
链接各个动静态库,进行主程序初始化。
Mach-O
Mach-O
: Mach Object
文件格式的缩写,它是一种用于可执行文件,目标文件.o
,动态库,内核转储的文件格式。作为a.out
格式的替代,Mach-O
提供了更强的扩展性,并提升了符号表中信息的访问速度。
Mach-O
格式的文件:
Executable
(产物为ipa包)Dynamic Library
(产物为动态库)Bundle
(产物为bundle文件)Static Library
(产物为静态库)Relocatable Object File
(重定向文件)
- 打开
ipa
内容文件:
- 通过
MachOView
可查看可执行文件信息:
Mach-O
由四部分组成:Mach-O Header
、Load Commands
、Section
、Other Data
。
典型的Mach-O文件
包含三个区域:
-
Header
:保存Mach-O
的一些基本信息,包括平台、文件类型、指令数、指令总大小,dyld
标记Flags
等等。 -
Load Commands
:紧跟Header
,加载Mach-O文件
时会使用这部分数据确定内存分布,对系统内核加载器和动态连接器起指导作用。 -
Data
:每个segment
的具体数据保存在这里,包含具体的代码、数据等等。详情可阅读这里,了解这个部分是iOS
逆向工程的基础。
App启动的过程:
当我们点击iPhone上的应用图标后,整个iOS
系统启动App经历了:
- 系统调用
exec()
分配内存、开辟进程 app
对应的可执行文件
加载到内存dyld
动态链接器加载到内存:load dyld
dyld
动态链接器进行动态链接:rebase
->bing
->Objc
->initalizers
- 调起
main()
函数
启动时间的划分可以把main()
函数作为关键点分割成两块
三、dyld
入口探索
在Viewcontroller
的load
方法添加断点,运行程序。在控制台输入指令bt
,查看运行的函数调用堆栈信息。
- 图:
- llvm打印:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001047cde38 DyldTest`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
frame #1: 0x00000001bed30a80 libobjc.A.dylib`load_images + 824
frame #2: 0x00000001e40d83fc dyld`dyld4::RuntimeState::notifyObjCInit(dyld4::Loader const*) + 164
frame #3: 0x00000001e40dbcc0 dyld`dyld4::Loader::runInitializersBottomUp(dyld4::RuntimeState&, dyld3::Array<dyld4::Loader const*>&) const + 204
frame #4: 0x00000001e40e133c dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const + 328
frame #5: 0x00000001e4115244 dyld`dyld4::APIs::runAllInitializersForMain() + 360
frame #6: 0x00000001e40ea66c dyld`dyld4::prepare(dyld4::APIs&, dyld3::MachOAnalyzer const*) + 3388
frame #7: 0x00000001e40e88d4 dyld`start + 2388
(lldb)
- 结论:
-
从左侧的堆栈信息可以找到了程序的入口是
start
,这个部分是iOS16
后的启动情况。现在最新的iOS
系统基本上都是dyld4
流程,与网上大部分资料还是iOS15
以前版本的dyld3
流程是有所不同,但是这里不进行详细展开。 -
调用流程:
start
->dyld4::prepare
->dyld4::APIs::runAllInitializersForMain
->dyld4::Loader::runInitializersBottomUpPlusUpwardLinks
->dyld4::Loader::runInitializersBottomUp
->dyld4::RuntimeState::notifyObjCInit
->load_images
->[ViewController load]
。
四、探索dyld源码
截止目前(2023年2月)dyld
已经更新至dyld4(1042.1)了,iOS15
以上系统都是使用的dyld4
。
下载dyld
当前最新版本是dyld-1042.1 ,这部分源码是不能编译的,可以结合objc4
源码分析。
- 打开
dyld
源码,虽然控制台是从start
开始打印的,但是dyld4
跟dyld3
一样都是从__dyld_start
。先在工程里全局搜索__dyld_start
,开始探索程序加载流程。最终在dyldStartup.s
文件中搜索到了伪汇编流程。
-
见下图:
-
结论:
①. 此汇编代码只是对齐堆栈并跳转到
C++
代码dyld:: start
- 针对不同的环境提供了不同的实现方式,比如,我们现在看到的就是针对
arm64
真机环境的实现。看注释发现,不论是何种环境,最终都会走到start
流程中。这也和我们前面bt
看到的函数调用堆栈继续!下面全局搜索start
。
- 见下图:
start
查看dyld-1042.1
工程里找到的start
函数。获取Mach-O
解析器,解析Mach-O
文件,还有获取一个随机数(ASLR
)。
namespace dyld4 {
...
...
// dyld的入口点。内核加载dyld并跳转到__dyld_start,它设置一些寄存器并调用这个函数。
// 注意:这个函数永远不会返回,它调用exit()。
// 因此,堆栈保护程序是无用的,因为永远不会执行epilog。
// 标记函数no-return禁用堆栈保护。
// 堆栈保护器也会导致armv7k代码生成的问题,因为它通过prolog中的GOT槽访问随机值,但dyld还没有rebased。
void start(const KernelArgs* kernArgs, void* prevDyldMH) __attribute__ ((noreturn)) __asm("start");
void start(const KernelArgs* kernArgs, void* prevDyldMH)
{
// 发出kdebug跟踪点来指示dyld引导程序已经启动 <rdar://46878536>
// 注意:这是在dyld rebased之前调用的,所以kdebug_trace_dyld_marker()不能使用任何全局变量
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// 走所有的fixups chains 和 rebase dyld
// 注意:withChainStarts()和fixupAllChainedFixups()不能使用任何静态数据指针,因为它们还没有rebased
const MachOAnalyzer* dyldMA = getDyldMH(); //获取Mach-O解析器,用于解析Mach-O格式文件内容
uintptr_t slide = dyldMA->getSlide();// 会生成一个随机数(ASLR)
if ( !dyldMA->inDyldCache() ) {
assert(dyldMA->hasChainedFixups());
__block Diagnostics diag;
dyldMA->withChainStarts(diag, 0, ^(const dyld_chained_starts_in_image* starts) {
dyldMA->fixupAllChainedFixups(diag, starts, slide, dyld3::Array<const void*>(), nullptr);
});
diag.assertNoError();
// make __DATA_CONST read-only (kernel maps it r/w)
dyldMA->forEachSegment(^(const MachOAnalyzer::SegmentInfo& segInfo, bool& stop) {
if ( segInfo.readOnlyData ) {
const uint8_t* start = (uint8_t*)(segInfo.vmAddr + slide);
size_t size = (size_t)segInfo.vmSize;
sSyscallDelegate.mprotect((void*)start, size, PROT_READ);
}
});
}
// 现在,我们可以调用使用DATA的函数
mach_init();// mach消息初始化
// 为stack canary设置随机值
__guard_setup(kernArgs->findApple()); //栈溢出保护
// 设置以便open_with_subsystem()工作
_subsystem_init(kernArgs->findApple());
// 在__DATA_CONST中将ProcessConfig对象变为只读之前,使用placement new来构造它
handleDyldInCache(dyldMA, kernArgs, (MachOFile*)prevDyldMH);
bool useHWTPro = false;
// 在自己的分配池中创建一个分配器Allocator
Allocator& allocator = Allocator::persistentAllocator(useHWTPro);
// 使用placement new在Allocator池中构造ProcessConfig对象
// 构建进程配置
ProcessConfig& config = *new (allocator.aligned_alloc(alignof(ProcessConfig), sizeof(ProcessConfig))) ProcessConfig(kernArgs, sSyscallDelegate, allocator);//构建进程配置
#if !SUPPPORT_PRE_LC_MAIN
// 堆栈分配RuntimeLocks。它们不能在通常为只读的Allocator池中
RuntimeLocks sLocks;
#endif
// 在分配器中创建API(也称为RuntimeState)对象
APIs& state = *new (allocator.aligned_alloc(alignof(APIs), sizeof(APIs))) APIs(config, allocator, sLocks);
#if !TARGET_OS_SIMULATOR
// FIXME:我们应该更早地移动它,但目前我们需要在设置压缩信息之前将运行时状态初始化,
// 直到我们这样做之前,压缩信息可能会错过某些早期的dyld崩溃
auto processSnapshot = state.getCurrentProcessSnapshot();
processSnapshot->setPlatform((uint64_t)state.config.process.platform);
processSnapshot->setDyldState(dyld_process_state_dyld_initialized);
FileRecord cacheFileRecord = state.fileManager.fileRecordForVolumeDevIDAndObjID(gProcessInfo->sharedCacheFSID, gProcessInfo->sharedCacheFSObjID);
if (cacheFileRecord.exists()) {
auto sharedCache = SharedCache(state.ephemeralAllocator, std::move(cacheFileRecord), processSnapshot->identityMapper(), (uint64_t)config.dyldCache.addr, gProcessInfo->processDetachedFromSharedRegion);
processSnapshot->addSharedCache(std::move(sharedCache));
}
// 将dyld添加到压缩信息
if ( dyldMA->inDyldCache() && processSnapshot->sharedCache() ) { ··· } else { ··· }
// 将主可执行文件添加到压缩信息
FileRecord mainExecutableFile;
if (state.config.process.mainExecutableFSID && state.config.process.mainExecutableObjID) { ··· } else { ··· }
auto mainExecutableImage = Image(state.ephemeralAllocator, std::move(mainExecutableFile), processSnapshot->identityMapper(), (const mach_header*)state.config.process.mainExecutable);
processSnapshot->addImage(std::move(mainExecutableImage));
processSnapshot->setInitialImageCount(state.initialImageCount());
state.commitProcessSnapshot();
#endif
// -------------重点入口-------------------
// 加载所有的库与程序
MainFunc appMain = prepare(state, dyldMA);
// 现在将所有dyld分配的数据结构设置为只读
state.decWritable();
// 调用main(),如果它返回,调用exit()并返回结果
// 注意:这是经过组织的,以便在程序的主线程中回溯时只在“main”下面显示“start”。
int result = appMain(state.config.process.argc, state.config.process.argv, state.config.process.envp, state.config.process.apple);
// 如果我们到达这里,main()返回(与程序调用exit()相反)
#if TARGET_OS_OSX
// <rdar://74518676>libSystemHelpers没有为模拟器设置,因此直接调用_exit()
if ( MachOFile::isSimulatorPlatform(state.config.process.platform) )
_exit(result);
#endif
state.libSystemHelpers->exit(result);
}
...
...
}// namespace
-
结论:
①.
dyld4
跟dyld3
不同就是,main
没有了返回值,是通过exit
结束!无法通过epilog
退出代码,它释放堆栈空间并返回到调用者。②.
start
中读取mach-o
文件中的符号地址都是虚拟地址,在程序启动的时候,系统会生成一个随机数(ASLR
),使用虚拟地址加上ASLR
才是物理地址,也就是程序真正调用的地址。我们把从虚拟地址换算成物理地址的过程称之为rebase
。
prepare
//
// 加载任何相关的dylib并将它们绑定在一起。
// 返回main()在target中的地址。
//
__attribute__ ((noinline)) static MainFunc prepare(APIs& state, const MachOAnalyzer* dyldMH)
{
// `gProcessInfo` 是存储dyld所有镜像信息的结构体:,保存着mach_header, dyld_uuid_info, dyldVersion等等信息。
// 配置dyld信息中停止标识符
gProcessInfo->terminationFlags = 0; // 默认情况下,在崩溃日志中显示回溯
// 配置dyld信息中平台信息
gProcessInfo->platform = (uint32_t)state.config.process.platform;
// 配置dyld信息中路径信息
gProcessInfo->dyldPath = state.config.process.dyldPath;
uint64_t launchTraceID = 0;
if ( dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE) ) {
launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)state.config.process.mainExecutable, 0, 0);
}
#if TARGET_OS_OSX //模拟环境,涉及Simulator和Sim(略过)
const bool isSimulatorProgram = MachOFile::isSimulatorPlatform(state.config.process.platform);
if ( const char* simPrefixPath = state.config.pathOverrides.simRootPath() ) {
#if __arm64e__
if ( strcmp(state.config.process.mainExecutable->archName(), "arm64e") == 0 )
halt("arm64e not supported for simulator programs");
#endif
if ( isSimulatorProgram ) {
char simDyldPath[PATH_MAX];
strlcpy(simDyldPath, simPrefixPath, PATH_MAX);
strlcat(simDyldPath, "/usr/lib/dyld_sim", PATH_MAX);
return prepareSim(state, simDyldPath);
}
halt("DYLD_ROOT_PATH only allowed with simulator programs");
}
else if ( isSimulatorProgram ) {
halt("DYLD_ROOT_PATH not set for simulator program");
}
#endif
#if 0
// 检查主可执行文件是否有效
Diagnostics diag;
bool validMainExec = state.config.process.mainExecutable->isValidMainExecutable(diag, state.config.process.mainExecutablePath, -1, *(state.config.process.archs), state.config.process.platform);
if ( !validMainExec && state.config.process.mainExecutable->enforceFormat(dyld3::MachOAnalyzer::Malformed::sdkOnOrAfter2021)) {
state.log("%s in %s", diag.errorMessage(), state.config.process.mainExecutablePath);
halt(diag.errorMessage());
}
#endif
// 如果询问,log打印环境env变量
if ( state.config.log.env ) {
for (const char* const* p=state.config.process.envp; *p != nullptr; ++p) {
state.log("%s\n", *p);
}
}
// 检查预构建pre-built Loader
state.initializeClosureMode();
const PrebuiltLoaderSet* mainSet = state.processPrebuiltLoaderSet();
Loader* mainLoader = nullptr;
if ( mainSet != nullptr ) {
mainLoader = (Loader*)mainSet->atIndex(0);
state.loaded.reserve(state.initialImageCount()); // 帮助避免Vector的重新定位
}
if ( mainLoader == nullptr ) {
// 如果没有预构建的Loader,制造一个just-in-time
// just-in-time是dyld4的新特性:`pre-build + just-in-time`预构建+实时解析的双解析模式
state.loaded.reserve(512); // 猜测Vector大小的起点
Diagnostics buildDiag;
mainLoader = JustInTimeLoader::makeLaunchLoader(buildDiag, state, state.config.process.mainExecutable,
state.config.process.mainExecutablePath, nullptr);
if ( buildDiag.hasError() ) {
state.log("%s in %s\n", buildDiag.errorMessage(), state.config.process.mainExecutablePath);
halt(buildDiag.errorMessage());
}
}
state.setMainLoader(mainLoader);
// 首先将主可执行文件添加到mainLoader预构建的镜像列表中
state.notifyDebuggerLoad(mainLoader);
const bool needToWritePrebuiltLoaderSet = !mainLoader->isPrebuilt && (state.saveAppClosureFile() || state.failIfCouldBuildAppClosureFile());
// <rdar://problem/10583252> Add dyld to uuidArray to enable symbolication of stackshots (unless dyld is in the cache)
// 装载uuid相关信息
if ( !dyldMH->inDyldCache() ) {
dyld_uuid_info dyldInfo;
dyldInfo.imageLoadAddress = dyldMH;
dyldMH->getUuid(dyldInfo.imageUUID);
addNonSharedCacheImageUUID(state.persistentAllocator, dyldInfo);
}
// 加载所需要插入的动态库(dylibs)
STACK_ALLOC_OVERFLOW_SAFE_ARRAY(Loader*, topLevelLoaders, 16);
topLevelLoaders.push_back(mainLoader);
Loader::LoadChain loadChainMain { nullptr, mainLoader };
Loader::LoadOptions options;
options.staticLinkage = true;
options.launching = true;
options.insertedDylib = true;
options.canBeDylib = true;
options.rpathStack = &loadChainMain;
state.config.pathOverrides.forEachInsertedDylib(^(const char* dylibPath, bool& stop) {
Diagnostics insertDiag;
if ( Loader* insertedDylib = (Loader*)Loader::getLoader(insertDiag, state, dylibPath, options) ) {
topLevelLoaders.push_back(insertedDylib);
state.notifyDebuggerLoad(insertedDylib);
if ( insertedDylib->isPrebuilt )
state.loaded.push_back(insertedDylib);
}
else if ( insertDiag.hasError() && !state.config.security.allowInsertFailures ) {
state.log("terminating because inserted dylib '%s' could not be loaded: %s\n", dylibPath, insertDiag.errorMessageCStr());
halt(insertDiag.errorMessage());
}
});
// 将插入的库移到处于状态state.loaded中的主可执行文件之前,以获得正确的平面命名空间查找
if ( topLevelLoaders.count() != 1 ) {
state.loaded.erase(state.loaded.begin());
state.loaded.push_back(mainLoader);
}
// 用于录制一定丢失的文件
__block MissingPaths missingPaths;
auto missingLogger = ^(const char* mustBeMissingPath) {
missingPaths.addPath(mustBeMissingPath);
};
// 递归加载 主可执行文件 和 插入的dylib所需的所有内容
// 记录插入信息, 遍历所有dylibs, 一些记录检查操作继续往下走
Diagnostics depsDiag;
options.insertedDylib = false;
if ( needToWritePrebuiltLoaderSet )
options.pathNotFoundHandler = missingLogger;
for ( Loader* ldr : topLevelLoaders ) {
ldr->loadDependents(depsDiag, state, options);
if ( depsDiag.hasError() ) {
//state.log("%s loading dependents of %s\n", depsDiag.errorMessage(), ldr->path());
// 让debugger/crashreporter了解我们能够加载的dylib
uintptr_t topCount = topLevelLoaders.count();
STACK_ALLOC_VECTOR(const Loader*, newLoaders, state.loaded.size() - topCount);
for (size_t i = topCount; i != state.loaded.size(); ++i)
newLoaders.push_back(state.loaded[i]);
state.notifyDebuggerLoad(newLoaders);
gProcessInfo->terminationFlags = 1; // 不要显示痕迹,因为没有什么有趣的
halt(depsDiag.errorMessage());
}
}
uintptr_t topCount = topLevelLoaders.count();
{
STACK_ALLOC_VECTOR(const Loader*, newLoaders, state.loaded.size());
for (const Loader* ldr : state.loaded)
newLoaders.push_back(ldr);
// 在主可执行文件之后通知调试器debuger所有加载的映像
std::span<const Loader*> unnotifiedNewLoaders(&newLoaders[topCount], newLoaders.size() - topCount);
state.notifyDebuggerLoad(unnotifiedNewLoaders);
// 通知kernel内核任何dtrace静态用户探测
state.notifyDtrace(newLoaders);
}
// <rdar://problem/7739489> 记录images的初始计数
// 因此CrashReporter可以记录哪些images是动态加载的
gProcessInfo->initialImageCount = state.loaded.size();
// 添加到永久范围
STACK_ALLOC_ARRAY(const Loader*, nonCacheNeverUnloadLoaders, state.loaded.size());
for (const Loader* ldr : state.loaded) {
if ( !ldr->dylibInDyldCache )
nonCacheNeverUnloadLoaders.push_back(ldr);
}
state.addPermanentRanges(nonCacheNeverUnloadLoaders);
// proactive weakDefMap means we build the weakDefMap before doing any binding
// 主动weakDefMap意味着我们在进行任何绑定之前构建weakDefMap
if ( state.config.process.proactivelyUseWeakDefMap ) {
state.weakDefMap = new (state.persistentAllocator.malloc(sizeof(WeakDefMap))) WeakDefMap();
STACK_ALLOC_VECTOR(const Loader*, allLoaders, state.loaded.size());
for (const Loader* ldr : state.loaded)
allLoaders.push_back(ldr);
Loader::addWeakDefsToMap(state, allLoaders);
}
// 在执行修复之前检查是否插入元组
state.buildInterposingTables();
// do fixups做修补工作,这里也是去处理了`rebase`
{
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
// 以防万一我们需要修补这个案子
DyldCacheDataConstLazyScopedWriter cacheDataConst(state);
// C++规范表示,主要可执行文件可以定义非弱函数,这些函数覆盖dylib中的weak-defs
// 对于在启动时绑定的任何对象,这都会自动发生,但dyld缓存是预绑定pre-bound的,因此我们需要修补主可执行文件中被该非弱对象覆盖的任何绑定。
// 注意,在macOS上,我们还允许dylib具有weak-defs的non-weak覆盖
if ( !mainLoader->isPrebuilt )
JustInTimeLoader::handleStrongWeakDefOverrides(state, cacheDataConst);
for ( const Loader* ldr : state.loaded ) {
Diagnostics fixupDiag;
ldr->applyFixups(fixupDiag, state, cacheDataConst, true);
if ( fixupDiag.hasError() ) {
halt(fixupDiag.errorMessage());
}
// 根目录需要修补缓存中未找到的GOT
ldr->applyCachePatches(state, cacheDataConst);
}
// 如果我们有单例修补
state.doSingletonPatching();
}
// 如果存在插入,则将插入元组应用于dyld缓存
if ( !state.interposingTuplesAll.empty() ) {
Loader::applyInterposingToDyldCache(state);
}
// 如果mainLoader是pre-build,则可能会覆盖dyld缓存中的weak-defs
if ( mainLoader->isPrebuilt ) {
DyldCacheDataConstLazyScopedWriter dataConstWriter(state);
DyldCacheDataConstLazyScopedWriter* dataConstWriterPtr = &dataConstWriter; // 在cacheWeakDefFixup中进行变通以使其可访问
state.processPrebuiltLoaderSet()->forEachCachePatch(^(const PrebuiltLoaderSet::CachePatch& patch) {
uintptr_t newImpl = (uintptr_t)patch.patchTo.value(state);
state.config.dyldCache.addr->forEachPatchableUseOfExport(patch.cacheDylibIndex, patch.cacheDylibVMOffset,
^(uint64_t cacheVMOffset,
dyld3::MachOLoaded::PointerMetaData pmd, uint64_t addend) {
uintptr_t* loc = (uintptr_t*)(((uint8_t*)state.config.dyldCache.addr) + cacheVMOffset);
uintptr_t newValue = newImpl + (uintptr_t)addend;
#if __has_feature(ptrauth_calls)
if ( pmd.authenticated )
newValue = MachOLoaded::ChainedFixupPointerOnDisk::Arm64e::signPointer(newValue, loc, pmd.usesAddrDiversity, pmd.diversity, pmd.key);
#endif
// 忽略重复的修补程序条目
if ( *loc != newValue ) {
dataConstWriterPtr->makeWriteable();
if ( state.config.log.fixups )
state.log("cache patch: %p = 0x%0lX\n", loc, newValue);
*loc = newValue;
}
});
});
}
// call kdebug trace for each image
if ( kdebug_is_enabled(KDBG_CODE(DBG_DYLD, DBG_DYLD_UUID, DBG_DYLD_UUID_MAP_A)) ) {
// dyld in the cache event was sent earlier when we unmapped the on-disk dyld
// 缓存事件中的dyld是在我们取消映射磁盘上的dyld时发送的
if ( !dyldMH->inDyldCache() ) {
// 为dyld本身添加跟踪
uuid_t dyldUuid;
dyldMH->getUuid(dyldUuid);
struct stat stat_buf;
fsid_t dyldFsid = { { 0, 0 } };
fsobj_id_t dyldFfsobjid = { 0, 0 };
if ( dyld3::stat(state.config.process.dyldPath, &stat_buf) == 0 ) {
dyldFfsobjid = *(fsobj_id_t*)&stat_buf.st_ino;
dyldFsid = { { stat_buf.st_dev, 0 } };
}
kdebug_trace_dyld_image(DBG_DYLD_UUID_MAP_A, state.config.process.dyldPath, &dyldUuid, dyldFfsobjid, dyldFsid, dyldMH);
}
// 为每个image的加载过程添加跟踪
for ( const Loader* ldr : state.loaded ) {
const MachOLoaded* ml = ldr->loadAddress(state);
fsid_t fsid = { { 0, 0 } };
fsobj_id_t fsobjid = { 0, 0 };
struct stat stat_buf;
if ( !ldr->dylibInDyldCache && (dyld3::stat(ldr->path(), &stat_buf) == 0) ) { //FIXME Loader knows inode
fsobjid = *(fsobj_id_t*)&stat_buf.st_ino;
fsid = { { stat_buf.st_dev, 0 } };
}
uuid_t uuid;
ml->getUuid(uuid);
kdebug_trace_dyld_image(DBG_DYLD_UUID_MAP_A, ldr->path(), &uuid, fsobjid, fsid, ml);
}
}
// 通知任何其他检查此进程的进程
// 通知跟踪此进程中负载的任何进程
STACK_ALLOC_ARRAY(const char*, pathsBuffer, state.loaded.size());
STACK_ALLOC_ARRAY(const mach_header*, mhBuffer, state.loaded.size());
for ( const Loader* ldr : state.loaded ) {
pathsBuffer.push_back(ldr->path());
mhBuffer.push_back(ldr->loadAddress(state));
}
notifyMonitoringDyld(false, (unsigned int)state.loaded.size(), &mhBuffer[0], &pathsBuffer[0]);
// 将libdyld.dylib连接到dyld
LibdyldDyld4Section* libdyld4Section = nullptr;
if ( state.libdyldLoader != nullptr ) {
const MachOLoaded* libdyldML = state.libdyldLoader->loadAddress(state);
uint64_t sectSize;
libdyld4Section = (LibdyldDyld4Section*)libdyldML->findSectionContent("__DATA", "__dyld4", sectSize, true);
#if __has_feature(ptrauth_calls)
if ( libdyld4Section == nullptr )
libdyld4Section = (LibdyldDyld4Section*)libdyldML->findSectionContent("__AUTH", "__dyld4", sectSize, true);
#endif
if ( libdyld4Section != nullptr ) {
// 设置指向全局API对象的指针
libdyld4Section->apis = &state;
// 将指针设置为dyld_all_image_infosos
libdyld4Section->allImageInfos = gProcessInfo;
// 程序变量(例如environ)通常在libdyld.dylib中定义(但可能在旧macOS二进制文件的主可替换文件中定义)
// 记住progams变量的位置,以便libc可以同步它们
state.vars = &libdyld4Section->defaultVars;
state.vars->mh = state.config.process.mainExecutable;
*state.vars->NXArgcPtr = state.config.process.argc;
*state.vars->NXArgvPtr = (const char**)state.config.process.argv;
*state.vars->environPtr = (const char**)state.config.process.envp;
*state.vars->__prognamePtr = state.config.process.progname;
}
else {
halt("compatible libdyld.dylib not found");
}
}
else {
halt("libdyld.dylib not found");
}
if ( state.libSystemLoader == nullptr)
halt("program does not link with libSystem.B.dylib");
#if !TARGET_OS_SIMULATOR
// 如果使用JustInTimeLoader启动,可能需要将其序列化
if ( needToWritePrebuiltLoaderSet ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_BUILD_CLOSURE, 0, 0, 0);
if ( state.config.log.loaders )
state.log("building PrebuiltLoaderSet for main executable\n");
Diagnostics prebuiltDiag;
const PrebuiltLoaderSet* prebuiltAppSet = PrebuiltLoaderSet::makeLaunchSet(prebuiltDiag, state, missingPaths);
if ( (prebuiltAppSet != nullptr) && prebuiltDiag.noError() ) {
if ( state.failIfCouldBuildAppClosureFile() )
halt("dyld: PrebuiltLoaderSet expected but not found");
// 将PrebuiltLoaderSet保存到磁盘以便下次启动时使用,继续使用JustInTimeLoaders运行
if ( state.saveAppPrebuiltLoaderSet(prebuiltAppSet) )
state.setSavedPrebuiltLoaderSet();
prebuiltAppSet->deallocate();
timer.setData4(dyld3::DyldTimingBuildClosure::LaunchClosure_Built);
}
else if ( state.config.log.loaders ) {
state.log("could not build PrebuiltLoaderSet: %s\n", prebuiltDiag.errorMessage());
}
}
// 如果应用程序启动pre-warm,请提前退出
if ( state.config.process.environ("DYLD_JUST_BUILD_CLOSURE") != nullptr ) {
return &fake_main;
}
#endif
#if SUPPPORT_PRE_LC_MAIN
uint32_t progVarsOffset;
dyld3::DyldLookFunc* dyldLookupFuncAddr = nullptr;
bool crtRunsInitializers = false;
if ( state.config.process.mainExecutable->hasProgramVars(progVarsOffset, crtRunsInitializers, dyldLookupFuncAddr) ) {
// 这是一个旧的macOS应用程序,它有自己的NXArgv等全局变量。我们需要使用它们。
ProgramVars* varsInApp = (ProgramVars*)(((uint8_t*)state.config.process.mainExecutable) + progVarsOffset);
varsInApp->mh = state.config.process.mainExecutable;
*varsInApp->NXArgcPtr = state.config.process.argc;
*varsInApp->NXArgvPtr = (const char**)state.config.process.argv;
*varsInApp->environPtr = (const char**)state.config.process.envp;
*varsInApp->__prognamePtr = state.config.process.progname;
state.vars = varsInApp;
}
if ( dyldLookupFuncAddr ) {
if ( libdyld4Section != nullptr ) {
*dyldLookupFuncAddr = (dyld3::DyldLookFunc)libdyld4Section->dyldLookupFuncAddr;
} else {
halt("compatible libdyld.dylib not found");
}
}
if ( !crtRunsInitializers )
state.runAllInitializersForMain();
#else
// 运行所有的初始化
dyld4::notifyMonitoringDyldBeforeInitializers();
state.runAllInitializersForMain();
...
...
return result;
}
-
结论:
①. 返回
main
函数地址,所有配置完后_dyld_register_driverkit_main
就是我们自定义工程的main
地址。②.
gProcessInfo
是存储dyld
所有images
镜像信息的结构体,保存着mach_header
、dyld_uuid_info
、dyldVersion
等等信息。③. 创建
just-in-time
,是dyld4
一个新特性。dyld4
在保留了dyld3
的mach-o
解析器基础上,同时也引入了just-in-time
的加载器来优化。-
dyld3
出于对启动速度的优化的目的, 增加了预构建(闭包)。App
第一次启动或者App发生变化时会将部分启动数据创建为闭包存到本地,那么App下次启动将不再重新解析数据,而是直接读取闭包内容。当然前提是应用程序和系统应很少发生变化,但如果这两者经常变化等, 就会导闭包丢失或失效。 -
dyld4
采用了pre-build
+just-in-time
的双解析模式,预构建pre-build
对应的就是dyld3
中的闭包,just-in-time
可以理解为实时解析。当然just-in-time
也是可以利用pre-build
的缓存的,所以性能可控。有了just-in-time
, 目前应用首次启动、系统版本更新、普通启动,dyld4
则可以根据缓存是否有效去选择合适的模式进行解析。
-
runAllInitializersForMain
// 这是dyldMain.cpp中提取出来的一部分,以支持使用crt1.o的旧macOS应用程序
void APIs::runAllInitializersForMain()
{
// 禁用page-in链接,不用于dlopen()加载images
if ( !config.security.internalInstall || (config.process.pageInLinkingMode != 3) )
config.syscall.disablePageInLinking();
// 首先允许libSystem的初始化器
const_cast<Loader*>(this->libSystemLoader)->beginInitializers(*this);
this->libSystemLoader->runInitializers(*this);
gProcessInfo->libSystemInitialized = true;
// 运行libSystem的初始化程序后,告诉objc在libSystem子dylib上运行任何+load方法
this->notifyObjCInit(this->libSystemLoader);
// <rdar://problem/32209809> 调用所有已经images的'init' 函数 (below libSystem)
// 使用下标索引进行迭代,以便在+load dloopen时数组不会在我们下面增长
for ( uint32_t i = 0; i != this->loaded.size(); ++i ) {
const Loader* ldr = this->loaded[i];
if ( (ldr->dylibInDyldCache || ldr->analyzer(*this)->isDylib()) && (strncmp(ldr->analyzer(*this)->installName(), "/usr/lib/system/lib", 19) == 0) ) {
// 检查安装名称而不是路径,以处理libsystem子dylibs的DYLD_LIBRARY_PATH覆盖
const_cast<Loader*>(ldr)->beginInitializers(*this);
this->notifyObjCInit(ldr);
const_cast<Loader*>(ldr)->runInitializers(*this);
}
}
#if TARGET_OS_OSX
// If we're PID 1, scan for roots
if ( (this->config.process.pid == 1) && (this->libSystemHelpers->version() >= 5) ) {
this->libSystemHelpers->run_async(&ProcessConfig::scanForRoots, (void*)&this->config);
}
#endif // TARGET_OS_OSX
//自下而上运行所有其他初始化器,首先运行插入的dylib初始化器
//使用下标索引进行迭代,以便如果初始dloopen时数组不会向下增长
for ( uint32_t i = 0; i != this->loaded.size(); ++i ) {
const Loader* ldr = this->loaded[i];
ldr->runInitializersBottomUpPlusUpwardLinks(*this);
// stop as soon as we did main executable
// normally this is first image, but if there are inserted N dylibs, it is Nth in the list
if ( ldr->analyzer(*this)->isMainExecutable() )
break;
}
}
-
结论:
①. 在libSystem的初始化时,用
notifyObjCInit
通知libSystem
依赖的子dylibs
运行所有的+load
方法,这里不是我们自定义工程里的+load
②. 处理完
libSystem
的初始化后,再加载后自下而上运行所有其他镜像的初始化器。
runInitializersBottomUpPlusUpwardLinks
void Loader::runInitializersBottomUpPlusUpwardLinks(RuntimeState& state) const
{
//state.log("runInitializersBottomUpPlusUpwardLinks() %s\n", this->path());
state.incWritable();
// 递归运行所有初始化程序 initializers
STACK_ALLOC_ARRAY(const Loader*, danglingUpwards, state.loaded.size());
this->runInitializersBottomUp(state, danglingUpwards);
//state.log("runInitializersBottomUpPlusUpwardLinks(%s), found %d dangling upwards\n", this->path(), danglingUpwards.count());
// 返回到所有向上链接的images,并重新检查它们是否已初始化(可能是挂起的)
STACK_ALLOC_ARRAY(const Loader*, extraDanglingUpwards, state.loaded.size());
for ( const Loader* ldr : danglingUpwards ) {
//state.log("running initializers for dangling upward link %s\n", ldr->path());
ldr->runInitializersBottomUp(state, extraDanglingUpwards);
}
if ( !extraDanglingUpwards.empty() ) {
// 如果有两个向上悬挂的images,请再次检查初始化器
danglingUpwards.resize(0);
for ( const Loader* ldr : extraDanglingUpwards ) {
//state.log("running initializers for dangling upward link %s\n", ldr->path());
ldr->runInitializersBottomUp(state, danglingUpwards);
}
}
state.decWritable();
}
-
结论:
①.递归运行所有初始化程序
initializers
runInitializersBottomUp
void Loader::runInitializersBottomUp(RuntimeState& state, Array<const Loader*>& danglingUpwards) const
{
// 如果已运行初始化程序,则不执行任何操作
if ( (const_cast<Loader*>(this))->beginInitializers(state) )
return;
//state.log("runInitializersBottomUp(%s)\n", this->path());
// 在运行初始化程序之前,请确保此image下面的所有内容都已初始化
const uint32_t depCount = this->dependentCount();
for ( uint32_t i = 0; i < depCount; ++i ) {
DependentKind childKind;
if ( Loader* child = this->dependent(state, i, &childKind) ) {
if ( childKind == DependentKind::upward ) {
// 向上添加到列表以稍后处理
if ( !danglingUpwards.contains(child) )
danglingUpwards.push_back(child);
}
else {
child->runInitializersBottomUp(state, danglingUpwards);
}
}
}
// 告诉objc在此image映像中运行任何+load方法(在C++初始化器之前完成)
state.notifyObjCInit(this);
// 为此image映像运行初始化程序
this->runInitializers(state);
}
-
结论:
①. 最后通过
notifyObjCInit
通知objc
在此image
映像中运行任何+load
方法(在C++
初始化器之前完成)。
notifyObjCInit
void RuntimeState::notifyObjCInit(const Loader* ldr)
{
//this->log("objc-init-notifier checking mh=%p, path=%s, +load=%d, objcInit=%p\n", ldr->loadAddress(), ldr->path(), ldr->mayHavePlusLoad, _notifyObjCInit);
if ( (_notifyObjCInit != nullptr) && ldr->mayHavePlusLoad ) {
const MachOLoaded* ml = ldr->loadAddress(*this);
const char* pth = ldr->path();
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)ml, 0, 0);
if ( this->config.log.notifications )
this->log("objc-init-notifier called with mh=%p, path=%s\n", ml, pth);
_notifyObjCInit(pth, ml);
}
}
-
结论:
②. 主要是遍历各个库文件,通过
_notifyObjCInit
来加载+load
方法。
探究_notifyObjCInit
来源
_notifyObjCInit
的定义是在RuntimeState
类的声明里:
_dyld_objc_notify_init
在setObjCNotifiers
被赋值的:
#if BUILDING_DYLD || BUILDING_UNIT_TESTS
void RuntimeState::setObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped, _dyld_objc_notify_patch_class patchClass,
_dyld_objc_notify_mapped2 mapped2)
{
_notifyObjCMapped = mapped;
_notifyObjCInit = init;
_notifyObjCUnmapped = unmapped;
_notifyObjCPatchClass = patchClass;
_notifyObjCMapped2 = mapped2;
withLoadersReadLock(^{
if ( (_notifyObjCPatchClass != nullptr) && !this->objcReplacementClasses.empty() ) {
// 告诉Symbolication象征我们正在修补类
this->setDyldPatchedObjCClasses();
for ( const ObjCClassReplacement& classReplacement : this->objcReplacementClasses )
(*_notifyObjCPatchClass)(classReplacement.cacheMH, (void*)classReplacement.cacheImpl,
classReplacement.rootMH, (const void*)classReplacement.rootImpl);
if ( this->config.log.notifications ) {
this->log("objc-patch-class-notifier called with %ld patches:\n", this->objcReplacementClasses.size());
}
// 清除替换类。如果发生dlopen,我们不想再次通知它们
this->objcReplacementClasses.clear();
}
// 回调关于已加载图像images
size_t maxCount = this->loaded.size();
STACK_ALLOC_ARRAY(const mach_header*, mhs, maxCount);
STACK_ALLOC_ARRAY(const char*, paths, maxCount);
STACK_ALLOC_ARRAY(_dyld_objc_notify_mapped_info, infos, maxCount);
for ( const Loader* ldr : loaded ) {
// 这里不需要_mutex互斥锁,因为这是在进程仍然是单线程时调用的
const MachOLoaded* ml = ldr->loadAddress(*this);
if ( ldr->hasObjC ) {
paths.push_back(ldr->path());
mhs.push_back(ml);
infos.push_back({ml, ldr->path(), ldr->dyldDoesObjCFixups(), 0});
// 在map_images运行时内存read-write可读写
if ( ldr->hasConstantSegmentsToProtect() && ldr->hasReadOnlyObjC )
ldr->makeSegmentsReadWrite(*this);
}
}
if ( !mhs.empty() ) {
if ( _notifyObjCMapped != nullptr )
// 通过指针_notifyObjCMapped指向运行map_images
(*_notifyObjCMapped)((uint32_t)mhs.count(), &paths[0], &mhs[0]);
if ( _notifyObjCMapped2 != nullptr )
(*_notifyObjCMapped2)((uint32_t)mhs.count(), &infos[0]);
if ( this->config.log.notifications ) {
this->log("objc-mapped-notifier called with %ld images:\n", mhs.count());
for ( uintptr_t i = 0; i < mhs.count(); ++i ) {
this->log(" objc-mapped: %p %s\n", mhs[i], paths[i]);
}
}
// 运行map_images后使内存为只读read-only
for ( const Loader* ldr : loaded ) {
if ( ldr->hasObjC && ldr->hasConstantSegmentsToProtect() && ldr->hasReadOnlyObjC )
ldr->makeSegmentsReadOnly(*this);
}
}
});
}
- 那
setObjCNotifiers
是在_dyld_objc_notify_register
被调用的:
// FIXME: 一旦libobjc移动到_dyld_objc_register_callbacks(),请删除此项
// 这个是先前的注册方法
void APIs::_dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
if ( config.log.apis )
log("_dyld_objc_notify_register(%p, %p, %p)\n", mapped, init, unmapped);
setObjCNotifiers(mapped, init, unmapped, nullptr, nullptr);
// If we have prebuilt loaders, then the objc optimisations may hide duplicate classes from libobjc.
// We need to print the same warnings libobjc would have.
if ( const PrebuiltLoaderSet* mainSet = this->processPrebuiltLoaderSet() )
mainSet->logDuplicateObjCClasses(*this);
}
// 最新的代码objc4的注册方法
void APIs::_dyld_objc_register_callbacks(const _dyld_objc_callbacks* callbacks)
{
if ( config.log.apis ) {
void** p = (void**)callbacks;
log("_dyld_objc_register_callbacks(%lu, %p, %p, %p, %p)\n", callbacks->version, p[1], p[2], p[31], p[4]);
}
if ( callbacks->version == 1 ) {
const _dyld_objc_callbacks_v1* v1 = (const _dyld_objc_callbacks_v1*)callbacks;
setObjCNotifiers(v1->mapped, v1->init, v1->unmapped, v1->patches, nullptr);
}
else if ( callbacks->version == 2 ) {
const _dyld_objc_callbacks_v2* v2 = (const _dyld_objc_callbacks_v2*)callbacks;
setObjCNotifiers(nullptr, v2->init, v2->unmapped, v2->patches, v2->mapped);
}
else {
}
// 如果我们有预构建的加载器,那么objc优化可能会从libobjc中隐藏重复的类。
// 我们需要打印libobjc将具有的相同警告。
if ( const PrebuiltLoaderSet* mainSet = this->processPrebuiltLoaderSet() )
mainSet->logDuplicateObjCClasses(*this);
}
-
结论:
①. 先前
dyld3
的注册方法_dyld_objc_notify_register
,最新dyld4
是_dyld_objc_register_callbacks
。
- 而
_dyld_objc_register_callbacks
是在objc4-866.9源码里的objc_init()
里被调用的
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**************************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
static_init();
runtime_init();
exception_init();
cache_t::init();
_imp_implementationWithBlock_init();
_dyld_objc_callbacks_v1 callbacks = {
1, // version
&map_images,
load_images,
unmap_image,
_objc_patch_root_of_class
};
_dyld_objc_register_callbacks((_dyld_objc_callbacks*)&callbacks);
didCallDyldNotifyRegister = true;
}
-
结论:
①. 这里解释了先前dyld入口探索,调用完
notifyObjCInit
后就是load_images
。②.
map_images
映射镜像、load_images
加载镜像、unmap_image
解除镜像是objc4
源码初始化配置的重点,为了进一步了解dyld4
对于images
镜像们的管理,我要先去了解objc4
的objc_init。
五、总结
-
分析了
map_images
与load_images
的注册和load_images
的调用流程,但是未具体分析objc4
的_objc_init
、map_images
、load_images
的源码与流程。 -
只分析了
dyld4
的部分流程,而未分析dyld3
的流程。下面是两者对比:
-
dyld3
: -
dyld4
: