0%

详解 dyld 加载过程

超长预警…….

本文将会从dyld接管进程开始,超级详细的分析/解释,dyld 是如何把若干个动态库“组装”在一起,在ASLR的影响下形成一个完整的可执行进程,以及objc的runtime如何在dyld的初始化过程中进行实例化。

dyld 加载过程

dyld的启动入口在 dyldStartup.s 文件的汇编代码中,代码入口说明了:在内核初始化完成进程,并且load 完可执行文件之后,把环境变量等参数压栈,并且调用dyld的入口函数__dyld_start,而这个函数则根据调用约定,准备完调用参数之后,就直接跳转到dyld::start 这个函数。

接下来看dyld::start

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
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
intptr_t slide, const struct macho_header* dyldsMachHeader,
uintptr_t* startGlue)
{
// 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本身的动态偏移
slide = slideOfMainExecutable(dyldsMachHeader);
bool shouldRebase = slide != 0;
if ( shouldRebase ) {
//rebase Dyld
rebaseDyld(dyldsMachHeader, slide);
}

// allow dyld to use mach messaging
//mach消息初始化。
mach_init();

// kernel sets up env pointer to be just past end of agv array
//环境变量
const char** envp = &argv[argc+1];

// kernel sets up apple pointer to be just past end of envp array
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
//初始化 在dyld中的 所有的C++构造器
runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif

// now that we are done bootstrapping dyld, call dyld's main
//至此bootstrap dyld已经全部完成,调用 dyld 的 main 函数
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
return dyld:: _main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

dyld::start的函数分为以下几步骤:

  1. rebase dyld本身
    rebase dyld
  2. 初始化进程间通讯 mach_msg
    直接调用<mach/mach_init.h>内的某个内核调用
  3. 初始化环境变量
    从调用参数中拿到apple指针,内核会把这个指针紧跟在envp的数组最后
  4. 设置栈保护
    从第三步拿到的apple指针中找到“stack_guard=xx”的字符串,自定义重置栈保护
  5. 初始化 dyld 中的所有构造器函数
    因为dyld本身是一个动态库,我们知道,动态库是共享的,但是共享的只是代码段(PIC 位置无关代码)。在内核给每一个进程的虚拟地址映射完dyld之后,dyld本身这个动态库的数据段是没有初始化的,需要自己做自己的初始化。具体代码看这里
  6. 前五步已经完成 dyld 的自启动(bootstrap),后面就是进入真正的 dyld main函数。

这里可以总结一下,start的工作内容主要是一些全局环境,以及dyld自身的初始化。

开始 分析 dyld 的_main函数

RebaseDyld

因为Dyld也是一个动态库,和普通的动态库一样,会在mach_o的load_command中指定要求需要加载到某个虚拟地址下。但是内核还是会对dyld做地址随机偏移,如果是这样,dyld需要修复数据段的数据。

  1. 找到Load_Commands中 __LINKEDIT 和 LC_DYLD_INFO_ONLY 两段,__LINKEDIT段内包含了链接信息,符号表,动态符号表等等,而LC_DYLD_INFO_ONLY内标明了:Rebase,Bind,WeakBind,LazyBind,Export 这个5类,而这5类都会存储在Dynamic Loader Info这一节中。
  2. 根据rebase 和 bind 的 Opcodes等,循环进行。

在后文rebase/bind 可执行文件依赖的动态库时,会进行详细说明。

回去继续

初始化Dyld的构造器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern const Initializer  inits_start  __asm("section$start$__DATA$__mod_init_func");
extern const Initializer inits_end __asm("section$end$__DATA$__mod_init_func");

//
// For a regular executable, the crt code calls dyld to run the executables initializers.
// For a static executable, crt directly runs the initializers.
// dyld (should be static) but is a dynamic executable and needs this hack to run its own initializers.
// We pass argc, argv, etc in case libc.a uses those arguments
//
static void runDyldInitializers(const struct macho_header* mh, intptr_t slide, int argc, const char* argv[], const char* envp[], const char* apple[])
{
for (const Initializer* p = &inits_start; p < &inits_end; ++p) {
(*p)(argc, argv, envp, apple);
}
}

在dyld的该方法的注释上,苹果写的很清楚:对于常规的可执行文件,crt代码会通过调用dyld来初始化这个可执行文件的初始化构造器。但是对于静态可执行文件,crt会自己去执行初始化。dyld可以认为是“静态”的,但是本质是一个动态库,所以需要自己来初始化自己,但是苹果称之为hack的方式。

实际上这是一段内联汇编,__asm("section$start$__DATA$__mod_init_func")意思是:获取__DATA__mod_init_func的起始地址,当然下面那行就是获取结束地址了。

在Demo代码中加入两个构造器:

然后再看这个mach-o 文件:

可以看到在数据段增加了一节__mod_init_func,编译期会把所有构造器的函数地址,都放在这节中。例如这个Section中第一个地址存储的是demo_init1的函数地址,去掉虚拟起始地址,文件内偏移地址就是0xED0:

可以直接验证确实是这个函数,demo_init2也是如此。

回过头来看,dyld的代码:这个inits_start中就是构造器列表中第一个构造器的函数地址了。inits_end就是结束地址,循环即可依次按照约定,初始化dyld自己内部的所有构造器函数了。

在dyld的内部还会有很多诸如此类的用法。
具体可以看参考文献中 1 和 2

回去继续

dyld::_main函数

在dyld自身的rebase和bind、mach_msg初始化、环境变量初始化、构造函数初始化全部完成之后,开始dyld的真正工作内容,dyld::_main 函数。

dyld::_main函数最终是返回了主进程App可执行文件的main函数入口,并且由dyld::start函数再返回给调用它的__dyld_start这个汇编函数,最终由最开始的这个汇编函数,准备好调用参数之后,直接调用我们所熟悉的可执行文件的main函数。

这个_main函数很长,我们只看步骤。

  1. 进入函数的最开始是对环境变量的初始化加载和控制,例如 crashlog,环境变量控制log,主进程可执行文件路径…
  2. 加载共享缓存。// iOS cannot run without shared region,iOS必须开启共享共享缓存。
    在所有libpath中搜索不到对应名字的动态库的时候,就会尝试从共享缓存中找。(当然这里为了便于理解,只是简单这样描述,实际的搜索方式远比这个要复杂)
  3. 实例化可执行文件:
    实例化可执行程序,生成ImageLoaderMachO对象。实例化主程序
    实例化可执行文件有两种(Classic和Compressed),分别由ImageLoaderMachOCompressedImageLoaderMachOClassic来创建可执行文件对象,这两者均继承于ImageLoaderMachO
  4. 加载从参数插入的动态库loadInsertedDylib,只做加载,不做链接。Load 插入的动态库.
  5. 链接可执行程序。链接可执行程序
  6. 链接从参数插入的动态库。
  7. 从可执行文件,递归开始调用初始化。

实例化主程序

在dyld获得控制权之前,内核已经把可执行文件映射到了某个线性地址空间了,所以dyld可以直接获取到 macho_header,dyld会为可执行文件创建一个ImageLoader实例对象,每一个MachO在dyld中都会有一个这个对象与之对应。

1
2
3
4
5
6
7
8
9
10
11
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);
addImage(image);
return (ImageLoaderMachO*)image;
}

