应用启动-Main函数之前的那些事儿

MACH-O

上一篇文章中我们介绍了应用启动在objc_init方法执行前的调用堆栈,根据这个堆栈我们可以看出在main函数之前实际上系统内核以及dyld还做了很多的操作,那么这篇文章我们来详细的看一下在这个过程中到底做了哪些事情。

我们在来看下这这张图:

从上图中我们看一看到应用启动的入口实际是_dyld_start函数,我们从XNU源码dyldStartup.s中找到了这个方法:

__dyld_start

1
2
3
4
5
6
__dyld_start:
//..........省略掉汇编代码
// call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
//..........省略掉汇编代码

__dyld_start是一个汇编方法(看不懂😢),不过我们也可以看出这个方法里实际上是调用了dyldbootstrap::start方法,恰好也验证了我们截图中的调用堆栈。

dyldbootstrap::start

dyldbootstrap::start(...), 首先bootstrapping dyld, 然后调用dyld::_main核心方法

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
// appsMachHeader 即mach-o文件的header字段
// argc 即 argument count 即程序运行的参数个数
// argv[] 即 argument value 是一个字符串数组 用来存放指向你的字符串参数的指针数组,每一个元素指向一个参数
// slide 偏移量
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
// 如果slide dyld, 我们必须 fixeup dyly中的内容
if ( slide != 0 ) {
// 重新设定dyld
rebaseDyld(dyldsMachHeader, slide);
}

// allow dyld to use mach messaging
// 允许dyld使用mach消息传递
mach_init();

// kernel sets up env pointer to be just past end of agv array
// 内核设置的env pointers, 也就是环境参数
// envp = environment pointer
// 取出argv的第argc条数据 但是实际上argv 只有argc个参数
// 因此 envp 默认是紧挨着argv存储的
const char** envp = &argv[argc+1];

// kernel sets up apple pointer to be just past end of envp array
// kernel将apple指针设置为刚好超出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
// 在dyld中运行所有C初始化程序
runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif

// now that we are done bootstrapping dyld, call dyld's main
// 调用dyld的main
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

这里附上start方法的重要参数macho_header的结构体:

1
2
3
4
5
6
7
8
9
10
struct mach_header_64 {
uint32_t magic; /* 区分系统架构版本 */
cpu_type_t cputype; /*CPU类型 */
cpu_subtype_t cpusubtype; /* CPU具体类型 */
uint32_t filetype; /* 文件类型 */
uint32_t ncmds; /* loadcommands 条数,即依赖库数量*/
uint32_t sizeofcmds; /* 依赖库大小 */
uint32_t flags; /* 标志位 */
uint32_t reserved; /* 保留字段,暂没有用到*/
};

start方法的主要作用就是:先读取Mach-O文件的头部信息,设置虚拟地址偏移,这里的偏移主要用于重定向。接下来就是初始化Mach-O文件,用于后续加载库文件和DATA数据,再运行C++的初始化器,最后进入dyly的主函数。

dyld::_main

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// dyld的main函数 dyld的入口方法kernel加载dyld并设置设置一些寄存器并调用此函数,之后跳转到__dyld_start
// mainExecutableSlide 主程序的slider,用于做重定向 会在main方法中被赋值
// mainExecutableMH 主程序MachO的header
// argc 表示main函数参数个数
// argv 表示main函数的参数值 argv[argc] 可以获取到参数值
// envp[] 表示以设置好的环境变量
// apple 是从envp开始获取到第一个值为NULL的指针地址
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
uintptr_t result = 0;
// 1 设置运行环境,处理环境变量 mainExecutableMH为macho_header类型
// 表示的是当前主程序的Mach-O头部信息, 有了头部信息, 加载器就可以从头开始, 遍历整个Mach-O文件的信息
sMainExecutableMachHeader = mainExecutableMH;

CRSetCrashLogMessage("dyld: launch started");

// 设置上下文 包括一些回调函数, 参数与标志设置信息
setContext(mainExecutableMH, argc, argv, envp, apple);

// Pickup the pointer to the exec path.
// 获取指向exec路径的指针 执行exec相关指令 apple是一个数组 所以apple表示数组首元素的地址
// _simple_getenv 方法可以理解为从apple中获取"executable_path"对应的值
sExecPath = _simple_getenv(apple, "executable_path");

// <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
if (!sExecPath) sExecPath = apple[0];

// 将可执行文件的路径由相对路径转化成绝对路径
bool ignoreEnvironmentVariables = false;
// 判断是否是相对路径的条件
if ( sExecPath[0] != '/' ) {
// have relative path, use cwd to make absolute
// 相对路径-->绝对路径
char cwdbuff[MAXPATHLEN];
//
if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
// maybe use static buffer to avoid calling malloc so early...
char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
strcpy(s, cwdbuff);
strcat(s, "/");
strcat(s, sExecPath);
sExecPath = s;
}
}
// Remember short name of process for later logging
// 获取可执行文件去除前面的路径, 获取它的name
// strrchr:在参数 sExecPath 所指向的字符串中搜索最后一次出现字符 '/'的位置
sExecShortName = ::strrchr(sExecPath, '/');
// 如果获取到了文件名的位置
if ( sExecShortName != NULL )
// 文件名真正的起始位置
++sExecShortName;
else
// 文件名起始位置就是绝对路径
sExecShortName = sExecPath;

