【iOS】源码学习-dyld加载
日常开发中我们默认main函数是程序入口,但实际上+load方法、C++全局构造函数都会优先于main执行。这一切行为都由苹果系统动态连接器dyld全权调度。dyld(The dynamic link editor)是Apple的动态链接器,是Apple操作系统的一个重要组成部分。在应用被编译打包成可执行文件(Mach-O)后,将其交由dyld负责链接,加载程序。dyld贯穿了App启动的过程,包
【iOS】源码学习-dyld加载
前言
日常开发中我们默认main函数是程序入口,但实际上+load方法、C++全局构造函数都会优先于main执行。这一切行为都由苹果系统动态连接器dyld全权调度。
dyld介绍
dyld(The dynamic link editor)是Apple的动态链接器,是Apple操作系统的一个重要组成部分。在应用被编译打包成可执行文件(Mach-O)后,将其交由dyld负责链接,加载程序。dyld贯穿了App启动的过程,包含加载依赖库、主程序。性能优化、启动优化都和dyld紧密相关。
编译器调试时,整个过程分为4个步骤:
- 预处理:处理#开头的预处理指令,替换宏,展开头文件,删除注释,输出中间文件.i
- 编译:对.i文件进行词法、语法和语义分析,执行代码优化,生成汇编代码,输出中间文件.s
- 汇编:将.s汇编文件翻译成机器码,输出目标文件.o
- 链接:将多个.o文件与系统库、框架等一起链接成可执行文件,解决函数/变量引用、地址重定位等,输出最终文件,即可执行程序。在这个过程中,链接器将不同的目标文件链接起来,因为不同的目标文件之间可能有相互引用的变量或调用函数(例如我们常用的Foundation和UIKit框架中的方法和变量,就需要链接器将它们与我们自己的代码链接起来)
像Foundation和UIKit这种可以共享代码、实现代码的复用统称为库,它是可执行代码的二进制文件,可以被操作系统写入内存。其分为静态库和动态库。
- 静态库:是一种将代码编译后封装起来的二进制文件。在程序编译阶段被打包进最终的可执行文件中,运行时不再依赖外部库文件。静态库不是运行时加载一个库文件,而是在构建最终程序时将需要的代码拷贝进去,因此会使最终可执行文件体积变大(例如自己封装的.a静态库,SDWebImage、AFNetworking等第三方库,第三方SDK等)
- 动态库:是指在程序运行时动态加载的代码块,也称为共享库。不会在编译时被打包进可执行文件中,而是以共享形式存在,运行时由操作系统加载。多个程序可以共享一个动态库的实例,系统只需要加载一次动态库,有利于节省内存(系统自带的Foundation、UIKit、CoreAnimation、AVFoundation都是动态库)
其中Framework可以装静态库,也可以装动态库,它们的区别是:- 动态Framework:其组成为Header+.dylib+签名+资源文件。它不会在编译时塞进主程序,而是运行时由dyld动态加载。(Header:对外暴露的头文件;.dylib:动态库二进制文件;签名/资源:打包进Framework里的附属文件)
- 静态Framework:其组成为Header+.a+签名+资源文件。编译链接时,直接把.a里的代码塞进App中可执行文件里,运行时不需要额外加载。(.a:静态库二进制文件)
当我们在Xcode里把动态Framework的Embed选项设置为Embed&Sign后,打包App时,Xcode会把整个.framework文件夹复制到最终App包的Frameworks目录下;运行时,dyld会去这个目录加载它。(这个操作只是改变了它在App包里的位置,不会改变它的类型,它依然是动态库)。
设置Embed&Sign的本质是把自己的动态库拷贝进App包里,给dyld运行时加载用。
这里分两种情况:
- 自己写的/第三方动态库:不设置运行必报错。因为运行时dyld去磁盘里找,没有Embed,在App包里找不到framework,启动失败。
- 系统Framework:不设置也不会报错。因为手机系统自带,dyld去系统路径就能找到。
以上编译过程和动静态库都是dyld工作的前提和基础。
dyld加载流程
App的启动流程如下:

通过打断点,我们发现app启动是从dyld中的__dyld_start中开始的。