throw "main executable not a known format";
}

instantiateMainExecutable这个方法内还会根据不同的mach-o类型(compress 或者 classic)来返回ImageLoader的不同实现的子类对象。

addImage(image);这个函数有两个作用:

  1. 把生成的镜像实例(ImageLoader)加入到一个全局镜像容器sAllImages内。之后加载的所有动态库都会被加入这个容器。
  2. 把可执行文件的每一个段所占据的实际线性地址范围,映射到一个全局链表 sMappedRangesStart内,而这个全局链表的作用就是为了能够快速的通过 地址 反向获取到对应的ImageLoader

回去继续

加载动态库

这一步循环遍历DYLD_INSERT_LIBRARIES环境变量中指定的动态库列表,并调用loadInsertedDylib()将其加载。

该函数调用load()完成加载工作。load()会调用loadPhase0()尝试从文件加载,loadPhase0()会向下调用下一层phase来查找动态库的路径,直到loadPhase6(),查找的顺序为DYLD_ROOT_PATH->LD_LIBRARY_PATH->DYLD_FRAMEWORK_PATH->原始路径->DYLD_FALLBACK_LIBRARY_PATH。

找到后调用ImageLoaderMachO::instantiateFromFile()来实例化一个 ImageLoader,之后调用 checkandAddImage() 验证映像并将其加入到全局镜像列表中。

