目录

前言

编译过程与动静态库

编译过程

动静态库

dyld

📌 什么是 dyld?

dyld_shared_cache:

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方法。

截屏2025-05-11 03.38.02

dyldbootstrap::start

dyldbootstrap::start是指dyldbootstrap这个命名空间作用域里的 start函数。通过命名空间找到这个方法,方法的核心是返回值调用dyld的main函数,第一个参数是一个Mach-O(可执行文件)的头部。Mach-O类型分为四个部分:Mach-O头部Load CommandsectionOther 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(分类的方法、属性等结构)

  • ✅ 加载过程:

    1. 程序启动或动态库加载时,dyld 加载镜像。

    2. 调用 libobjcload_images()

    3. 遍历 __objc_catlist,找到所有分类。

    4. 将分类合并到其主类(category -> class)上。

    5. 如果分类实现了 +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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/89492.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/89492.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/89492.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从零开始,手把手教你本地部署Stable Diffusion AI绘画(Win最新版)

本号之前有发过一篇win平台的教程,由于是去年10月发布的,而Al绘画技术发展很快,那篇教程已经有些不适用了,有些同学执行到第二步就出错了。 应广大同学的期望,我更新一版新版详细教程。 一、前言 1.为什么要本地部署…

day21 力扣669. 修剪二叉搜索树 力扣108.将有序数组转换为二叉搜索树 力扣538.把二叉搜索树转换为累加树

修剪二叉搜索树 给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关…

《设计模式之禅》笔记摘录 - 7.中介者模式

中介者模式的定义中介者模式的定义为:Define an object that encapsulates how a set of objects interact.Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently…

Flutter:上传图片,选择相机或相册:wechat_assets_picker

图片选择功能:可选单张,或多张。 1、showModalBottomSheet(选择相册/相机) 2、WechatImagePicker(选取图片) 3、CompressMediaFile(图片压缩)1、ActionSheetUtilimport package:duca…

pytest--0

1 pytest 使用方式 pytest测试框架-- 基本功能使用详解 2 pytest-mock常用方式 pytest–1–pytest-mock常用的方法 3

multiprocessing.Pool 中的 pickle 详解

前言: 在 Python 的 multiprocessing.Pool 中,任务和数据需要通过序列化(pickle)传递给子进程。pickle 是 Python 的内置序列化模块,用于将 Python 对象转换为字节流,以便在进程间通信时传递。然而&#xf…

Java集合框架体系详解:List/Set/Map接口对比与核心实现原理

一、集合框架核心接口对比 1.1 List/Set/Map接口特性接口类型特性描述典型实现List有序可重复,支持索引访问ArrayList/LinkedListSet无序不可重复,基于哈希表或树实现HashSet/TreeSetMap键值对存储,键唯一值可重复HashMap/TreeMap核心差异&am…

LeafletJS 进阶:GeoJSON 与动态数据可视化

