【iOS】dyld加载

dyld加载流程

编译过程与动静态库

我们载编译器上点击按钮进行开发调试,其中经历了四个步骤:预处理、编译、汇编和链接。

  • 预处理:处理#开头的预处理指令,替换展开头文件、删除注释,输出中间文件.i
  • 编译:对.i文件进行词法、语法和语义的分析,执行代码优化,生成汇编代码,输出中间文件.s

词法分析:将源代码的字符序列分割成一个个的token(关键字、标识符、字面量、特殊符号),例如将标识符放到符号表中

语法分析:生成抽象语法树AST,此时运算符号的优先级确定,一些符号的多重语义也确定了。如果出现语法错误就会报错

静态分析:分析类型声明和匹配问题。

中间语法生成:LLVM编译器先生成一种中间语言LLVM IR,可以在编译期就做一些表达式的优化

目标代码生成与优化:将IR语法转换为机器依赖的汇编语言,并优化,如果这个过程中有变量切定义在同一个编译单元中就会给这个变量分配空间,确定变量地址。如果不在就等链接时确定。

  • 汇编:将.s汇编文件翻译成机器码,输出目标文件.o
  • 链接:将多个.o文件与系统库、框架等一起链接成可执行文件

链接

链接的主要内容就是将各个模块之间的相互引用部分处理好,使得各个模块之间能够正确衔接。链接过程包括了地址和空间的分配、符号决议和重定位,链接器将经过汇编器编译成的所有目标文件和库进行链接成最终的可执行问津啊,而最常见的就是运行时库。所以库说白了就是一组常用代码编译成目标文件再经过链接后的存放

装载

可执行文件是一个静态的概念,运行之前只是硬盘上的一个文件,而进程是一个运行时的一个过程,每个程序运行起来后,都会拥有自己的独立的虚拟地址空间,这个地址空间的大小是由计算机的硬件决定的。但是程序运行在系统上是不可能任意使用全部的虚拟空间的,操作系统为了实现监控程序运行等目的,进程的虚拟空间都会被操作系统控制。并且进程的虚拟地址空间是彼此隔离的,如果出现越界访问,会强制结束进程。将硬盘上的可执行文件映射到虚拟内存的过程就是重载。由于内存的稀有,研究发现程序运行时是有局部性原理的,可以只将常用的部分驻留在内存中,不太常用的数据放在磁盘里,即动态装载的基本原理,覆盖载入和页映射就是两种经典的动态装载方法。

这个过程中,操作系统主要做三件事

  • 创建一个独立的虚拟地址空间
  • 读取可执行文件头,并建立虚拟空间与可执行文件的映射关系
  • 将CPU的指令寄存器设置为可执行文件的入口地址,并启动执行

静态库和动态库

  • 静态库:静态库会在链接阶段,被链接器从库文件中取出当前程序实际用到的目标代码,并拷贝到最终生成的可执行文件中。所以一旦链接完成,运行时就不再依赖这个静态库文件本身。静态库通常以.a或lib以及MacOS独有的.framework为扩展名
    • 优点:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖,直接就可以运行
    • 缺点:由于静态库会有两份,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大

静态库本质上不是运行时加载的独立模块,而更像是一个目标文件集合包。静态库不是运行时加载一个库文件,而是在构建最终程序时,将需要的代码拷贝进去。

静态库中的可执行代码会被链接到可执行文件的代码段中,与主程序的代码一起存储在这个段中。

静态库中的初始化数据会被链接到可执行文件的数据段中。

静态库中的未初始化数据会被链接到 BSS 段中。

  • 动态库:程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才会被载入,常见格式有:.framework 或 .dylib以及.tbd等
    • 优点:减小包体积。共享内存,节约资源,同一份库可以被多个程序使用。可以通过更新动态库,达到更新程序的目的(依赖于运行时载入的特性,可以随时对库进行替换,而不需要重新编译代码)
    • 缺点:动态载入会带来一部分的性能损失,会使得应用程序依赖于外部环境,如果环境缺失了动态库,或者是版本不正确,会导致程序无法正常运行。

上面说framework格式动态/静态库都有,那他们有啥区别呢?

进入对应路径之后,使用file文件输出信息区分。

体积小于最小单位16块的静态库编译踹出来的动态库统计等于16块

虽然动态库会导致一些速度降低,但是会通过延迟绑定(lazy binding)技术优化