如果loadPhase0()返回为空,表示在路径中没有找到动态库,就尝试从共享缓存中查找,找到就调用ImageLoaderMachO::instantiateFromCache()从缓存中加载。

否则就抛出没找到镜像的异常。

看加载动态库部分的源代码:

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// map in file and instantiate an ImageLoader
// 映射某个动态库文件并且实例化一个ImagerLoader
static ImageLoader* loadPhase6(int fd, const struct stat& stat_buf, const char* path, const LoadContext& context)
{
//dyld::log("%s(%s)\n", __func__ , path);
uint64_t fileOffset = 0;
uint64_t fileLength = stat_buf.st_size;

// validate it is a file (not directory)
if ( (stat_buf.st_mode & S_IFMT) != S_IFREG )
throw "not a file";

uint8_t firstPages[MAX_MACH_O_HEADER_AND_LOAD_COMMANDS_SIZE];
uint8_t *firstPagesPtr = firstPages;
bool shortPage = false;

// min mach-o file is 4K
// 最小的mach-o文件就是4k(保证一个页框大小)
if ( fileLength < 4096 ) {
if ( pread(fd, firstPages, (size_t)fileLength, 0) != (ssize_t)fileLength )
throwf("pread of short file failed: %d", errno);
shortPage = true;
}
else {
// optimistically read only first 4KB
// 优先读取header文件的4kb,因为一个分页最小是4k。
if ( pread(fd, firstPages, 4096, 0) != 4096 )
throwf("pread of first 4K failed: %d", errno);
}

// if fat wrapper, find usable sub-file
// 如果这个动态库是个fat文件,就直接找到合适的“子文件”
const fat_header* fileStartAsFat = (fat_header*)firstPages;
if ( fileStartAsFat->magic == OSSwapBigToHostInt32(FAT_MAGIC) ) {
if ( OSSwapBigToHostInt32(fileStartAsFat->nfat_arch) > ((4096 - sizeof(fat_header)) / sizeof(fat_arch)) )
throwf("fat header too large: %u entries", OSSwapBigToHostInt32(fileStartAsFat->nfat_arch));
if ( fatFindBest(fileStartAsFat, &fileOffset, &fileLength) ) {
if ( (fileOffset+fileLength) > (uint64_t)(stat_buf.st_size) )
throwf("truncated fat file. file length=%llu, but needed slice goes to %llu", stat_buf.st_size, fileOffset+fileLength);
if (pread(fd, firstPages, 4096, fileOffset) != 4096)
throwf("pread of fat file failed: %d", errno);
}
else {
throw "no matching architecture in universal wrapper";
}
}

// try mach-o loader
if ( shortPage )
throw "file too short";

if ( isCompatibleMachO(firstPages, path) ) {

// only MH_BUNDLE, MH_DYLIB, and some MH_EXECUTE can be dynamically loaded
const mach_header* mh = (mach_header*)firstPages;
switch ( mh->filetype ) {
case MH_EXECUTE:
case MH_DYLIB:
case MH_BUNDLE:
break;
default:
throw "mach-o, but wrong filetype";
}

uint32_t headerAndLoadCommandsSize = sizeof(macho_header) + mh->sizeofcmds;
if ( headerAndLoadCommandsSize > MAX_MACH_O_HEADER_AND_LOAD_COMMANDS_SIZE )
throwf("malformed mach-o: load commands size (%u) > %u", headerAndLoadCommandsSize, MAX_MACH_O_HEADER_AND_LOAD_COMMANDS_SIZE);

if ( headerAndLoadCommandsSize > fileLength )
dyld::throwf("malformed mach-o: load commands size (%u) > mach-o file size (%llu)", headerAndLoadCommandsSize, fileLength);

if ( headerAndLoadCommandsSize > 4096 ) {
// read more pages
// 如果 head 和 LC_COMMANDS的大小大于已读取的4096,那么就继续把 headerAndLoadCommandsSize 读完
unsigned readAmount = headerAndLoadCommandsSize - 4096;
if ( pread(fd, &firstPages[4096], readAmount, fileOffset+4096) != readAmount )
throwf("pread of extra load commands past 4KB failed: %d", errno);
}

#if TARGET_IPHONE_SIMULATOR
// <rdar://problem/14168872> dyld_sim should restrict loading osx binaries
if ( !isSimulatorBinary(firstPages, path) ) {
#if TARGET_OS_WATCH
throw "mach-o, but not built for watchOS simulator";
#elif TARGET_OS_TV
throw "mach-o, but not built for tvOS simulator";
#else
throw "mach-o, but not built for iOS simulator";
#endif
}
#endif

#if __MAC_OS_X_VERSION_MIN_REQUIRED
if ( gLinkContext.marzipan ) {
const dyld3::MachOFile* mf = (dyld3::MachOFile*)firstPages;
bool isiOSMacBinary = mf->supportsPlatform(dyld3::Platform::iOSMac) || iOSMacWhiteListed(path);
bool isProhibitedMacOSBinary = !isiOSMacBinary && iOSMacBlackListed(path);
if ( (context.enforceIOSMac && !isiOSMacBinary) || isProhibitedMacOSBinary ) {
throw "mach-o, but not built for iOSMac";
}
}
#endif

#if __arm64e__
if ( (sMainExecutableMachHeader->cpusubtype == CPU_SUBTYPE_ARM64_E) && (mh->cpusubtype != CPU_SUBTYPE_ARM64_E) )
throw "arm64 dylibs cannot be loaded into arm64e processes";
#endif
ImageLoader* image = nullptr;
{
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_MAP_IMAGE, path, 0, 0);
//调用 instantiateFromFile 来实例化 ImageLoader
image = ImageLoaderMachO::instantiateFromFile(path, fd, firstPagesPtr, headerAndLoadCommandsSize, fileOffset, fileLength, stat_buf, gLinkContext);
timer.setData4((uint64_t)image->machHeader());
}

// validate
// 加入全局链表
return checkandAddImage(image, context);
}