// 配置进程是否受到限制
sProcessIsRestricted = processRestricted(mainExecutableMH, &ignoreEnvironmentVariables, &sProcessRequiresLibraryValidation);
// 如果进程受限
if ( sProcessIsRestricted ) {
#if SUPPORT_LC_DYLD_ENVIRONMENT
// 检查加载命令环境变量
// 遍历Mach-O中所有的LC_DYLD_ENVIRONMENT加载命令, 然后调用processDyldEnvironmentVariable()对不同的环境变量做相应的处理
checkLoadCommandEnvironmentVariables();
#endif
// 删除进程的LD_LIBRARY_PATH与所有以DYLD_开头的环境变量, 这样以后创建的子进程就不包含这些环境变量了
pruneEnvironmentVariables(envp, &apple);
// set again because envp and apple may have changed or moved
// 重新设置链接上下文。这一步执行的主要目的是由于环境变量发生变化了, 需要更新进程的envp与apple参数
setContext(mainExecutableMH, argc, argv, envp, apple);
}
else {
if ( !ignoreEnvironmentVariables )
// 检查环境变量
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
}

// 打印信息 不需要关注
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);

// 获取当前设备的CPU架构信息
getHostInfo(mainExecutableMH, mainExecutableSlide);

// install gdb notifier
// 注册gdb的监听者, 用于调试
stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
stateToHandlers(dyld_image_state_mapped, sSingleHandlers)->push_back(updateAllImages);
// make initial allocations large enough that it is unlikely to need to be re-alloced
sAllImages.reserve(INITIAL_IMAGE_COUNT);
sImageRoots.reserve(16);
sAddImageCallbacks.reserve(4);
sRemoveImageCallbacks.reserve(4);
sImageFilesNeedingTermination.reserve(16);
sImageFilesNeedingDOFUnregistration.reserve(8);


#ifdef WAIT_FOR_SYSTEM_ORDER_HANDSHAKE
// <rdar://problem/6849505> Add gating mechanism to dyld support system order file generation process
WAIT_FOR_SYSTEM_ORDER_HANDSHAKE(dyld::gProcessInfo->systemOrderFlag);
#endif

//2 初始化主程序
try {
// add dyld itself to UUID list
// 将dyld添加到UUIDlist中
addDyldImageToUUIDList();

CRSetCrashLogMessage(sLoadingCrashMessage);
// instantiate ImageLoader for main executable
// 加载sExecPath路径下的可执行文件, 实例化一个ImageLoader对象
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
// 设置上下文, 将MainExecutable 这个 ImageLoader设置给链接上下文, 配置链接上下文其他变量
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.processIsRestricted = sProcessIsRestricted;
gLinkContext.processRequiresLibraryValidation = sProcessRequiresLibraryValidation;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

// load shared cache
// 3 加载共享缓存
checkSharedRegionDisable();
#if DYLD_SHARED_CACHE_SUPPORT
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
// 映射共享缓存
mapSharedCache();
#endif

// Now that shared cache is loaded, setup an versioned dylib overrides
#if SUPPORT_VERSIONED_PATHS
checkVersionedPaths();
#endif

// load any inserted libraries
// 4 加载插入的动态库
// 变量 `DYLD_INSERT_LIBRARIES` 环境变量, 调用`loadInsertedDylib`方法加载所有要插入的库,
// 这些库都被加入到`sAllImages`数组中
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// record count of inserted libraries so that a flat search will look at
// inserted libraries, then main, then others.
// 记录插入的库的数量,以便进行统一搜索插入的库,然后是main,然后是其他
sInsertedDylibCount = sAllImages.size()-1;

// link main executable
// 5 链接主程序
// 开始链接主程序, 此时主程序已经被加载到gLinkContext.mainExecutable中,
// 调用 link 链接主程序。内核调用的是ImageLoader::link 函数。
gLinkContext.linkingMainExecutable = true;
// link方法
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
// 设置永不递归卸载
sMainExecutable->setNeverUnloadRecursive();
// mach-o header中的MH_FORCE_FLAT
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}

// link any inserted libraries
// 6 链接插入的动态库
// 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
// 对 sAllimages (除了主程序的Image外)中的库调用link进行链接,
// 然后调用 registerInterposing 注册符号插入, 例如是libSystem就是此时加入的
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
// link
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
image->setNeverUnloadRecursive();
}
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
// 注册符号插入,Interposition, 是通过编写与函数库同名的函数来取代函数库的行为.
image->registerInterposing();
}
}

// <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
for (int i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image->inSharedCache() )
continue;
image->registerInterposing();
}

// apply interposing to initial set of images
for(int i=0; i < sImageRoots.size(); ++i) {
sImageRoots[i]->applyInterposing(gLinkContext);
}
gLinkContext.linkingMainExecutable = false;

// <rdar://problem/12186933> do weak binding only after all inserted images linked
// 7 执行弱符号绑定
sMainExecutable->weakBind(gLinkContext);

CRSetCrashLogMessage("dyld: launch, running initializers");
#if SUPPORT_OLD_CRT_INITIALIZATION
// Old way is to run initializers via a callback from crt1.o
if ( ! gRunInitializersOldWay )
initializeMainExecutable();
#else
// run all initializers
// 8 执行初始化方法
// 执行初始化方法, 其中`+load` 和constructor方法就是在这里执行,
// `initializeMainExecutable`方法先是内部调用动态库的初始化方法, 然后调用主程序的初始化方法
initializeMainExecutable();
#endif
// find entry point for main executable
// 9 查找APP入口点并返回
result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getMain();
*startGlue = 0;
}
}
catch(const char* message) {
syncAllImages();
halt(message);
}
catch(...) {
dyld::log("dyld: launch failed\n");
}

