dyld
在 MacOS 和 iOS 上,可执行程序的启动依赖于 xnu 内核进程运作和动态链接加载器 dyld。dyld 全称 the dynamic link editor,即动态链接器,其本质是 Mach-O 文件,是专门用来加载动态库的库。
在 xnu 内核为程序启动做好准备后,执行由内核态切换到用户态,由 dyld 完成后面的加载工作:dyld 会将 App 依赖的动态库加载到内存并做一些初始化的工作。
_dyld_start之前
我们都知道我们在APP中通过打断点最早能追溯到的代码是_dyld_start
,大多数的博客分析也是从_dyld_start开始的,但是_dyld_start之前又发生什么了呢?主要是完成了以下三件事:
- 内核为App fork一个新的进程
- 解析加载App主二进制文件
- 根据App的LoadCommand解析出dyld,并解析加载dyld
1
2
3
4
5
6
7
8
9
10
▼ execve // 用户点击了app, 用户态会发送一个系统调用 execve 到内核,然后内核 fork 一个新的进程。
▼ __mac_execve // 创建线程
▼ exec_activate_image // 在 encapsulated_binary 这一步会根据image的类型选择imgact的方法
▼ exec_mach_imgact
▼ load_machfile
▶︎ parse_machfile //解析主二进制macho
▼ load_dylinker // 解析完 macho后,根据macho中的 LC_LOAD_DYLINKER 这个LoadCommand来启动这个二进制的加载器,即 /usr/bin/dyld
▼ parse_machfile // 解析 dyld 这个mach-o文件,这个过程中会解析出entry_point
▼ activate_exec_state
▶︎ thread_setentrypoint // 设置entry_point。
如果我们在 _dyld_start 打一个断点,然后 image dump sections 一下看看,可以看到此时确实只加载了App的二进制和dyld两个文件,其他动态库还未被加载进来(这正是dyld后续要做的工作)

_dyld_start
_dyld_start再通过 start 跳转到 _main 函数中,然后开始表演时刻。main函数大概有900行代码,主要做了以下事情:
- 设置运行环境
- 加载共享缓存
- 实例化主程序
- 加载插入的动态库
- 链接主程序
- 链接插入的动态库
- 弱符号绑定
- 初始化动态库、主程序
- 查找并返回主程序函数入口
下面针对其中的几个环境展开分析一下
加载共享缓存
一个iOS APP要启动起来大概要加载四百多个动态库,要加载这个四百多个动态库要消耗大量的时间和内存空间。共享缓存机制的引入就是为了解决这个问题的。从iOS 3.1开始,所有的系统库文件都被打包合并成一个大的缓存文件中,而原来的动态库文件则被去除了,系统直接去缓存文件中加载动态库,该共享缓存文件保存在/System/Library/Caches/com.apple.dyld/目录下