// try other file formats here...


// throw error about what was found
switch (*(uint32_t*)firstPages) {
case MH_MAGIC:
case MH_CIGAM:
case MH_MAGIC_64:
case MH_CIGAM_64:
throw "mach-o, but wrong architecture";
default:
throwf("unknown file type, first eight bytes: 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X",
firstPages[0], firstPages[1], firstPages[2], firstPages[3], firstPages[4], firstPages[5], firstPages[6],firstPages[7]);
}
}

这部分的主要任务就是,从本地文件中加载指定的动态库,并且返回ImageLoader实例对象。

步骤如下:

  1. 优先读取这个文件的4k,因为一个分页最小是4k,并且解析为 fat_header。
  2. 如果这个动态库是个fat文件,就直接找到合适CPU架构的“子文件”
  3. 检查文件类型,大小等。
  4. 如果 head 和 Load_Commands 的大小大于已读取的4k,那么就继续把剩下的 headerAndLoadCommandsSize 读完,实例化ImageLoader。因为实例化ImageLoader需要整个 Load_Commands 部分。
  5. checkandAddImage。

再来看 checkandAddImage:

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
static ImageLoader* checkandAddImage(ImageLoader* image, const LoadContext& context)
{
// now sanity check that this loaded image does not have the same install path as any existing image
// 在sAllImages 中检查一下,是否有同样路径的动态库,如果有,则返回链表中原有的那个镜像
const char* loadedImageInstallPath = image->getInstallPath();
if ( image->isDylib() && (loadedImageInstallPath != NULL) && (loadedImageInstallPath[0] == '/') ) {
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
ImageLoader* anImage = *it;
const char* installPath = anImage->getInstallPath();
if ( installPath != NULL) {
if ( strcmp(loadedImageInstallPath, installPath) == 0 ) {
//dyld::log("duplicate(%s) => %p\n", installPath, anImage);
removeImage(image);
ImageLoader::deleteImage(image);
return anImage;
}
}
}
}