CRSetCrashLogMessage(NULL);

return result;
}

下面我们对main函数进行拆分讲解

1 设置运行环境,处理环境变量

这一步我们主要关注sExecPath,processRestricted,getHostInfo这几个方法。

sExecPath
1
sExecPath = _simple_getenv(apple, "executable_path");

我们在介绍参数的时候介绍到 apple 实际上存储这应用的环境变量的数组,executable_path就表示执行路径,而_simple_getenv方法就是从apple中获取executable_path对应的值。不过这里获取到的可能是一个相对路径,而dyld判断是否为相对路径的条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ( sExecPath[0] != '/' ) {
// 相对路径
if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
// maybe use static buffer to avoid calling malloc so early...
char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
// 拷贝
strcpy(s, cwdbuff);
// 拼接
strcat(s, "/");
strcat(s, sExecPath);
// 重新赋值
sExecPath = s;
}
}

这样我们就可以获取到执行文件的绝对路径。在获取到绝对路径后,我们可以根据绝对路径获取到执行文件的文件名:

1
sExecShortName = ::strrchr(sExecPath, '/');

strrchr方法的功能为:在参数 sExecPath 所指向的字符串中搜索最后一次出现字符 '/'的位置

processRestricted

进程是否受限,这里我们主要关注下Mach-O相关的一个判断:

1
2
3
4
5
6
7
8
9
10
11
12
// 进程受限
static bool processRestricted(const macho_header* mainExecutableMH, bool* ignoreEnvVars, bool* processRequiresLibraryValidation)
{
// <rdar://problem/13158444&13245742> Respect __RESTRICT,__restrict section for root processes
// 段名受限。当Mach-O包含一个__RESTRICT/__restrict段时,进程会被设置成受限
if ( hasRestrictedSegment(mainExecutableMH) ) {
// existence of __RESTRICT/__restrict section make process restricted
sRestrictedReason = restrictedBySegment;
return true;
}
return false;
}

hasRestrictedSegment方法的实现如下:

1
2
3
4
5
6
7
8
9
//dyld::log("seg name: %s\n", seg->segname);
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}

实际上是从Mach-O文件中依次读取所有的segment,判断segment->segname是否包含__RESTRICT字符串来判断是否受限。

getHostInfo

getHostInfo是用来获取当前设备的CPU架构信息。

我们来简单看下这个方法的实现:

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
static void getHostInfo(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide)
{
#if CPU_SUBTYPES_SUPPORTED
#if __ARM_ARCH_7K__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7K;
#elif __ARM_ARCH_7A__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7;
#elif __ARM_ARCH_6K__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V6;
#elif __ARM_ARCH_7F__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7F;
#elif __ARM_ARCH_7S__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7S;
#else
struct host_basic_info info;
mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT;
mach_port_t hostPort = mach_host_self();
kern_return_t result = host_info(hostPort, HOST_BASIC_INFO, (host_info_t)&info, &count);
if ( result != KERN_SUCCESS )
throw "host_info() failed";
sHostCPU = info.cpu_type;
sHostCPUsubtype = info.cpu_subtype;
mach_port_deallocate(mach_task_self(), hostPort);
#if __x86_64__
#if TARGET_IPHONE_SIMULATOR
sHaswell = false;
#else
sHaswell = (sHostCPUsubtype == CPU_SUBTYPE_X86_64_H);
// <rdar://problem/18528074> x86_64h: Fall back to the x86_64 slice if an app requires GC.
if ( sHaswell ) {
if ( isGCProgram(mainExecutableMH, mainExecutableSlide) ) {
// When running a GC program on a haswell machine, don't use and 'h slices
sHostCPUsubtype = CPU_SUBTYPE_X86_64_ALL;
sHaswell = false;
gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
}
}
#endif
#endif
#endif
#endif
}

设置环境变量完成且获取了CPU信息后,dyld就开始准备初始化主程序了,下面我们看下main函数的下一步初始化主程序。

2 初始化主程序

初始化主程序主要做了两件事:

  • 将dyld添加到UUIDlist中
  • 加载可执行文件 实例化ImageLoader对象

下面我们来详细看下这两步分别都做了什么

addDyldImageToUUIDList