先认识两个概念:
- 重定位:程序编译阶段生成的代码、数据中,大量地址都属于未确定的虚拟偏移地址,并非进程运行时的真实内存地址。而重定位就是在装载或者链接阶段,把这些未定死的偏移地址统一修改为当前进程内存空间中真实可用的物理内存地址,让程序能够正常访问函数、全局变量、外部符号等资源。
- 自举:dyld自身同样是Mach-O文件,其内部的全局变量、静态变量、函数调用地址都需要完成重定位才能正常使用,进而出现逻辑矛盾—执行代码需要重定位完成,完成重定位又需要代码执行。为解决该问题,dyld内置了一段无需依赖全局变量、静态变量,也不用调用任何外部函数,仅依靠寄存器完成基础逻辑的特殊启动代码。
完整流程如下:
- 内核创建进程,交给dyld
系统内核接收应用启动指令,创建全新进程并分配隔离虚拟内存空间,完成主程序Mach-O文件基础内存映射,最终将CPU执行控制权正式交给dyld动态链接器。
- dyld自举(bootstrap)
执行__dyld_start汇编入口函数,进入dyld自举阶段。查看源码,发现这个函数了以下事情:
- 调用rebaseDyld完成dyld自身内部地址重定位
- 初始化栈内存保护机制,配置安全哨兵值
- 执行dyld自身C++初始化逻辑
- 读取主程序内存偏移slide值,为后续主程序处理做准备
- 完成自举后,从底层启动阶段过渡至dyld正式业务逻辑,跳转至dyld::_main主函数
- dyld::_main环境配置
进入dyld核心主函数,完成运行环境初始化,包括:
- 读取系统dyld运行环境变量,配置内核启动标识
- 获取当前设备运行架构(真机arm64/模拟器x86_64)
- 读取主程序CDHash校验值、程序本地路径(沙盒路径/安装路径)等核心信息
- 向内核上报dyld与主程序加载状态,即dyld已经完成初始化,开始加载主程序,启动耗时追踪
CDHash:代码签名哈希,用于系统安全检验
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
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)mainExecutableMH, 0, 0);
}
//Check and see if there are any kernel flags
dyld3::BootArgs::setFlags(hexToUInt64(_simple_getenv(apple, "dyld_flags"), nullptr));
// Grab the cdHash of the main executable from the environment
// 配置相关环境操作
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
// apple数组:内核传给dyld的启动参数包
// _simple_getenv(apple, key):apple数组中按key取值,从启动参数包里读取对应的启动标志信息
// 通过_simple_getenv,即读取dyld启动标志、读取主程序的CDHash、读取dyld和主程序对应的文件路径
if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
#if !TARGET_OS_SIMULATOR
// Trace dyld's load
notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
// Trace the main executable's load
notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif
uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;
sMainExecutableSlide = mainExecutableSlide;
本质上就是内核传递给dyld启动参数,dyld读取整理信息,回传给内核。
- 映射系统共享缓存
先检测系统共享缓存开关状态,以及共享缓存是否映射到共享区域,再加载dyld_shared_cache系统内置缓存文件。
共享区域存放苹果所有系统底层代码、框架代码、运行时数据和符号地址表。
这个缓存内置了所有系统核心动态库,包括libobjc(OC运行时)、Foundation基础框架、CoreFoundation核心基础、UIKit/AppKit界面框架、libSystem系统底层库以及所有系统自带framework。
共享缓存的核心特点是系统启动时加载一次,所有App共同映射、共同使用,不重复拷贝、不重复解析、不重复占用内存,只读映射、安全稳定。这样可以大幅减少App冷却时间,极大降低设备整体内存占用。
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache();
#else
mapSharedCache();
#endif
}
- 实例化主程序
调用instantiateFromLoadedImage函数,基于已映射到内存的主程序Mach-O文件,创建ImageLoader管理对象。
// The kernel maps in main executable before dyld gets control. We need to
// make an ImageLoader* for the already mapped in main executable.
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
// 检验Mach-O文件格式合法性(头部是否合法、架构是否匹配、文件类型是否是可执行文件)
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
// instantiateMainExecutable内部读取Mach-O文件头,解析所有Load Command加载指令,完成基础信息校验
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
// 封装主程序内存镜像,纳入dyld统一镜像管理列表
addImage(image);
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}
- 加载插入的动态库(把外来库强行塞进来)
遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib方法批量加载注入动态库,即在主程序运行前,强制插入一个动态库到内存。该机制常用于代码注入、线上调试、底层hook场景等。
// load any inserted libraries // 加载所有需要被插入的动态库
// dyld检查是否设置了DYLD_INSERT_LIBRARIES环境变量,如果有就按顺序加载列表里的所有动态库,插入到当前进程中
// sEnv:dyld全局环境变量结构体
// DYLD_INSERT_LIBRARIES:dyld最著名的环境变量,用来指定要插入进程的动态库路径列表
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
// 该变量不为空时说明要插入第三方dylib
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
- 广度优先递归加载所有依赖动态库(把程序本身要用到的库挨个找出来加载)
dyld采用广度优先遍历算法,逐层解析主程序所有依赖库,即加载程序自身依赖的库。
首先遍历主程序依赖列表,依次加载三方动态库、内嵌动态库,递归加载依赖库自身关联的其他库文件。每加载完成一个库,就将其符号表合并至全局总符号表,统一管理所有外部符号。
- 重定位+符号绑定
完成所有镜像文件加载后,统一执行两大核心链接操作:
- Rebase内部重定位:修正所有镜像内部指针偏移,适配ASLR地址随机化机制。即每次App启动时,系统都会把库随机放到内存不同的位置,因此Rebase就是统一给它们加上slide偏移量,让内部指针重新指向正确位置。
- Bind外部符号绑定:匹配全局符号表,将代码中外部函数、类、变量符号绑定到真实内存地址。同步完成弱符号绑定处理,解决符号重复声明冲突问题。
- 执行初始化
按照先依赖库、后主程序的优先级,批量执行各类初始化方法。
// let objc know we are about to initialize this image
// 准备开始初始化这个镜像,即触发Runtime初始化_objc_init,注册dyld监听回调
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo); // dyld发出状态通知
// initialize this image
// 真正执行初始化方法
// doInitialization内部会做三件事:1.调用load_images执行所有+load方法 2.调用doModInitFunctions执行C++全局构造函数 3.执行attribute((constructor))方法
// load_images:所有类最早执行的方法。在main函数之前调用,用来初始化类相关信息、交换方法、运行时注册、组建启动(埋点、日志、热修复)
// doModInitFunctions:C++全局对象的构造函数。用来初始化C++库、初始化全局单例、底层组件启动
// attribute((constructor)):C语言函数级别的main之前自动执行的方法。用来启动日志、环境初始化、注入逻辑、hook配置
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
// 通知完成所有动态库自定义初始化逻辑,告诉Runtime这个镜像初始化完毕,可以继续下一个
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);
hook钩子:不改原有代码就能修改原有功能、拦截调用、替换执行逻辑。
埋点:自动统计用户点击了什么、看到了什么页面、做了什么操作,用来做数据分析、运营统计。
- 找到main函数,移交控制权
dyld解析Mach-O文件中的LC_MAIN加载指令,精准定位main函数内存地址,结束所有预启动逻辑,将进程执行权限正式移交开发者编写的main函数,App进入常规业务运行阶段。
总结一下:
- dyld是App真正的起点,比main更早,负责把二进制变成可运行程序。
- dyld核心工作为:加载库->修正地址(Rebase/Bind)->初始化(+load/构造)->调用main。
- dyld三大核心功能:共享缓存、符号(函数名、类名、方法名、变量名)绑定、初始化。这决定了App启动的速度与稳定性。
dyld版本进化
dyld的发展历程是从“以空间换时间”到“预计算换时间”,再到“智能化平衡”的性能进化史。
dyld1.0(1996-2004)
dyld1.0包含在NeXTStep3.3中,在此之前的NeXT使用静态二进制数据,所用不是很大。
- 优化:Prebinding预绑定
- 提前给所有动态库分配固定地址
- 运行时直接复用地址,省去重复计算
- 缺点:
- 对C++初始化逻辑支持差,大型C++动态库加载效率极低
- 无完善安全机制,地址管理简陋
- 直接修改二进制文件,安全性差,维护成本低
dyld2.0(2004-2017)
随着macOS Tiger正式推出,dyld2问世,这完全重构dyld1,这也是应用时间最长、最经典的一个版本。
- 核心优化:
- 废弃预绑定,引入共享缓存:系统将常用的系统库(UIKit、Foundation等)合并成一个巨大的文件,即dyld_shared_cache(共享缓存)。该文件在系统更新时生成,包含了优化后的符号表和数据结构。所有App启动时直接映射这个文件,极大减少了内存占用,并加快了速度。
- 安全性提升:引入了ASLR(地址空间布局随机化),每次启动库的地址随机,防止攻击;引入了Code Signing(代码签名),确保库未被篡改。
- C++支持:完美支持C++初始化语义,完善dlopen/dlsym/dlclose动态库调用 API,提升了对C++库的支持效率。
- dlopen:打开/加载一个动态库
- dlsym:在库里找一个函数/符号
- dlclose:关闭。卸载动态库
- dyld2标准启动流程:
- dyld自举初始化,进入dyld::_main核心函数
- 读取内核启动参数、校验CDHash、配置运行环境
- 映射系统共享缓存
- 实例化主程序Mach-O镜像,校验文件合法性
- 加载DYLD_INSERT_LIBRARIES注入动态库
- 广度优先递归加载全部业务依赖库
- 执行Rebase内部重定位和Bind外部符号函数,适配ASLR地址偏移
- 执行初始化(+load方法、C++全局构造函数、constructor函数)
- 解析LC_MAIN入口,移交权限执行main函数
- 缺点:所有操作在主线程串行执行,App启动时同步完成所有解析、链接、符号查找,如果库很多,会明显阻塞App启动。
加载共享缓存:
加载共享缓存是dyld2在macOS和iOS系统中用于加快动态链接的一项优化技术。共享缓存是一种预先生成的动态库集合,包含了多个应用程序常用的动态共享库。当应用程序启动时,因为Foundation会依赖一些其他动态库,这些依赖的其他库还会再依赖更多的库,所以相互依赖的符号会很多,需要处理的时间也会比较长,此时dyld2可以直接从共享缓存中加载所需的动态库,而无需再重新从磁盘上逐个加载动态库,从而加快应用程序的启动速度。
共享缓存的加载过程如下:
- 生成共享缓存:在系统安装或更新时,操作系统会预先生成一个共享缓存,其中包含多个常用的系统动态共享库和框架。这个过程通常在设备首次启动、操作系统升级或开发者重新编译时进行。
- 共享缓存路径:共享缓存被保存在系统文件中,其路径是
/System/Library/Caches/com.apple.dyld/dyld_shared_cache_x86_64(macOS)或/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64(iOS)等,具体路径会根据设备和架构而有所不同。- 应用程序启动:当应用程序启动时,dyld2会首先检查共享缓存是否可用,并尝试加载共享缓存。
- 加载共享缓存:如果共享缓存可用,dyld2会直接将共享缓存映射到内存中,并建立共享缓存中动态库与应用程序之间的链接关系。
- 符号解析和重定位:在加载共享缓存后,dyld2会进行符号解析和重定位,将应用程序中的符号引用与共享缓存中的符号地址进行关联。
- 动态库链接:如果应用程序还依赖其他未包含在共享缓存中的动态共享库,dyld2会根据需要逐个加载这些动态共享库,并进行链接和符号解析。
- 应用程序初始化:一旦所有动态共享库都加载并完成符号解析后,dyld2会开始执行应用程序的main函数,从而正式启动应用程序的执行。
dyld3.0(2017-至今)
dyld3是全新的动态链接器。在iOS13系统中,dyld3带来了可观的性能提升,减少了App的启动时间。
dyld3核心工作流程:
主要包含三部分:
- Out-of-Process(进程外预计算):当App安装、更新或手机重启后,系统会在后台预先解析App的Mach-O文件,计算好所有的依赖关系、符号地址和偏移量,生成一个二进制文件,称为Launch Closure。
- In-Process(进程内执行):App启动时,dyld不再需要解析Mach-O头或查找符号,而是直接读取预先计算好的Launch Closure。
- Launch Closure:启动闭包缓存服务。系统程序的launch直接内置在shared cache中,而第三方App在安装或者更新时生成。这样就能保证launch closure总能在App打开之前准备好。
大多数程序启动时会使用缓存,而不需要调用进程外Mach-O分析器或编译器,并且launch closure比Mach-O更简单,它们是内存映射文件,不需要用复杂的方法进行分析,进而可以提高速度。
- 优点:跳过耗时的符号查找和依赖解析过程,冷启动速度显著提升,使得性能也提升。
- 缺点:如果App或其他依赖库被修改(如热修复、签名变更),预先计算的闭包就会失效,dyld3必须回退到类似dyld2的慢速模式重新生成闭包。
这里值得注意的是:
dyld2默认采取的是lazy symbol的符号加载方式,但在dyld3中,在App启动前,符号解析的结果已经在lauch closure内,因此lazy symbol不再被需要。这时,在dyld2中首次调用缺失符号时App会crash;而dyld3中会导致App一启动就crash。即dyld2懒加载符号,缺符号调用才崩溃;dyld3全量校验符号,缺符号启动就直接崩溃。
dyld4.0(iOS16+全新架构)
随着iOS16和macOS13的发布,dyld4登场,它结合了dyld2的灵活性和dyld3的高性能,旨在解决dyld3在频繁更新场景下的实效问题。
- 核心优化:双模式引擎,dyld4引入了两种加载器,并可以根据场景智能切换。
- PrebuiltLoader(预构建加载器):类似于dyld3的闭包,但更轻量。它优先从dyld_shared_cache或磁盘缓存中加载,只存储必要的元数据。
- JustInTimeLoader(即时加载器):类似于dyld2的实时解析。当预构建加载器失败(如首次安装、热更新)时,dyld4会无缝切换到即时加载模式,解析完成后再生成新的预构建缓存供下次使用。
- 优点:
- 既享受了预计算带来的极速启动,又完美适配了热修复、频繁更新等动态场景。
- PrebuiltLoader只存储元数据,内存占用比dyld3的闭包更低。
总结一下:
dyld1靠固定地址提速,dyld2靠共享缓存兼顾速度与安全,dyld3靠后台预编译极致提速,dyld4实现高速与灵活双向兼顾。dyld2核心优化是共享缓存,dyld3核心优化是启动闭包,dyld4核心优化是双模式加载。
dyld与objc关联
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
//读取影响运行时的环境变量,如果需要,还可以打开环境变量帮助 export OBJC_HELP = 1
environ_init();
//关于线程key的绑定,例如线程数据的析构函数
tls_init();
//运行C++静态构造函数,在dyld调用我们的静态析构函数之前,libc会调用_objc_init(),因此我们必须自己做
static_init();
//runtime运行时环境初始化,里面主要是unattachedCategories、allocatedClasses -- 分类初始化
runtime_init();
//初始化libobjc的异常处理系统
exception_init();
//缓存条件初始化
cache_init();
//启动回调机制,通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib
_imp_implementationWithBlock_init();
/*
_dyld_objc_notify_register -- dyld 注册的地方
- 仅供objc运行时使用
- 注册处理程序,以便在映射、取消映射 和初始化objc镜像文件时使用,dyld将使用包含objc_image_info的镜像文件数组,回调 mapped 函数
map_images:dyld将image镜像文件加载进内存时,会触发该函数
load_images:dyld初始化image会触发该函数
unmap_image:dyld将image移除时会触发该函数
*/
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}