// some API's restrict what they can load
if ( context.mustBeBundle && !image->isBundle() )
throw "not a bundle";
if ( context.mustBeDylib && !image->isDylib() )
throw "not a dylib";

// regular main executables cannot be loaded
if ( image->isExecutable() ) {
if ( !context.canBePIE || !image->isPositionIndependentExecutable() )
throw "can't load a main executable";
}

// don't add bundles to global list, they can be loaded but not linked. When linked it will be added to list
if ( ! image->isBundle() )
//加入全局链表中
addImage(image);

return image;
}

代码很简单,判断这个路径的动态库是不是已经在全局链表中了,已经在的话就直接返回,否则就AddImage

返回继续

标题是链接可执行文件,但是因为这是一个递归操作,所以这一节主要是讲述dyld如何链接所有的动态库。

void link(ImageLoader* image, bool forceLazysBound, bool neverUnload, const ImageLoader::RPathChain& loaderRPaths, unsigned cacheIndex)

上面是这个函数的方法签名,在_main的执行过程中,首先是可执行文件的 mach-o 镜像 调用,其次紧跟着的是环境变量中插入的动态库会依次调用,这个函数内部最终还是调用到了 ImageLoader.link函数

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
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();
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();
context.clearAllDepths();
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();
this->recursiveRebase(context);
context.notifyBatch(dyld_image_state_rebased, false);

t3 = mach_absolute_time();
if ( !context.linkingMainExecutable )
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;
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);

//计算时间,控制log输出
fgTotalLoadLibrariesTime += t1 - t0;
fgTotalRebaseTime += t3 - t2;
fgTotalBindTime += t4 - t3;
fgTotalWeakBindTime += t5 - t4;
fgTotalDOF += t7 - t6;

// done with initial dylib loads
fgNextPIEDylibAddress = 0;
}

链接函数的操作思路很清晰:

  1. this->recursiveLoadLibraries 递归加载依赖的所有动态库 (这里仅仅是Load,Load包含open文件以及实例化ImageLoader),最终的结果和Load通过参数插入的动态库一样。
  2. this->recursiveUpdateDepth 递归刷新依赖库的层级
  3. this->recursiveRebase
    每一个动态库的递归rebase无非就是再次依次调用,当前动态库所依赖的其他动态库的recursiveRebase。真正rebase是在rebase(const LinkContext& context, uintptr_t slide)Rebase
  4. this->recursiveBindWithAccounting
    这里就是经典的non-lazy bind和lazy bind了。和Rebase的代码结构类似,真正的rebind是在doBind(const LinkContext& context, bool forceLazysBound)
  5. this->weakBind 弱符号绑定
  6. this->recursiveGetDOFSections 注册DOF节
  7. this->recursiveApplyInterposing

其中每一个步骤都会有一个时间戳,用来根据可控的环境变量来输出log。

Rebase

首先解释一下 Rebase :

都知道,Rebase是调整动态库内部的符号,因为ASLR,内核加载的时候会在随机的地址中映射动态库。一开始我的想法是,mach-o的动态库在编译期已经做了PIC(地址无关代码),为什么还会有”rebase”这个过程?

事实上PIC确实存在,但是rebase也是必须的。