addDyldImageToUUIDList方法是加载DYLD到UUID list中,我们来看下这个方法的实现:

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
// <rdar://problem/10583252> Add dyld to uuidArray to enable symbolication of stackshots
// 将dyld添加到uuidArray以启用符号堆叠
static void addDyldImageToUUIDList()
{
const struct macho_header* mh = (macho_header*)&__dso_handle;
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)((char*)mh + sizeof(macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_UUID: {
uuid_command* uc = (uuid_command*)cmd;
// 新建一个dyld_uuid_info
dyld_uuid_info info;
// 给新建的info imageLoadAddress 字段赋值
info.imageLoadAddress = (mach_header*)mh;
// 复制uc->uuid的16个字节给info.imageUUID
memcpy(info.imageUUID, uc->uuid, 16);
// 利用组装好的info给dyld的gProcessInfo的uuidArray和uuidArrayCount赋值
addNonSharedCacheImageUUID(info);
return;
}
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}

从代码中我们可以看出,这个方法是遍历了Mach-O中的load_command并将cmd->cmd值为LC_UUID的添加到dyld::gProcessInfo->uuidArray中并更新个数。

我们可以通过addNonSharedCacheImageUUID的实现进一步确认:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 将info中的uuidArray添加到dyld::gProcessInfo中
void addNonSharedCacheImageUUID(const dyld_uuid_info& info)
{
// set uuidArray to NULL to denote it is in-use
// 将uuidArray设置为NULL 表示这个字段正在使用中
dyld::gProcessInfo->uuidArray = NULL;

// append all new images
// 追加外部传入的info到sImageUUIDs中
sImageUUIDs.push_back(info);
// 重新设置追加后uuidArrayCount
dyld::gProcessInfo->uuidArrayCount = sImageUUIDs.size();

// set uuidArray back to base address of vector (other process can now read)
// 更新追加后的uuidArray
dyld::gProcessInfo->uuidArray = &sImageUUIDs[0];
}
instantiateFromLoadedImage

从方法名中我们就可以看到这个方法是实例化一个ImageLoader,下面我们来详细了解下这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// mh 即 Mach-O文件的header
// slide 表示偏移量
// path 表示可执行文件地址
static ImageLoader* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
// 检查mach-o的subtype是否是当前cpu可以支持
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
// 根据传入的参数实例化一个ImageLoaderMachO类型的ImageLoader
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
// 将主程序添加到全局主列表sAllImages中,
// 最后调用addMappedRange()申请内存, 更新主程序映像映射的内存区
addImage(image);
return image;
}

throw "main executable not a known format";
}

实例化一个ImageLoaderMachO后,我们将第一步获取到的一些变量设置给我们刚创建的ImageLoader:

1
2
3
4
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.processIsRestricted = sProcessIsRestricted;
gLinkContext.processRequiresLibraryValidation = sProcessRequiresLibraryValidation;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

3 加载共享缓存

何为共享缓存,比如我们都知道iOS开发中会依赖系统的UIKit以及Foundation库,那么iOS系统中安装很多应用每个应用都要有自己独立加载UIKit吗?当然不是,所有的App会共用一份UIKit库,而这份UIKit库就存放在共享缓存中。

这一步我们重点关注:checkSharedRegionDisable,mapSharedCache,checkVersionedPaths这几个方法:

checkSharedRegionDisable
1
2
3
4
static void checkSharedRegionDisable()
{
// iPhoneOS cannot run without shared region
}

这个方法中包含了一些Mac OS的判断不过在方法的最后,系统的注释: iOS如果没有共享库将无法运行。所以这个方法我们也不需要多做解读

mapSharedCache
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
static void mapSharedCache() {
// 快速检查缓存是否已经被加载到共享缓存中了 如果没有返回-1
if ( _shared_region_check_np(&cacheBaseAddress) == 0 ) {
if ( (header->mappingOffset >= 0x48) && (header->slideInfoSize != 0) ) {
// solve for slide by comparing loaded address to address of first region
// 通过比较加载的地址和第一个区域的地址来解决偏移问题
const uint8_t* loadedAddress = (uint8_t*)sSharedCache;
const dyld_cache_mapping_info* const mappings = (dyld_cache_mapping_info*)(loadedAddress+header->mappingOffset);
const uint8_t* preferedLoadAddress = (uint8_t*)(long)(mappings[0].address);
//加载的地址 - 第一个区域的地址
// 更新偏移量
sSharedCacheSlide = loadedAddress - preferedLoadAddress;
dyld::gProcessInfo->sharedCacheSlide = sSharedCacheSlide;
}
// if cache has a uuid, copy it
// 更新UUID
if ( header->mappingOffset >= 0x68 ) {
memcpy(dyld::gProcessInfo->sharedCacheUUID, header->uuid, 16);
}
} else {
if ( (sysctlbyname("kern.safeboot", &safeBootValue, &safeBootValueSize, NULL, 0) == 0) && (safeBootValue != 0) ) {
// 安全模式下
::unlink(MACOSX_DYLD_SHARED_CACHE_DIR DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME);
// 设置sharedRegionMode = kDontUseSharedRegion
gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
return;
} else {
// map in shared cache to shared region
int fd = openSharedCacheFile();
if ( fd != -1 ) {
uint8_t firstPages[8192];
if ( ::read(fd, firstPages, 8192) == 8192 ) {
dyld_cache_header* header = (dyld_cache_header*)firstPages;
for (const dyld_cache_mapping_info* p = fileMappingsStart; p < fileMappingsEnd; ++p, ++i) {
mappings[i].sfm_address = p->address;
mappings[i].sfm_size = p->size;
mappings[i].sfm_file_offset = p->fileOffset;
mappings[i].sfm_max_prot = p->maxProt;
mappings[i].sfm_init_prot = p->initProt;
// rdar://problem/5694507 old update_dyld_shared_cache tool could make a cache file
// that is not page aligned, but otherwise ok.
if ( p->fileOffset+p->size > (uint64_t)(stat_buf.st_size+4095 & (-4096)) ) {
dyld::log("dyld: shared cached file is corrupt: %s" DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME "\n", sSharedCacheDir);
goodCache = false;
}
if ( (mappings[i].sfm_init_prot & (VM_PROT_READ|VM_PROT_WRITE)) == (VM_PROT_READ|VM_PROT_WRITE) ) {
readWriteMappingIndex = i;
}
if ( mappings[i].sfm_init_prot == VM_PROT_READ ) {
readOnlyMappingIndex = i;
}
if ( gLinkContext.verboseMapping ) {
dyld::log("dyld: calling _shared_region_map_and_slide_np() with regions:\n");
for (int i=0; i < mappingCount; ++i) {
dyld::log(" address=0x%08llX, size=0x%08llX, fileOffset=0x%08llX\n", mappings[i].sfm_address, mappings[i].sfm_size, mappings[i].sfm_file_offset);
}
}
if (_shared_region_map_and_slide_np(fd, mappingCount, mappings, codeSignatureMappingIndex, cacheSlide, slideInfo, slideInfoSize) == 0) {
// successfully mapped cache into shared region
sSharedCache = (dyld_cache_header*)mappings[0].sfm_address;
sSharedCacheSlide = cacheSlide;
dyld::gProcessInfo->sharedCacheSlide = cacheSlide;
//dyld::log("sSharedCache=%p sSharedCacheSlide=0x%08lX\n", sSharedCache, sSharedCacheSlide);
// if cache has a uuid, copy it
if ( header->mappingOffset >= 0x68 ) {
memcpy(dyld::gProcessInfo->sharedCacheUUID, header->uuid, 16);
}
}
}
}
}

}

}
}