dyld加载流程分析

重定位:将代码和数据中那些还没有最终定死的地址在装载或链接时修改成当前进程中真正可用的地址

  • 动态链接器本身不依赖于其他任何对象
  • 动态链接器本身所需要的全局和静态变量的重定位工作由它本身完成

第二个条件要求动态链接器在启动的时候必须有一段代码可以在获得自身的重定位表和符号表的同时有不能用到全局和静态变量,甚至不能调用函数,这样的启动代码被称为自举。当操作系统将进程控制权交给动态链接器时,自举代码开始执行,他会寻找到自身的重定位入口,进而完成自身的重定位。自举之后动态链接器将可执行文件和自身符号表合并,即全局符号表。之后链接器开始寻找可执行文件所依赖的共享对象。常见使用一个广度优先算法。每当一个新的共享对象被装载进来之后,其符号表就会呗合并进入全局符号表中,装载完成之后,链接器重新遍历可执行文件和共享对象的重定位表,修正位置(类似于静态链接)。重定位完成之后动态链接器开始共享对象初始化(进行可执行文件初始化,由代码负责)。完成重定位与初始化之后,动态链接器就会将进程的控制权归还给程序入口并执行。

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的重要组成部分,在app被编译时打包成可执行文件格式的Mach-O文件后,交由dyld负责链接,加载程序。

dyld::_main源码分析

在源码中查找__dyld_start,可以发现其中调用了一个start方法

我们查找到这个方法如下:

这个函数做的事:

rebaseDyld(dyldsMachHeader);dyld的自举,先修正内部的定位

初始化栈保护值,实现一个栈保护哨兵,一个随机信息

运行dyld自己的C++初始化器

获取主程序的slide,为后续处理主程序做铺垫

处理完上述步骤后,进入dyld真正的主逻辑。有bootstrap过渡阶段进入真正的动态装载逻辑

接下来我们了解一下_main函数流程

  • 【环境变量配置】根据环境变量设置相应的值以及获取当前运行的架构
objc 复制代码
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;
	if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
    //通过_simple_getenv(apple, "...")从apple[]中按key取值:读取dyld启动标志、读取主程序的cdHash、读取dyld和主程序对应的文件路径,用户通知内核/追踪
		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;
  • 【共享缓存】检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如UIKit、CoreFoundation等
objc 复制代码
// load shared cache
	checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
	if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
		if ( sSharedCacheOverrideDir)
			mapSharedCache();
#else
		mapSharedCache();
#endif
	}//libSystem、Foundation、UIKit/AppKit、libobjc、其他系统framework。操作系统在访问这些系统库数据时,不会让每个进程单独去磁盘上执行查找解析等操作,而是提前将这些常用系统库整理成一个大的共享文件。
  • 【主程序初始化】调用instantiateFromLoadedImage函数实例化一个ImageLoader对象
objc 复制代码
//主程序的Mach-O已经被内核提前映射到内存,dyld现在创建一个ImageLoader对象管理它(将裸的Mach-O映像包装成dyld内部可管理的对象)
// 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
	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";
}
  • 【插入动态库】遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInserted
objc 复制代码
// load any inserted libraries
		if	( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
			for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
				loadInsertedDylib(*lib);
		}//修改这个配置可以让自己写的库被加载
  • 【重定位完成所有需要重定位的库,然后初始化主程序】
objc 复制代码
// 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);

经过一系列初始化函数调用之后,到达notifySingle函数。其回调函数是load_images函数,这个函数中执行call_load_methods函数,其中循环调用各个类的load方法。

然后调用doModInitFunctions函数,内部调用全局C++对象构造函数。最后返回主程序的入口函数,