举个例子:在动态库/可执行文件 初始化 的过程中有一个步骤是构造器初始化(这个步骤本文也有具体说明),而实现这个这个步骤的,实际上是在mach-o的数据段中有一个名为__mod_init_func的段,而这个段存储的指针数组就是构造器初始化函数的地址。说到这就比较明显了,在dyld 寻找构造器初始化地址指针的时候,已经不再是以数据段中的vmaddr为基准了,而是需要加上一个偏移 – slide。

下面就按照上面例子,看看代码中是如何实现这一步骤的,compressed mach-o(现代版本的好像几乎都是这个压缩的mach-o格式了) 在这一节上做了压缩,因此实现都在ImageLoaderMachOCompressed中。

先分析一下 rebase 这一节在mach-o内的数据结构

每一个 rebase 信息都由若干个 操作码 – 操作数 来描述,每一个记录都包含两个元素(opcode 和 immediate),可以理解为一个操作码,一个是操作数,这两个都可以通过掩码来获取。

以第一个为例:
REBASE_OPCODE_TYPE_IMM == 11:标识操作数是一个Type,而Type是个指针
REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB == 22:当前rebase的数据在第2个segment,并且偏移是用uleb表示
uleb128 == 24:上面提到的uleb值
REBASE_OPCODE_DO_REBASE_IMM_TIMES == 2:当前rebase操作执行2次。

总结:在第2个段开始往后偏移24个字节的指针地址,连续执行两次rebase。

再看看:

0是起始段,那么第2段就是数据段。我们再找,段的文件偏移 0x3000 + 偏移 24 =0x3018 。

没错,就是这两个构造器,并且当前节就是__mod_init_func~。

下面是rebase的源码,当然在本例中有些 操作码 和 操作数 并没有用到。

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 ImageLoaderMachOCompressed::rebase(const LinkContext& context, uintptr_t slide)
{
CRSetCrashLogMessage2(this->getPath());
// fLinkEditBase 是__LINKEDIT段在 内存实际偏移与预期偏移 的 差值 (== slide?)
// start 的值是 rebase信息在内存中的实际起始地址
const uint8_t* const start = fLinkEditBase + fDyldInfo->rebase_off;
// end 的值是 rebase信息在内存中的实际结束地址
const uint8_t* const end = &start[fDyldInfo->rebase_size];
const uint8_t* p = start;

try {
uint8_t type = 0;
int segmentIndex = 0;
uintptr_t address = segActualLoadAddress(0);
uintptr_t segmentStartAddress = segActualLoadAddress(0);//第N个段地址的实际开始地址
uintptr_t segmentEndAddress = segActualEndAddress(0);//第N个段的实际结束地址
uintptr_t count;
uintptr_t skip;
bool done = false;
while ( !done && (p < end) ) {
uint8_t immediate = *p & REBASE_IMMEDIATE_MASK;
uint8_t opcode = *p & REBASE_OPCODE_MASK;
++p;
switch (opcode) {
case REBASE_OPCODE_DONE:
done = true;
break;
case REBASE_OPCODE_SET_TYPE_IMM:
type = immediate;
break;
case REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB:
segmentIndex = immediate;
if ( segmentIndex >= fSegmentsCount )
dyld::throwf("REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB has segment %d which is too large (0..%d)",
segmentIndex, fSegmentsCount-1);
#if TEXT_RELOC_SUPPORT
if ( !segWriteable(segmentIndex) && !segHasRebaseFixUps(segmentIndex) && !segHasBindFixUps(segmentIndex) )
#else
if ( !segWriteable(segmentIndex) )
#endif
dyld::throwf("REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB has segment %d which is not a writable segment (%s)",
segmentIndex, segName(segmentIndex));
//读取当前段的实际开始和结束地址
segmentStartAddress = segActualLoadAddress(segmentIndex);
segmentEndAddress = segActualEndAddress(segmentIndex);

address = segmentStartAddress + read_uleb128(p, end);
break;
case REBASE_OPCODE_ADD_ADDR_ULEB:
address += read_uleb128(p, end);
break;
case REBASE_OPCODE_ADD_ADDR_IMM_SCALED:
address += immediate*sizeof(uintptr_t);
break;
case REBASE_OPCODE_DO_REBASE_IMM_TIMES:
for (int i=0; i < immediate; ++i) {//循环N次
if ( (address < segmentStartAddress) || (address >= segmentEndAddress) )
throwBadRebaseAddress(address, segmentEndAddress, segmentIndex, start, end, p);
rebaseAt(context, address, slide, type);
address += sizeof(uintptr_t);
}
fgTotalRebaseFixups += immediate;
break;
case REBASE_OPCODE_DO_REBASE_ULEB_TIMES:
count = read_uleb128(p, end);
for (uint32_t i=0; i < count; ++i) {
if ( (address < segmentStartAddress) || (address >= segmentEndAddress) )
throwBadRebaseAddress(address, segmentEndAddress, segmentIndex, start, end, p);
rebaseAt(context, address, slide, type);
address += sizeof(uintptr_t);
}
fgTotalRebaseFixups += count;
break;
case REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB:
if ( (address < segmentStartAddress) || (address >= segmentEndAddress) )
throwBadRebaseAddress(address, segmentEndAddress, segmentIndex, start, end, p);
rebaseAt(context, address, slide, type);
address += read_uleb128(p, end) + sizeof(uintptr_t);
++fgTotalRebaseFixups;
break;
case REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB:
count = read_uleb128(p, end);
skip = read_uleb128(p, end);
for (uint32_t i=0; i < count; ++i) {
if ( (address < segmentStartAddress) || (address >= segmentEndAddress) )
throwBadRebaseAddress(address, segmentEndAddress, segmentIndex, start, end, p);
rebaseAt(context, address, slide, type);
address += skip + sizeof(uintptr_t);
}
fgTotalRebaseFixups += count;
break;
default:
dyld::throwf("bad rebase opcode %d", *(p-1));
}
}
}
catch (const char* msg) {
const char* newMsg = dyld::mkstringf("%s in %s", msg, this->getPath());
free((void*)msg);
throw newMsg;
}
CRSetCrashLogMessage2(NULL);
}