这一步先通过mapSharedCache()方法来映射共享缓存, 该函数先通过_shared_region_check_np()来检查缓存是否已经映射到了共享区域了, 如果已经映射了, 就更新缓存的slide与UUID, 然后返回;
如果有没有映射 判断系统是否处于安全启动模式(safe-boot mode)下,如果是就删除缓存文件并返回, 如果非安全启动模式, 接下来调用openSharedCacheFile()打开缓存文件, 该函数在sSharedCacheDir路径下, 打开与系统当前cpu架构匹配的缓存文件,也就是/var/db/dyld/dyld_shared_cache_x86_64h, 接着读取缓存文件的前8192字节, 解析缓存头dyld_cache_header的信息, 将解析好的缓存信息存入mappings变量, 最后调用_shared_region_map_and_slide_np()完成真正的映射工作。

checkVersionedPaths
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void checkVersionedPaths()
{
// search DYLD_VERSIONED_LIBRARY_PATH directories for dylibs and check if they are newer
// 读取DYLD_VERSIONED_LIBRARY_PATH环境变量
if ( sEnv.DYLD_VERSIONED_LIBRARY_PATH != NULL ) {
for(const char* const* lp = sEnv.DYLD_VERSIONED_LIBRARY_PATH; *lp != NULL; ++lp) {
// 判断是否需要覆盖当前目录下的库
checkDylibOverridesInDir(*lp);
}
}
// 读取DYLD_VERSIONED_FRAMEWORK_PATH环境变量
// search DYLD_VERSIONED_FRAMEWORK_PATH directories for dylibs and check if they are newer
if ( sEnv.DYLD_VERSIONED_FRAMEWORK_PATH != NULL ) {
for(const char* const* fp = sEnv.DYLD_VERSIONED_FRAMEWORK_PATH; *fp != NULL; ++fp) {
// 判断是否需要覆盖当前目录下的库
checkFrameworkOverridesInDir(*fp);
}
}
}

4、加载插入的动态库

1
2
3
4
5
6
7
// 遍历 `DYLD_INSERT_LIBRARIES` 环境变量, 调用`loadInsertedDylib`方法加载所有要插入的库,
// 这些库都被加入到`sAllImages`数组中
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
// 这里传入的是每个lib的path
loadInsertedDylib(*lib);
}

上面的这段代码主要是:遍历sEnv.DYLD_INSERT_LIBRARIES所有要拆入的库(地址连续所以使用++获取地址),然后调用了loadInsertedDylib方法进行加载插入的库。

下面我们来详细看下这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void loadInsertedDylib(const char* path)
{
// 创建一个imageloader
ImageLoader* image = NULL;
try {
LoadContext context;
context.useSearchPaths = false;
context.useFallbackPaths = false;
context.useLdLibraryPath = false;
context.implicitRPath = false;
context.matchByInstallName = false;
context.dontLoad = false;
context.mustBeBundle = false;
context.mustBeDylib = true;
context.canBePIE = false;
context.origin = NULL; // can't use @loader_path with DYLD_INSERT_LIBRARIES
context.rpath = NULL;
// 根据外部传入的path和新建的context构造一个ImageLoader
image = load(path, context);
}
}

load方法会先调用loadPhase0方法方式从文件加载,而loadPhase0又会调用loadPhase1loadPhase2去加载,实际上调用层次没加一层都是在对应load方法的path参数后拼接了一层,是不断的完善path路径的过程:

加载拆入的库后,还需要更新sInsertedDylibCount:

1
sInsertedDylibCount = sAllImages.size()-1;

这里的-1操作实际上是排除主程序之外

5、链接主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
// 开始链接主程序, 此时主程序已经被加载到gLinkContext.mainExecutable中,
// 调用 link 链接主程序。内核调用的是ImageLoader::link 函数。
gLinkContext.linkingMainExecutable = true;
// link方法
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
// 设置永不递归卸载
sMainExecutable->setNeverUnloadRecursive();
// mach-o header中的MH_FORCE_FLAT
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}