dyld历史

  • dyld(The Dynamic Link Editor)是苹果操作系统(iOS, macOS, watchOS, tvOS)中至关重要的动态链接器 。它的核心职责是在应用程序启动时,负责加载和链接程序所依赖的动态库(如 UIKit, Foundation 等),并进行符号绑定、重定位等操作,最终将控制权交给 main 函数。

    dyld 的发展历程是一部典型的**"以空间换时间" "预计算换时间",再到"智能化平衡"**的性能进化史。

    以下是 dyld 从 1.0 到 4.0 的详细演进历程:


    1. dyld 1.0:

    关键词:静态二进制、预绑定 (Prebinding)

    dyld 1 最早随 NeXTStep 3.3 发布。在这个阶段,动态链接的概念刚刚起步,性能并不是最优的。

    • 核心痛点:早期的 dyld 在系统广泛使用 C++ 动态库之前编写。C++ 的初始化器(Initializers)在静态环境下工作良好,但在动态环境中会导致 dyld 做大量工作,显著拖慢启动速度。
    • 优化手段:预绑定 (Prebinding)
      • 为了加速启动,苹果引入了预绑定技术。它的原理是为系统中的所有动态库和应用程序分配固定的内存地址
      • 如果加载成功,dyld 会修改二进制文件,将预计算的地址写入。下次启动时,如果地址没变,就不需要重新计算偏移,直接加载即可。
    • 缺陷
      • 安全性差:每次启动可能都需要修改二进制数据,这在安全上是不友好的。
      • 维护困难:一旦系统库更新,所有依赖它的 App 都需要重新进行预绑定,导致更新过程缓慢。

    2. dyld 2.0:

    关键词:共享缓存 (Shared Cache)、ASLR、代码签名

    随着 macOS Tiger (10.4) 的发布,dyld 2 问世。这是对 dyld 的完全重写,也是应用时间最长、最经典的一个版本(一直沿用到 iOS 12)。

    • 核心改进

      1. 废弃预绑定,引入共享缓存 (Dyld Shared Cache)
        • 这是 dyld 2 最伟大的发明。系统将常用的系统库(如 UIKit, Foundation, CoreGraphics 等)合并成一个巨大的文件,即 dyld_shared_cache
        • 优势:这个文件在系统更新时生成,包含了优化后的符号表和数据结构。所有 App 启动时直接映射这个文件,极大地减少了内存占用(节省了 500MB-1GB 内存)并加快了加载速度。
      2. 安全性提升
        • 引入了 ASLR (地址空间布局随机化),每次启动库的地址随机,防止攻击。
        • 引入了 Code Signing (代码签名),确保库未被篡改。
      3. C++ 支持:正确支持了 C++ 初始化器语义,提升了对 C++ 库的支持效率。
    • 简化版工作流程

      在 dyld 2 中,App 启动时,dyld 需要在进程内 (In-Process) 同步执行以下操作:

      1. 自举 :解析 Mach-O 文件头。bootstrap 启动 dyld,实例化主程序对象。
      2. 映射 :映射 dyld_shared_cache,加载插入库。
      3. 加载:递归加载所有依赖库,构建依赖树。
      4. 链接Rebase (修内)和 Bind(连外)。
      5. 初始化 :跑 +load,找 main,交权。
      • 缺点:这些操作都在主线程串行执行,如果库很多,会明显阻塞 App 启动。
    • 详细工作流程:

bash 复制代码
1. 内核启动进程,把控制权交给 dyld
2. dyld 自身初始化,解析主程序 Mach-O 头和运行环境
3. 为主可执行文件建立 image loader
4. 检查 dyld shared cache 和 dyld 环境配置(如 DYLD_INSERT_LIBRARIES)
5. 递归加载主程序依赖的所有 dylib
6. 对所有 image 做 rebase / bind / 其他 fixups
7. ObjC runtime 根据 dyld 回调完成类、分类、协议、selector 注册
8. 按"先依赖后自身"执行各 image 的初始化逻辑(constructor / +load 相关流程)
9. 读取主程序的 LC_MAIN load command,取得入口地址
10. 跳转到入口,最终进入 main
    

3. dyld 3.0:

关键词:启动闭包 (Launch Closure)、进程外解析

在 WWDC 2017 上,苹果推出了 dyld 3,并在 iOS 13 中强制用于第三方 App。其核心理念是:"将耗时的操作移出 App 启动过程"

  • 核心机制:启动闭包 (Launch Closure)
    • 进程外预计算 (Out-of-Process) :当 App 安装、更新或手机重启后,系统会在后台(守护进程)预先解析 App 的 Mach-O 文件,计算好所有的依赖关系、符号地址和偏移量,生成一个二进制文件,称为 Launch Closure
    • 进程内执行 (In-Process):App 启动时,dyld 不再需要解析 Mach-O 头或查找符号,而是直接读取预先计算好的 Launch Closure。(lanch closure:缓存服务,系统程序的lauch直接内置在sharedcache中,而第三方APP在安装或者更新时生成)
  • 性能提升
    • 跳过了耗时的符号查找和依赖解析过程。
    • 冷启动速度提升显著(约 30%+)。
  • 局限性
    • 脆弱性:如果 App 或其依赖库被修改(如热修复、签名变更),预先计算的闭包就会失效,dyld 3 必须回退到类似 dyld 2 的慢速模式重新生成闭包。