initializeMainExecutable

其实到这里动态库的加载/rebase/bind…都已经完成了,而这一步要做的就是初始化构造器函数了,这里也就是runtime初始化的过程。

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
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]);
}

优先初始化插入的动态库,再初始化可执行文件。

sMainExecutable->runInitializers 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
uint64_t t1 = mach_absolute_time();
mach_port_t thisThread = mach_thread_self();
ImageLoader::UninitedUpwards up;
up.count = 1;
up.images[0] = this;
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized, false);
mach_port_deallocate(mach_task_self(), thisThread);
uint64_t t2 = mach_absolute_time();
fgTotalInitTime += (t2 - t1);
}

processInitializers 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
for (uintptr_t i=0; i < images.count; ++i) {
images.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}

recursiveInitialization 函数:

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
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();
}

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);
}

最终调到 doModInitFunctions 函数,实现逻辑和上文提到的dyld的自身的初始化一样,遍历调用_mod_init_func中的方法指针。

runtime就是在这里被初始化的,调试一下就可以看到:

调用栈和上面描述的一样,最终由 libSystem.B.dylib -> libdispatch.dylib -> libobjc.A.dylib 这个调用顺序初始化了runtime。
而runtime 的这个函数,在dyld反向注册了三个回调函数。
_dyld_objc_notify_register(&map_images, load_images, unmap_image);

而这个注册函数会把每个已经load完的镜像同步地回调给runtime:

从代码中我们也可以确认:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;

// call 'mapped' function with all images mapped so far
try {
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}

// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
ImageLoader* image = *it;
if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
}
}

遍历 sAllImages ,回调给 _dyld_objc_notify_init init函数。runtime就可以根据mach-o的 class 有关的 section 开始初始化了。

参考文献

  1. 内嵌汇编的一些黑科技:访问自身Mach-O、调用函数等
  2. Mach-O脱壳技巧
  3. dyld详解
  4. iOS 程序 main 函数之前发生了什么
  5. dylib动态库加载过程分析