这一步就是将加载进来的二进制变为可用状态的过程:rebase => binding

rebase就是针对 “mach-o在加载到内存中不是固定的首地址” 这一现象做数据修正的过程。
binding就是将这个二进制调用的外部符号进行绑定的过程。
lazyBinding就是在加载动态库的时候不会立即binding, 当时当第一次调用这个方法的时候再实施binding。

例如我们objc代码中需要使用到NSObject, 即符号_OBJC_CLASS_$_NSObject,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要binding这个操作将对应关系绑定到一起。

这一步我们主要是看link方法(简化版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths)
{
// 递归加载库
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths);
context.notifyBatch(dyld_image_state_dependents_mapped);
// 递归rebase
this->recursiveRebase(context);
context.notifyBatch(dyld_image_state_rebased);
// 递归bind
this->recursiveBind(context, forceLazysBound, neverUnload);

if ( !context.linkingMainExecutable )
// weakBind
this->weakBind(context);

context.notifyBatch(dyld_image_state_bound);

std::vector<DOFInfo> dofs;
// 递归获取DOFSection
this->recursiveGetDOFSections(context, dofs);
context.registerDOFs(dofs);
}

经过link操作后主程序达到了一个可用的状态。

6、链接插入的动态库

在链接主程序后链接插入的动态库,因此所有插入的动态库都会在系统使用的动态库后面。

与链接主程序相同,拆入的动态库也是通过调用link方法进行链接:

1
2
3
4
5
6
7
8
// sInsertedDylibCount 插入动态库的个数
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));
//
image->setNeverUnloadRecursive();
}

sInsertedDylibCount表示前期通过调用addImage方法插入到sAllImages的动态库的个数,遍历每一个拆入的动态库注意:这里sAllImages的下标是从1开始的,因为第0个位置存放的是主程序。

registerInterposing
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
void ImageLoaderMachO::registerInterposing()
{
// mach-o files advertise interposing by having a __DATA __interpose section
// 这个方法是要操作 Mach-O文件的__DATA__区
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) {
switch (cmd->cmd) {
// 找到load_commands中的LC_SEGMENT_COMMAND
case LC_SEGMENT_COMMAND:
{

for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
// 查找__DATA段的__interpose节区
if ( ((sect->flags & SECTION_TYPE) == S_INTERPOSING) || ((strcmp(sect->sectname, "__interpose") == 0) && (strcmp(seg->segname, "__DATA") == 0)) ) {

for (size_t i=0; i < count; ++i) {

// 找到需要应用插入操作(也可以叫作符号地址替换)的数据
if ( this->containsAddress((void*)tuple.replacement) ) {

// 将要替换的符号与被替换的符号信息存入fgInterposingTuples列表中, 供以后具体符号替换时查询
ImageLoader::fgInterposingTuples.push_back(tuple);
}
}
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}

registerInterposing()查找__DATA段的__interpose节区, 找到需要应用插入操作(也可以叫作符号地址替换)的数据, 然后做一些检查后, 将要替换的符号与被替换的符号信息存入fgInterposingTuples列表中, 供以后具体符号替换时查询(applyInterposing中会用到)。

applyInterposing

applyInterposing() -> recursiveApplyInterposing() -> doInterpose() -> eachBind() -> interposeAt()

下面看下interposeAt方法:

1
2
3
4
5
6
7
8
9
10
11
12
uintptr_t ImageLoaderMachOCompressed::interposeAt(const LinkContext& context, uintptr_t addr, uint8_t type, const char*, 
uint8_t, intptr_t, long, const char*, LastLookup*, bool runResolver)
{
if ( type == BIND_TYPE_POINTER ) {
uintptr_t* fixupLocation = (uintptr_t*)addr;
uintptr_t curValue = *fixupLocation;
uintptr_t newValue = interposedAddress(context, curValue, this);
if ( newValue != curValue)
*fixupLocation = newValue;
}
return 0;
}

这个方法的实现很简单就是对比了新值和旧值 如果不同就将对应地址的值改为新值。

7 执行弱符号绑定

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

void ImageLoader::weakBind(const LinkContext& context)
{
ImageLoader* imagesNeedingCoalescing[fgImagesRequiringCoalescing];
// 将sAllImages中所有含有弱符号的映像合并成一个列表
int count = context.getCoalescedImages(imagesNeedingCoalescing);
// don't need to do any coalescing if only one image has overrides, or all have already been done
// 如果进行weakbind的镜像个数>0
if ( (countOfImagesWithWeakDefinitionsNotInSharedCache > 0) && (countNotYetWeakBound > 0) ) {
// make symbol iterators for each
ImageLoader::CoalIterator iterators[count];
ImageLoader::CoalIterator* sortedIts[count];
for(int i=0; i < count; ++i) {
// 对镜像进行排序
imagesNeedingCoalescing[i]->initializeCoalIterator(iterators[i], i);
sortedIts[i] = &iterators[i];
}

int doneCount = 0;
while ( doneCount != count ) {
// 收集需要进行绑定的弱符号
// 该函数读取映像动态链接信息的weak_bind_off与weak_bind_size来确定弱符号的数据偏移与大小,然后挨个计算它们的地址信息
if ( sortedIts[0]->image->incrementCoalIterator(*sortedIts[0]) )
++doneCount;
// process all matching symbols just before incrementing the lowest one that matches
if ( sortedIts[0]->symbolMatches && !sortedIts[0]->done ) {

ImageLoader* targetImage = NULL;
for(int i=0; i < count; ++i) {
if ( strcmp(iterators[i].symbolName, nameToCoalesce) == 0 ) {
if ( iterators[i].weakSymbol ) {
if ( targetAddr == 0 ) {
// 按照映像的加载顺序在导出表中查找符号的地址
targetAddr = iterators[i].image->getAddressCoalIterator(iterators[i], context);
if ( targetAddr != 0 )
targetImage = iterators[i].image;
}
}
else {
targetAddr = iterators[i].image->getAddressCoalIterator(iterators[i], context);
if ( targetAddr != 0 ) {
targetImage = iterators[i].image;
// strong implementation found, stop searching
break;
}
}
}
}

// tell each to bind to this symbol (unless already bound)
if ( targetAddr != 0 ) {
for(int i=0; i < count; ++i) {
if ( strcmp(iterators[i].symbolName, nameToCoalesce) == 0 ) {
// 绑定操作
// 内部执行绑定的是bindLocation()
iterators[i].image->updateUsesCoalIterator(iterators[i], targetAddr, targetImage, context);
}
}

}
}
}

8、执行初始化方法

执行初始化方法, 其中+load 和constructor方法就是在这里执行。

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
// initializeMainExecutable 执行初始化方法,其中 +load 和 constructor 方法就是在这里执行。
// initializeMainExecutable 内部先调用了动态库的初始化方法,后调用主程序的初始化方法。
// 初始化主程序
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;

// run initialzers for any inserted dylibs
// 给被插入的所有的 dylibs 进行初始化 -- 调用 initialzers
ImageLoader::InitializerTimingList initializerTimes[sAllImages.size()];

initializerTimes[0].count = 0;

const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
// 这里是下标1开始 排除掉了主程序的初始化
for(size_t i=1; i < rootCount; ++i) {
// 执行镜像的初始化方法
// 从 sImageRoots 中的第一个变量是 MainExcutable image,
// 因此这里初始化的时候需要跳过第一个数据, 对其他后面插入的dylib进行调用
// ImageLoader::runInitializers进行初始化
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}

// run initializers for main executable and everything it brings up
// 调用主程序的初始化方法
// 单独对 main executable调用ImageLoader::runInitializers进行初始化
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 )
ImageLoaderMachO::printStatistics((unsigned int)sAllImages.size(), initializerTimes[0]);
}

这个方法主要是执行了ImageLoader的runInitializers方法,下面我看下这个方法的实现:

runInitializers
1
2
3
4
5
6
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
// 初始化当前 imageLoader 中的 image镜像的实际调用方法 ImageLoader::processInitializers
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized);
}
processInitializers
1
2
3
4
5
6
7
8
9
10
11
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
// 处理当前image依赖 dylib动态库, 调用 recursiveInitialization 方法!!!
for (uintptr_t i=0; i < images.count; ++i) {
images.images[i]->recursiveInitialization(context, thisThread, 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
60
61
62
63
64
65
66
67
68
69
70
// 递归调用 image 进行初始化, 先调用image依赖的image进行初始化. 直到自己
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
// 当前ImageLoader依赖的Image还没有初始化完, 进入if中, 如果执行完成, 直接返回
if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles
// break cycles -> 这是设置当前imageLoader的state接近依赖初始化.
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
// 首先初始化image底层的依赖库
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, timingInfo, uninitUps);
}
}
}