iOS这里主要做了以下三件事:
- 检测共享缓存是否可用
- 如果可用,映射共享缓存到共享区
- 添加 dyld 的 UUID 到缓存列表
其中核心逻辑就是映射共享缓存到共享区
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
static void mapSharedCache()
{
dyld3::SharedCacheOptions opts;
opts.cacheDirOverride = sSharedCacheOverrideDir;
opts.forcePrivate = (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);
#if __x86_64__ && !TARGET_IPHONE_SIMULATOR
opts.useHaswell = sHaswell;
#else
opts.useHaswell = false;
#endif
opts.verbose = gLinkContext.verboseMapping;
// 加载 dyld 缓存
loadDyldCache(opts, &sSharedCacheLoadInfo);
// update global state
// 更新进程的全局状态信息
if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
gLinkContext.dyldCache = sSharedCacheLoadInfo.loadAddress;
dyld::gProcessInfo->processDetachedFromSharedRegion = opts.forcePrivate;
dyld::gProcessInfo->sharedCacheSlide = sSharedCacheLoadInfo.slide;
dyld::gProcessInfo->sharedCacheBaseAddress = (unsigned long)sSharedCacheLoadInfo.loadAddress;
sSharedCacheLoadInfo.loadAddress->getUUID(dyld::gProcessInfo->sharedCacheUUID);
dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_SHARED_CACHE_A, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, 0, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
}
}
bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
results->loadAddress = 0;
results->slide = 0;
results->errorMessage = nullptr;
#if TARGET_IPHONE_SIMULATOR
// simulator only supports mmap()ing cache privately into process
// 模拟器只支持 mmap(内存映射) 缓存到当前进程
return mapCachePrivate(options, results);
#else
if ( options.forcePrivate ) {
// mmap cache into this process only
// 只加载 mmap(内存映射) 缓存到当前进程
return mapCachePrivate(options, results);
}
else {
// fast path: when cache is already mapped into shared region
bool hasError = false;
// 已加载过的
if ( reuseExistingCache(options, results) ) {
hasError = (results->errorMessage != nullptr);
}
// 未加载过的
else {
// slow path: this is first process to load cache
hasError = mapCacheSystemWide(options, results);
}
return hasError;
}
#endif
}
链接主程序
链接主程序是比较关键的一个步骤。它主要做了以下几件事情:
- 递归的加载主程序依赖的库
- 递归修正自己和依赖库的基地址(Rebase),因为 ASLR 的原因,需要根据随机 slide 修正基地址
- 递归绑定 noLazy 的符号表,lazy的符号会在运行时动态绑定(首次被调用才去绑定)
- 绑定弱符号表,比如未初始化的全局变量就是弱符号
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
void link(ImageLoader* image, bool forceLazysBound, bool neverUnload, const ImageLoader::RPathChain& loaderRPaths, unsigned cacheIndex)
{
// add to list of known images. This did not happen at creation time for bundles
// 添加到已知镜像列表中。这在创建 bundles 时没有处理。
if ( image->isBundle() && !image->isLinked() )
addImage(image);
// we detect root images as those not linked in yet
// 在根镜像中检测是否尚未链接
if ( !image->isLinked() )
addRootImage(image);
// process images
try {
const char* path = image->getPath();
#if SUPPORT_ACCELERATE_TABLES
if ( image == sAllCacheImagesProxy )
path = sAllCacheImagesProxy->getIndexedPath(cacheIndex);
#endif
// 调用 ImageLoader::link() 链接
image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
}
catch (const char* msg) {
// 标记 image 为未使用,处理
garbageCollectImages();
throw;
}
}
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
//dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);
// 起始时间。用于记录时间间隔
uint64_t t0 = mach_absolute_time();
// ①、递归加载主程序依赖的库,完成之后发送一个状态为 dyld_image_state_dependents_mapped的通知。
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
// we only do the loading step for preflights 只做预检的装载步骤
if ( preflightOnly )
return;
uint64_t t1 = mach_absolute_time();
// 清空 image 层级关系
context.clearAllDepths();
// 递归更新 image 层级关系
this->recursiveUpdateDepth(context.imageCount());
__block uint64_t t2, t3, t4, t5;
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
t2 = mach_absolute_time();
// ②、递归修正自己和依赖库的基地址,因为 ASLR 的原因,需要根据随机 slide 修正基地址。
this->recursiveRebase(context);
context.notifyBatch(dyld_image_state_rebased, false);
t3 = mach_absolute_time();
if ( !context.linkingMainExecutable )
// ③、递归绑定 noLazy 的符号表,lazy的符号会在运行时动态绑定(首次被调用才去绑定)
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
t4 = mach_absolute_time();
if ( !context.linkingMainExecutable )
// ④、绑定弱符号表,比如未初始化的全局变量就是弱符号。
this->weakBind(context);
t5 = mach_absolute_time();
}
if ( !context.linkingMainExecutable )
context.notifyBatch(dyld_image_state_bound, false);
uint64_t t6 = mach_absolute_time();
std::vector<DOFInfo> dofs;
// ⑤、递归获取/注册程序的 DOF 节区,dtrace 会用其动态跟踪。
this->recursiveGetDOFSections(context, dofs);
// 注册
context.registerDOFs(dofs);
uint64_t t7 = mach_absolute_time();
// interpose any dynamically loaded images
if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
// 递归应用插入的动态库
this->recursiveApplyInterposing(context);
}
// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);
// 计算出各种时间间隔
fgTotalLoadLibrariesTime += t1 - t0;
fgTotalRebaseTime += t3 - t2;
fgTotalBindTime += t4 - t3;
fgTotalWeakBindTime += t5 - t4;
fgTotalDOF += t7 - t6;
// done with initial dylib loads
fgNextPIEDylibAddress = 0;
}
这里同时还计算各步骤的耗时。如果想获取这些耗时,只需要在环境变量中添加 DYLD_PRINT_STATISTICS 就可以了。
如果我们在link处断点以下,可以发现第一次执行完link后,进程中的image数量由2个(主程序和dyld)变成了100多个,这多出来的100多个就是主程序递归依赖的。刚才我们说有400多个,另外的300后面还会继续调用link来加载。
初始化动态库、主程序
然后先初始化动态库,然后初始化主程序。例如objc runtime的初始化就是在此时进行的,我们可以通过堆栈来看一下:

