/usr/lib/libSystem.B.dylib
dyld简介
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。而且它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节。
dyld下载地址:http://opensource.apple.com/tarballs/dyld。笔者下载的是519.2.2版本。
共享缓存机制
在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而,很多系统库几乎是每个程序都会用到的,如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存分别保存着,笔者的iPhone6里面就有dyld_shared_cache_armv7s和dyld_shared_cache_armv64两个文件,如下图所示。

想要分析某个系统库,就需要从dyld_shared_cache里先将的原始二进制文件提取出来,这里从易到难提供3种方法:
1. dyld_cache_extract提取
dyld_cache_extract(https://github.com/macmade/dyld_cache_extract)是一个可视化的工具,使用极其简单,把dyld_shared_cache载入即可解析出来,如下图所示。

2. jtool提取
以提取CFNetwork为例,使用如下命令即可:
$ jtool -extract CFNetwork ./dyld_shared_cache_arm64
Extracting /System/Library/Frameworks/CFNetwork.framework/CFNetwork at 0x147a000 into dyld_shared_cache_arm64.CFNetwork
3. dsc_extractor提取
在dyld源代码的launch-cache文件夹里面找到dsc_extractor.cpp,将653行的“#if 0”修改为“#if 1”,然后用如下命令编译生成dsc_extractor,并使用它提取所有缓存文件:
$ clang++ dsc_extractor.cpp dsc_iterator.cpp -o dsc_extractor
$ ./dsc_extractor ./dyld_shared_cache_arm64 ./
dyld加载过程
一个iOS程序的main()函数位于main.m中,这是我们熟知的程序入口。但很少有人去关心main()函数之前到底发生了什么。本章就带着这个疑问,从main()函数入手,探索一下dyld的加载过程。
先用Xcode新建一个Single View App工程,并在main()函数下断,然后运行,调用栈如下图所示。

main()函数之前仅有一个libdyld.dylib`start入口,这显然不是我们想要的,根据这个线索顺藤摸瓜,在dyld源代码dyldStartup.s中找到了__dyld_start函数,此函数由汇编实现,兼容各种平台架构,此处仅摘录arm64架构下的汇编代码片段:
#if __arm64__
.data
.align 3
__dso_static:
.quad ___dso_handle
.text
.align 2
.globl __dyld_start
__dyld_start:
mov x28, sp
and sp, x28, #~15 // force 16-byte alignment of stack
mov x0, #0
mov x1, #0
stp x1, x0, [sp, #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp, #16 // make room for local variables
ldr x0, [x28] // get app's mh into x0
ldr x1, [x28, #8] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add x2, x28, #16 // get argv into x2
adrp x4,___dso_handle@page
add x4,x4,___dso_handle@pageoff // get dyld's mh in to x4
adrp x3,__dso_static@page
ldr x3,[x3,__dso_static@pageoff] // get unslid start of dyld
sub x3,x4,x3 // x3 now has slide of dyld
mov x5,sp // x5 has &startGlue
// call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
mov x16,x0 // save entry point address in x16
ldr x1, [sp]
cmp x1, #0
b.ne Lnew
// LC_UNIXTHREAD way, clean up stack and jump to result
add sp, x28, #8 // restore unaligned stack pointer without app mh
br x16 // jump to the program's entry point
// LC_MAIN case, set up stack for call to main()
Lnew: mov lr, x1 // simulate return address into _start in libdyld.dylib
ldr x0, [x28, #8] // main param1 = argc
add x1, x28, #16 // main param2 = argv
add x2, x1, x0, lsl #3
add x2, x2, #8 // main param3 = &env[0]
mov x3, x2
Lapple: ldr x4, [x3]
add x3, x3, #8
cmp x4, #0
b.ne Lapple // main param4 = apple
br x16
#endif // __arm64__
源码中可以看到一条bl命令,根据注释可以知道是跳转到dyldbootstrap::start()函数:
// call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
dyldbootstrap::start()函数中做了很多dyld初始化相关的工作,包括:
- rebaseDyld() dyld重定位。
- mach_init() mach消息初始化。
- __guard_setup() 栈溢出保护。
初始化工作完成后,此函数调用到了dyld::_main(),再将返回值传递给__dyld_start去调用真正的main()函数。在dyldInitialization.cpp文件中可以找到dyldbootstrap::start()函数的实现如下:
//
// This is code to bootstrap dyld. This work in normally done for a program by dyld and crt.
// In dyld we have to do this manually.
//
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
if ( slide != 0 ) {
// 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);
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
// 进入dyld::_main()函数
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
dyld::_main()是整个App启动的关键函数,此函数里面做了很多事情,代码如下:
//
// Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
// Grab the cdHash of the main executable from the environment
// 第一步,设置运行环境
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
// 获取主程序的hash
mainExecutableCDHash = mainExecutableCDHashBuffer;
// Trace dyld's load
notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
#if !TARGET_IPHONE_SIMULATOR
// Trace the main executable's load
notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif
uintptr_t result = 0;
// 获取主程序的macho_header结构
sMainExecutableMachHeader = mainExecutableMH;
// 获取主程序的slide值
sMainExecutableSlide = mainExecutableSlide;
CRSetCrashLogMessage("dyld: launch started");
// 设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
// Pickup the pointer to the exec path.
// 获取主程序路径
sExecPath = _simple_getenv(apple, "executable_path");
// <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
if (!sExecPath