// 到这里image底层的依赖库都递归调用, 初始化完成.

// let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
// 通知 runtime, 当前状态发生变化 -- image的依赖已经完全加载.
// 注意这里可能在runtime中注册了状态监听, 注册了callback函数, 当状态发送变化时,
// 会触发回调函数.
context.notifySingle(dyld_image_state_dependents_initialized, this);

// initialize this image
// 初始化当前image, `ImageLoaderMachO::doInitialization`方法内部会调用image
// 的"Initializer", 这是一个函数指针, 实际是image的初始化方法. 例如
// `libSystem.dylib`, 它的初始化方法就比较特殊, 我们可以参考libSystem的init.c源
// 码, 内部的`libsystem_initializer`函数就是初始化真正调用的函数

// _init_objc方法!!!!
bool hasInitializers = this->doInitialization(context);

// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
//通知runtime, 档期那状态发送变化 -- image自己已经完成初始化!!!!
context.notifySingle(dyld_image_state_initialized, this);

if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.images[timingInfo.count].image = this;
timingInfo.images[timingInfo.count].initTime = (t2-t1);
timingInfo.count++;
}

}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}
}
doInitialization
1
2
3
4
5
6
7
8
9
10
11
12
13
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());

// mach-o has -init and static initializers
// 调用Mach-O的 init 和 static initializers方法
doImageInit(context);
doModInitFunctions(context);

CRSetCrashLogMessage2(NULL);

return (fHasDashInit || fHasInitializers);
}
doImageInit