通过看libObjc中的_objc_init方法源码,我们发现_objc_init主要做了一下8件事:
- environ_init:读取Runtime环境变量,包括调试isa、打印load、查看启动耗时。
- tls_init:初始化线程局部存储,管理线程数据。
- static_init:执行系统C++构造函数。
- runtime_init:初始化类表、分类表,为后续类加载做准备。
- exception_init:初始化异常捕获,为崩溃收集提供底层支持。
- cache_init:初始化方法缓存机制。
- _imp_implementationWithBlock_init:提前加载底层跳板函数。
- _dyld_objc_notify_register:注册 dyld 三个核心回调。
这里列举几个环境变量:
- DYLD_PRINT_STATISTICS:打印pre-main启动耗时,做启动优化必备。
- OBJC_PRINT_LOAD_METHODS:打印所有类和分类的+load调用,排查启动冗余。
- OBJC_DISABLE_NONPOINTER_ISA:关闭 isa 优化,查看原始isa结构.
- OBJC_PRINT_IMAGES:打印所有加载的内存镜像image,看库加载情况。
- OBJC_PRINT_INITIALIZE_METHODS:打印+initialize调用时机。
- OBJC_PRINT_REPLACED_METHODS:打印分类替换掉的原方法,排查方法覆盖问题。
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
objc通过该方法向dyld注册监听,让dyld在加载镜像时主动通知Runtime。具体说明这三个回调:
- 先执行map_images:dyld把Mach-O镜像载入内存后触发,用于解析所有类、协议、分类、合并分类方法到主类。
- 再执行load_images:dyld初始化镜像阶段触发,用于执行所有+load方法、C++构造函数、constructor函数。(方法交换、hook、热修复全部在这里执行)
- unmap_image:镜像从内存移除时触发。
镜像:Mach-O文件加载到内存后的完整内存映像。平时编译出来的可执行程序、动态库.dylib、Framework,磁盘上叫Mach-O 文件,被dyld加载进内存之后,统一称作镜像Image。
总结
后续学习方法交换完全依托dyld启动流程,这里先进行一个学习总结,后续再学习笔者将再完善补充博客。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)