动态库和主程序的初始化是从 initializeMainExecutable -> runInitializers -> processInitializers -> recursiveInitialization 递归初始化当前 image 所依赖的库。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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]);
// 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]);
}
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.images[uninitUps.count] = dependentImage;
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
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
// 真正初始化镜像
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);
if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.addTime(this->getShortName(), t2-t1);
}
}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}
recursiveSpinUnLock();
}
recursiveInitialization 中调用 doInitialization 来初始化镜像:
1
2
3
4
5
6
7
8
9
10
11
12
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
- doImageInit 执行 LC_ROUTINES_COMMAND segment 中保存的函数。
- doModInitFunctions执行 __DATA 中 __mod_init_func section 中保存的函数。这个 section 中保存的是 C++ 的构造函数及带有 attribute((constructor)) 的 C 函数。
从下图中我们可以看到 libSystem_initializer -> libdispatch_init -> _os_object_init -> _objc_init 的执行路径




查找主程序入口
最后dyld通过分析主程序的macho_header找到主程序的main函数入口,并返回给上一级函数(_dyld_start)进行一个jmp操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void* ImageLoaderMachO::getEntryFromLC_MAIN() const
{
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
if ( cmd->cmd == LC_MAIN ) {
entry_point_command* mainCmd = (entry_point_command*)cmd;
void* entry = (void*)(mainCmd->entryoff + (char*)fMachOData);
// <rdar://problem/8543820&9228031> verify entry point is in image
if ( this->containsAddress(entry) )
return entry;
else
throw "LC_MAIN entryoff is out of range";
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
return NULL;
}
0x100015020 <+32>: callq 0x100015062 ; dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*)
0x100015025 <+37>: movq -0x8(%rbp), %rdi
0x100015029 <+41>: cmpq $0x0, %rdi
0x10001502d <+45>: jne 0x10001503f ; <+63>
0x10001502f <+47>: movq %rbp, %rsp
0x100015032 <+50>: addq $0x8, %rsp
0x100015036 <+54>: movq $0x0, %rbp
0x10001503d <+61>: jmpq *%rax
0x10001503f <+63>: addq $0x10, %rsp
0x100015043 <+67>: pushq %rdi
0x100015044 <+68>: movq 0x8(%rbp), %rdi
0x100015048 <+72>: leaq 0x10(%rbp), %rsi
0x10001504c <+76>: leaq 0x8(%rsi,%rdi,8), %rdx
0x100015051 <+81>: movq %rdx, %rcx
0x100015054 <+84>: movq (%rcx), %r8
0x100015057 <+87>: addq $0x8, %rcx
0x10001505b <+91>: testq %r8, %r8
0x10001505e <+94>: jne 0x100015054 ; <+84>
0x100015060 <+96>: jmpq *%rax

后续计划
- 我们分析完dyld的过程后,后续我们再来看看objc_init又都干了些什么?
- 链接主程序,链接动态库是具体怎么完成的?fishHook又是怎么实现的?