获取mach-o的init方法的地址并调用

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
void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
if ( fHasDashInit ) {
// mach-o文件中指令的个数
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) {
switch (cmd->cmd) {
case LC_ROUTINES_COMMAND:
// 获取macho_routines_command的init_address
Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
// <rdar://problem/8543820&9228031> verify initializers are in image
if ( ! this->containsAddress((void*)func) ) {
dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
}
if ( context.verboseInit )
dyld::log("dyld: calling -init function %p in %s\n", func, this->getPath());
// 执行-init方法
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
break;
}
// 计算下一个指令((char*)cmd)+cmd->cmdsize
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
doModInitFunctions

获取mach-o的static initializer的地址并调用

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
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
if ( fHasInitializers ) {
// mach-o文件中指令的个数
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) {
// 如果指令是Mach-o中的LC_SEGMENT_COMMAND
if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
// 从sectionsStart到sectionsEnd遍历所有的macho_section
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
//
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
Initializer* inits = (Initializer*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);
for (size_t i=0; i < count; ++i) {
// 获取到Initializer方法
Initializer func = inits[i];
// <rdar://problem/8543820&9228031> verify initializers are in image
if ( ! this->containsAddress((void*)func) ) {
dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
}
if ( context.verboseInit )
dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
// 执行initializer方法
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
}
}
}
// 根据指令的地址+指令大小获取到下一个指令
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}

9、查找APP入口点并返回

这一步也是最后一步主要功能为:
查找到main函数的地址,并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 9 查找APP入口点并返回
result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getMain();
*startGlue = 0;
}
getThreadPC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 查找主程序的LC_MAIN加载命令获取程序的入口点,
void* ImageLoaderMachO::getThreadPC() 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;
}

该方法遍历了Load Commands 找到LC_MAIN命令的入口点地址返回,这个地址就是main函数的地址

getMain

如果getThreadPC没有找到LC_MAIN的入口地址

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
// 在LC_UNIXTHREAD加载命令中去找, 找到后就跳到入口点指定的地址
void* ImageLoaderMachO::getMain() 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) {
switch (cmd->cmd) {
case LC_UNIXTHREAD:
{
#if __i386__
const i386_thread_state_t* registers = (i386_thread_state_t*)(((char*)cmd) + 16);
void* entry = (void*)(registers->eip + fSlide);
#elif __x86_64__
const x86_thread_state64_t* registers = (x86_thread_state64_t*)(((char*)cmd) + 16);
void* entry = (void*)(registers->rip + fSlide);
#elif __arm__
const arm_thread_state_t* registers = (arm_thread_state_t*)(((char*)cmd) + 16);
void* entry = (void*)(registers->__pc + fSlide);
#elif __arm64__
const arm_thread_state64_t* registers = (arm_thread_state64_t*)(((char*)cmd) + 16);
void* entry = (void*)(registers->__pc + fSlide);
#else
#warning need processor specific code
#endif
// <rdar://problem/8543820&9228031> verify entry point is in image
if ( this->containsAddress(entry) ) {
return entry;
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
throw "no valid entry point";
}

objc_init

从上面的步骤描述中我们知道实际上objc_init是在ImageLoaderMachO::doModInitFunctions时就被调用了,我们先来看代码

doModInitFunctions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// doModInitFunctions方法部分代码
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
for (size_t i=0; i < count; ++i) {
// 获取到Initializer方法
Initializer func = inits[i];
// <rdar://problem/8543820&9228031> verify initializers are in image
if ( ! this->containsAddress((void*)func) ) {
dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
}
if ( context.verboseInit )
dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
// 执行initializer方法
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
}

doModInitFunctions方法实际上调用了Initializer方法,对对一个动态库进行初始化是通过_libdispatch_init方法进行的 我们来看下这个方法:

_libdispatch_init

1
2
3
4
5
6
7
8
9
10
11
void
libdispatch_init(void)
{

_dispatch_hw_config_init();
_dispatch_time_init();
_dispatch_vtable_init();
_os_object_init();
_voucher_init();
_dispatch_introspection_init();
}

上面的方法我们看到,其中调用了_os_object_init方法,然后我们在看下这个方法的实现:

_os_object_init

1
2
3
4
5
6
7
void
_os_object_init(void)
{
// 省略....
_objc_init();
// 省略....
}

从上面的代码中我们看到,在_os_object_init方法中调用了我们熟知的_objc_init方法,至此运行时就开始。

_objc_init

1
2
3
4
5
6
7
8
9
10
11
12
13
void _objc_init(void)
{
// 省略代码...
/*
仅供objc运行时使用,注册在映射、取消映射和初始化objc映像调用的处理程序。dyld将使用包含objc-image-info回调给`mapped`.
这些dylibs将自动引用计数,因此objc将不再需要调用dlopen()防止未加载。
在调用_dyld_objc_notify_register()期间,dyld将调用 `mapped` 在已经加载好 images,稍后dlopen()。
在调动init的时候也会调用`mapped`,在dyld调用的时候,也会调用init函数

在调用任何images +load方法时候
*/
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

这个方法中,主要是注册了map_imagesload_images方法.

map_images

1
2
3
4
5
6
7
8
9
void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}

}

load_images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void
load_images(const char *path __unused, const struct mach_header *mh)
{

// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
//加载 class+load 和category+load方法
prepare_load_methods((const headerType *)mh);
}

// Call +load methods (without runtimeLock - re-entrant)
//执行 class+load 和category+load方法
call_load_methods();
}

总结

总结上述的整个过程便是在主工程的main函数开始执行之前系统做的所有操作,其中有一些步骤和环境我也不是太清楚,因此需要进一步的完善和梳理。

参考文章

libdispatch源码
dyld与ObjC
iOS App启动时发生了什么?
dyld加载应用启动原理详解