目录
前言
编译过程与动静态库
编译过程
动静态库
dyld
📌 什么是 dyld?
dyld加载流程
_dyld_start
dyldbootstrap::start
dyld::main()
配置环境变量
共享缓存
主程序的初始化
插入动态库
link主程序
link动态库
弱符号绑定
执行初始化方法
程序启动时(加载镜像)
类首次被访问时
程序退出时(卸载镜像)
动态库的卸载(例如插件机制)
寻找主程序入口
前言
我们平时编写的程序的入口函数都是main.m文件里面的main函数,但在重新load方法后,会发现load方法是在main函数之前执行的,那么在main函数之前到底发生了哪些事呢?这篇文章我们就来探究一下。
编译过程与动静态库
编译过程
当我们在编译器上按下按钮进行开发调试时,编译器其实帮我们做了许多事,整个过程可以拆分成四个步骤:预处理、编译、汇编和链接
这四个步骤会完成这些事:
-
预处理:处理#开头的预处理指令,替换宏、展开头文件、删除注释,输出中间文件:.i
-
编译:对.i文件进行词法、语法和语义分析,执行代码优化,生成汇编代码,输出中间文件.s
-
汇编:将.s汇编文件翻译成机器码,输出目标文件.o
-
链接:将多个.o文件与系统库、框架等一起链接成可执行文件,解决函数/变量引用、地址重定位等,输出最终文件:可执行程序。在这个过程中,链接器将不同的目标文件链接起来,因为不同的目标文件之间可能有相互引用的变量或调用的函数,如我们经常调用
Foundation
框架和UIKit
框架中的方法和变量,但是这些框架跟我们的代码并不在一个目标文件中,这就需要链接器将它们与我们自己的代码链接起来
动静态库
Foundation`和`UIKit`这种可以共享代码、实现代码的复用统称为`库`——它是可执行代码的二进制文件,可以被操作系统写入内存,它又分为`静态库`和`动态库
静态库:静态库是一种将代码编译后封装起来的二进制文件,在程序编译链接阶段被打包进最终的可执行文件中,运行时不再依赖外部库文件。但同时由于需要将库复制进最终程序,会使最终可执行文件体积变大。如.a、.lib都是静态库
动态库:动态库(Dynamic Library),也称为 共享库,是指在程序运行时动态加载的代码模块,不会在编译时被打包进可执行文件中,而是以共享形式存在,运行时由操作系统加载。多个程序可以共享同一个动态库的实例,系统只需加载一次动态库,可以节省内存。如.dylib、.framework都是动态库
dyld
📌 什么是 dyld?
dyld
(Dynamic Link Editor) 是苹果操作系统中的 动态链接器,负责在程序启动时将程序依赖的 动态库(dynamic libraries)加载到内存,并完成 符号解析与重定位,以确保程序能够正常运行。在应用被编译打包成可执行文件格式的Mach-O文件之后 ,交由dyld负责链接,加载程序。
所以应用程序启动应该是如下流程:
dyld_shared_cache:
为了优化程序启动速度和利用动态库缓存,苹果从
iOS3.1
之后,将所有系统库(私有与公有)编译成一个大的缓存文件,这就是dyld_shared_cache
,该缓存文件存在iOS系统下的/System/Library/Caches/com.apple.dyld/
目录下
dyld加载流程
我们在load方法和main方法处加一个断点,使用LLDB——bt指令打印,可以看到最初的起点。在最新的xcode中,这个函数是start
而笔者在阅读博客时,最初的起点是_dyld_start,而dyldbootstrap这个命名空间作用域里存在着这个start函数,_dyld_start会调用这个函数,整个启动逻辑应该差别不大,这里先按照博客中阅读的来讲解
_dyld_start
在dyld源码中可以搜索到_dyld_start这个函数,发现它是汇编实现的,并且它调用了dyldbootstrap::start方法。
dyldbootstrap::start
dyldbootstrap::start是指dyldbootstrap这个命名空间作用域里的 start函数。通过命名空间找到这个方法,方法的核心是返回值调用dyld的main函数,第一个参数是一个Mach-O(可执行文件)的头部。Mach-O类型分为四个部分:Mach-O头部、
Load Command、
section、
Other Data。
这个函数主要进行了一些 dyld 自身状态的初始化,进行重定位、栈溢出保护、参数解析等等,最重要的一步是调用dyld::_main()(真正开始启动app)。
dyld::main()
dyld::main的主要流程为:
-
配置环境变量:创建一个
RuntimeState
或类似结构体,保存当前 App 路径、argc/argv/env/apple、主程序的 header、slide 等启动信息。为接下来 image 加载和绑定做准备。(根据环境变量设置相应的值以及获取当前运行架构) -
加载共享缓存:优先从预编译的 dyld shared cache(系统框架缓存)中加载常用系统库(如 libSystem、Foundation 等),提高性能。 如果无法使用 shared cache,就退回到逐个加载(fallback 逻辑)。
-
主程序初始化:通过 instantiateFromLoadedImage()实例化主程序 image(Mach-O 文件),构造一个 ImageLoader 实例(封装 image 加载行为)
-
插入动态库:解析环境变量 DYLD_INSERT_LIBRARIES,加载用户或调试器插入的动态库。
-
Link 主程序:递归解析主程序的依赖库(LC_LOAD_DYLIB),完成主程序的符号绑定与链接(如解析外部函数地址)
-
Link 动态库:将主程序依赖的所有 dylib 递归 link 完成,
-
弱符号绑定
-
执行初始化方法:执行所有 image 的 mod_init_funcs(C++ 构造函数、ObjC +load 等)
-
寻找并跳转到主程序入口 main()
配置环境变量
平台,版本,路径,主机信息的确定
共享缓存
checkSharedRegionDisable检查是否开启共享缓存(在iOS中必须开启)
mapSharedCache加载共享缓存库,其中调用loadDyldCache函数有这么几种情况:
-
仅加载到当前进程mapCachePrivate(模拟器仅支持加载到当前进程)
-
共享缓存是第一次被加载,就去做加载操作mapCacheSystemWide
-
共享缓存不是第一次被加载,那么就不做任何处理
主程序的初始化
调用instantiateFromLoadedImage函数实例化了一个imageLoader对象
进入instantiateFromLoadedImage方法,其中创建了一个ImageLoader实例对象,通过instantiateMainExecutable方法创建。
进入instantiateMainExecutable源码,其作用是为主可执行文件创建映像,返回一个ImageLoader类型的image对象,即主程序。其中sniffLoadCommands函数会获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验
插入动态库
遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载,通过该环境变量我们可以注入自定义的一些动态库代码从而完成安全攻防,loadInsertedDylib内部会从DYLD_ROOT_PATH、LD_LIBRARY_PATH、DYLD_FRAMEWORK_PATH等路径查找dylib并且检查代码签名,无效则直接抛出异常
link主程序
这里rebase函数执行的是重定位,即将原本指向自己的段内部地址,修正为加载到内存中的真实地址。
link动态库
弱符号绑定
这里符号绑定(bind)是将原本指向自己的段内部地址,修正为加载到内存中的真实地址。
执行初始化方法
从函数调用栈里可以发现进入dyld::main()函数后,初始化的起点是dyld::initializeMainExecutable,进入initializeMainExecutable源码,主要是循环遍历去执行runInitializers
sImageRoots 是一个镜像根列表,第 0 个是主程序,其它的是插入的动态库;
遍历这些库,调用 runInitializers() 执行构造函数(构造器、initializer);
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);这一行初始化主程序及其依赖的动态库
找到runInitializers的源码,核心代码是 processInitializers,为初始化做准备
进入processInitializers函数的源码实现,其中核心部分是对镜像列表调用recursiveInitialization函数进行递归实例化。
这里的镜像列表包含的是还未执行初始化的镜像对象,主要包括:主程序本身、插入的动态库、主程序依赖的系统或用户动态库、间接依赖库。
找到recursiveInitialization函数,作用是获取到镜像的初始化
这里我们分成两个部分来看,一部分是notifySingle函数,另一部分是负责初始化镜像的doInitialization函数,首先探索notifySingle函数
源码中与启动流程相关的重点是一句通过静态全局函数指针调用的回调,函数指针sNotifyObjCInit在别的地方赋值(即注册回调),dyld在合适的时机进行调用(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
现在我们来探索在哪里对这个函数指针进行了赋值,全局搜索sNotifyObjcInit,可以找到赋值操作
搜索registerObjCNotifiers在哪里调用,发现只在_dyld_objc_notify_register里调用了,这个函数只在运行时提供给objc使用。
这时,_dyld_objc_notify_register的源码需要在libobjc源码中搜索,libobjc 是 Objective-C(OC)语言的核心库之一,负责处理运行时的动态行为。为什么要去libobjc中搜索,因为dyld 本身不负责 Objective-C 的类注册、+load 调用等逻辑,这些是 libobjc 的职责
dyld 只知道:
-
某个镜像(dylib)被加载了
-
某个镜像初始化完成了
所以 dyld 会说:
“我通知你(libobjc),该你干活了。”
libobjc 就会注册回调进来,让 dyld 通过这些回调「把控制权还给它」。
在objc源码中搜索_dyld_objc_notify_register,发现在_objc_init的是实现中调用了该方法,并传入了参数,所以sNotifyObjcInit中赋值的就是objc中的_load_images
这里的map_images、load_images、unmap_image三个函数都是在libobjc实现的,赋值给dyld中源码,由dyld在合适的时机调用。load_images会调用所有的+load方法,可以继续往里探索。
这里所谓合适的时机:具体的时机包括:
程序启动时(加载镜像)
dyld
初始化:在程序启动时,dyld
会被加载并开始执行,负责加载主可执行文件以及其依赖的动态库。在加载过程中,dyld
会调用map_images
来将这些库的内容映射到内存中。加载动态库时调用
load_images
:一旦镜像被映射到内存,dyld
会调用load_images
以确保库中的初始化代码得以执行,包括处理 Objective-C 类的元数据。此时,如果库中包含 Objective-C 类,libobjc
会接管对类元数据的管理,确保它们被正确初始化。类首次被访问时
延迟初始化:当程序第一次访问某个类时,
libobjc
会通过load_images
完成该类的初始化,包括注册类、方法解析、执行+initialize
等操作。这意味着,虽然类的元数据在程序启动时已经被加载到内存中,但类的完整初始化通常会延迟到第一次使用时。程序退出时(卸载镜像)
unmap_image
的调用时机:在程序退出时,dyld
会清理和卸载不再需要的动态库,这时unmap_image
会被调用,卸载那些已经加载的镜像,并释放相关资源。动态库的卸载(例如插件机制)
如果程序在运行过程中动态加载和卸载插件或其他动态库,
dyld
会在合适的时机调用unmap_image
来卸载镜像。这通常发生在使用dlclose
等系统调用卸载共享库时。
进入cal_load_methods()的源码
这里的核心就是do-while循环调用+load方法,上图红框中是类调用load方法,下面是类别调用load方法
进入call_class_loads函数
可以看到这里的实现其实就是直接通过SEL找到了load对应的IMP进行load方法的调用
注意:这里的类即所有类,镜像列表中会包含所有类的镜像——包括所谓的“懒加载类”,因为从底层实现来看,所有编译时就存在的类,其信息都会打包进对应的 Mach-O 镜像,并由 dyld 在启动或动态加载库时统一处理。
❗️懒加载 ≠ 没被加载到内存懒加载类的“懒”只是指
+initialize
的执行不是立即的,而不是类的元数据未被加载。懒加载类的元数据同样在dyld启动时加载到内存中。关于类别:
① 编译时定义的分类(在
.m
文件中写的@interface MyClass (CategoryName)
):
✅ 会被编译器生成元数据,并写入到 Mach-O 文件的:
__objc_catlist
(分类列表)
__objc_const
(分类的方法、属性等结构)✅ 加载过程:
程序启动或动态库加载时,
dyld
加载镜像。调用
libobjc
的load_images()
。遍历
__objc_catlist
,找到所有分类。将分类合并到其主类(category -> class)上。
如果分类实现了
+load
方法,进入call_category_loads()
执行。✅ 所以分类虽然不是类自身的一部分,但它们是随着镜像加载一起加载的。
② 运行时动态注册的分类:
🧩 通过 API 手动添加,比如:
extern void objc_addCategory(Class cls, Category *cat);注:这类函数不是公开 API,可能是私有或内部机制。
🚫 不依赖 dyld,也不会写在 Mach-O 的
__objc_catlist
中。✅ 它们注册后也能像普通分类一样扩展类的功能(方法、属性等),但不是通过镜像加载的,而是手动注册进 runtime 的哈希表中。
然而,实现了+load的类,也会非懒的效果,因为从语义上,人们把“懒加载类”理解为:
等到我真正用它的时候,它才在内存中变得活跃。
但
+load
彻底破坏了这种“懒”:
类注册后立即调用
+load
这个类的所有元数据都要准备好
如果它还有分类有
+load
,也会一并调用因此,在行为上,这些类无法再被延迟加载使用,所以说它是“非懒加载类”。
类的元数据包括:objc_class、class_rw_t、class_ro_t与方法、属性、变量、协议等列表结构(这些都来源于class_ro_t)
这时就又有了一个问题,_objc_init是什么时候调用的呢?还记得刚开始我们把recursiveInitialization分成了两部分来看吗?还有一部分就是doInitialization函数的源码实现,现在我们来看看这个函数
这里也分成两部分:doImageInit函数和doModInitFunctions函数
doImageInit函数:
这个函数主要是在镜像加载完毕后,检查当前 Mach-O 文件中是否存在 LC_ROUTINES_COMMAND
(旧版 Mach-O 中用于指定初始化函数),如果有,就提取出 init_address
,偏移 slide
后计算出真实地址,并立即调用它。
这个-init的作用是在 镜像(动态库或可执行文件)被 dyld 加载后立即执行某段初始化逻辑,目的是:
✅ 在程序或库使用前,执行初始化代码,配置运行环境、注册资源、设置全局状态等。
进入doModInitFunctions源码,可以发现这个函数的重点就是加载了所有Cxx文件。
到现在还是不知道_objc_init何时调用,我们为它添加断点查看堆栈信息可以发现在完成所有加载工作后,最先调用的函数是libSystem_initializer
在libsystem中查找libSystem_initializer,查看其中的实现:
可以发现会调用libdispatch_init函数,这个函数的源码是在libdispatch开源库中的,在libdispatch中搜索libdispatch_init
调用了_os_object_init函数
可以发现在这里调用了_objc_init
寻找主程序入口
最后,关于寻找主程序入口,底层通过汇编实现的,需要注意的是main是写定的函数,写入内存,读取到dyld。