引言 LeafletJS 作为一个轻量、灵活的 JavaScript 地图库,以其对 GeoJSON 数据格式的强大支持而闻名。GeoJSON 是一种基于 JSON 的地理数据格式,能够表示点(Point)、线(LineString)、多边形(Po…

【STM32实践篇】:F407 时钟系统

文章目录1. 时钟与启动2. CubeMX 时钟树2.1 时钟源2.2 PLL 锁相环2.3 时钟分发与选择2.4 频率限制1. 时钟与启动 复位默认时钟:系统复位后,CPU 时钟默认由 16MHz 内部 RC 振荡器(HSI)提供,该 RC 振荡器经工厂校准&…

纯前端html实现图片坐标与尺寸(XY坐标及宽高)获取

纯前端html实现图片坐标与尺寸&#xff08;XY坐标及宽高&#xff09;获取。用于证书图片或pdf打印的坐标测定。 <!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8"> <title>纯html前端实现图片坐标与尺寸&am…

飞睿UWB超宽带定位测距技术,数字钥匙重塑智能生活,高精度厘米级定位无感解锁

最近&#xff0c;数字钥匙领域动作频频&#xff0c;科技巨头与车企正掀起一波创新浪潮。小米15S Pro搭载恩智浦UWB芯片&#xff0c;用户靠近闸机即可无感通行深圳云巴一号线&#xff0c;轻触小米YU7车门自动解锁&#xff0c;实现手机-汽车-公共交通的无缝数字钥匙生态。在智能家…

基于springboot+vue+mysql平台的医疗病历交互系统(源码+论文)

一、开发环境 相关技术介绍 B/S模式分析 C/S模式&#xff1a;主要由客户应用程序(Client)、服务器管理程序(Server)和中间件(middleware)三个部件组成。客户应用程序是系统中用户与数据组件交互。服务器程序负责系统资源&#xff0c;如管理信息数据库的有效管理。中间件负责连…

arm架构,arm内核,处理器之间的关系

一、情景分析 我们经常说&#xff0c;stm32f103是采用cotex-M3内核&#xff0c;基于armv7架构设计的。 那么&#xff0c;stm32f103、cotex-M3、armv7之间有什么关系呢&#xff1f; 二、层次分析 1. 架构&#xff08;Architecture&#xff09; 定义&#xff1a;架构是处理器…

基于PHP的招投标系统_603gk

目录具体实现截图课程项目技术路线开发技术介绍PHP核心代码部分展示系统测试详细视频演示/源码获取具体实现截图 课程项目技术路线 招投标系统后端采用 PHP 语言搭配Thinkphp或者 Laravel 框架&#xff0c;PHP 语法简洁且功能强大&#xff0c;Laravel 或者Thinkphp框架能优化代…

深入解析 JavaScript 中的 `$.ajax()`:专业指南与实战示例

文章目录一、为什么需要 $.ajax()&#xff1f;二、核心语法解析三、关键参数深度剖析四、实战示例&#xff1a;从基础到进阶五、错误处理最佳实践六、性能与安全优化七、现代替代方案对比八、总结作为网站编辑&#xff0c;我将带您深入剖析 jQuery 的 $.ajax() 方法。本文不仅涵…

Flutter 前端开发中的常见问题全面解析

Flutter 开发中的常见问题全面解析一篇给 Flutter 开发者「灵儿」里里外外都能看的问题项。从基础开发到打包上线&#xff0c;每一步都充满坑&#xff0c;我们详细列出「环环盗光」的那些场景和解决思路&#xff01;【基础系统】开发环境问题 1. flutter doctor 报错 常见错误:…

STM32 单片机的停车场管理系统设计与实现

基于 STM32 的停车场管理系统设计与实现摘要随着城市汽车保有量的快速增长&#xff0c;停车场管理的效率与智能化水平愈发重要。本文设计并实现了一套基于 STM32 单片机的停车场管理系统&#xff0c;整合车辆检测、车位引导、计费管理及信息交互等功能。系统以 STM32 为控制核心…

STM32 写选项字 关键要加载HAL_FLASH_OB_Launch

AI乱写&#xff0c;还是得自己来&#xff01;void Write_OptionBytes_IWDG_STDBY(void) {FLASH_OBProgramInitTypeDef OBInit;HAL_FLASHEx_OBGetConfig(&OBInit); // 获取当前选项字节配置[6,7](ref)// 检查当前nRST_STDBY位&#xff08;IWDG_STDBY相关位&#xff09;是否…

153.在 Vue 3 中使用 OpenLayers + Cesium 实现 2D/3D 地图切换效果

&#x1f3ac; 效果演示截图 ✨ 前言 在实际项目开发中&#xff0c;我们经常需要提供「二维地图 三维地形」的可视化效果切换&#xff0c;例如&#xff1a; 智慧农业展示耕地分布 三维地形起伏&#xff1b; 智慧城市展示建筑物点位 三维城市&#xff1b; 数字孪生场景中&…

纯C++11实现!零依赖贝叶斯情感分析系统,掌握机器学习系统工程化的秘密!

本文深度剖析了一个完全基于C++11标准库实现的贝叶斯情感分析系统。该系统采用模块化设计,实现了从文本预处理、特征提取到朴素贝叶斯分类的完整机器学习流水线。 1. 系统架构概览 1.1 技术栈选择与设计哲学 该系统完全采用C++11标准库实现,无任何外部依赖,体现了"纯…