4. dyld 4.0:

关键词:双模式加载 (Dual-Mode)、PrebuiltLoader

随着 iOS 16 和 macOS 13 的发布,dyld 4 登场。它结合了 dyld 2 的灵活性和 dyld 3 的高性能,旨在解决 dyld 3 在频繁更新场景下的失效问题。

  • 核心架构:双模式引擎

    dyld 4 引入了两种加载器,并根据场景智能切换:

    1. PrebuiltLoader (预构建加载器)
      • 类似于 dyld 3 的闭包,但更轻量。它只存储必要的元数据。
      • 优先从 dyld_shared_cache 或磁盘缓存中加载。
    2. JustInTimeLoader (即时加载器)
      • 类似于 dyld 2 的实时解析。
      • 当预构建加载器失效(如首次安装、热更新)时,dyld 4 会无缝切换到即时加载模式,解析完成后再生成新的预构建缓存供下次使用。
  • 优势

    • 兼顾速度与灵活性:既享受了预计算带来的极速启动,又完美适配了热修复、频繁更新等动态场景。
    • 内存更优:PrebuiltLoader 仅存储元数据,内存占用比 dyld 3 的闭包更低。

dyld2->dyld3

  1. 在dyld2中,解析mach-o文件,处理搜索路径,查找依赖库等逻辑都放在进程内部,如果攻击者可以篡改App的二进制头文件或者利用环境变量注入,就可以在app运行时进行攻击。所以在dyld3中,将原本庞大的链接器拆分为近程外解析器(一个独立的后台进程,读取磁盘上的mach-o文件、解析头文件、遍历搜索路径、查找依赖库等操作都在这个进程中执行,即使被攻击了也不会影响app进程)以及进程内引擎(接收进程外生成的启动闭包,验证)
  2. dyld2中每次启动都会做大量的重复计算,特别是符号查找,很浪费CPU资源。所以dyld3引入了Launch Closure(启动闭包),在安装app或者系统更新时进程解析器就开始工作,计算好所有依赖库符号地址等,生成一个二进制启动闭包文件缓存在磁盘上,内部存储依赖图、符号绑定最终地址、初始化顺序等所有静态信息。App启动时进程内引擎直接mmap映射这个闭包文件。

dyld3符号缺失问题

  1. dyld2中默认采取lazy symble的符号加载方式
  2. dyld3中,app启动之前符号解析结果已经映射进启动闭包中了,所以lazy symbol就不再需要了
  3. dyld2中出现符号缺失情况:首次调用时app会crash。dyld3中app已启动就crash了
相关推荐
LoyalToOrigin6 小时前
iOS 26 libass字幕渲染问题兼容解决实践
ios·ffmpeg·objective-c
2501_915921436 小时前
穿越HTTPS迷雾:Wireshark中的TLS抓包秘诀与文件合并方法
网络协议·ios·小程序·https·uni-app·wireshark·iphone
懋学的前端攻城狮6 小时前
网络层架构演进:从回调地狱到声明式数据流
ios
程序员小崔日记6 小时前
当 AIR 只支持 Mac,我开始重新思考操作系统这件事
macos·操作系统·ai编程
一个人旅程~10 小时前
黑苹果系统都支持哪些硬件键盘和笔记本型号,以老旧电脑dell n4020为例安装黑苹果的可能性分析
经验分享·macos·电脑
Eloudy10 小时前
macOS 上开启 SSH 服务
运维·macos·ssh
蜜汁小强11 小时前
macOS 开发者的 tmux 实战配置:分屏导航、vi 复制模式与系统剪贴板一站打通
macos·策略模式
白狐_79811 小时前
【深度拆解】2026年数字化学习流:iPad 主动式电容笔的技术底层与选型实测
学习·ios·ipad·电容笔
2501_9159184111 小时前
快蝎iOS开发IDE:免Xcode开发,支持Swift/Flutter项目
ide·vscode·ios·个人开发·xcode·swift·敏捷流程