惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Security @ Cisco Blogs
P
Privacy & Cybersecurity Law Blog
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Threatpost
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
C
CERT Recently Published Vulnerability Notes
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Cisco Blogs
博客园 - 司徒正美
L
LINUX DO - 热门话题
D
Docker
M
MIT News - Artificial intelligence
GbyAI
GbyAI
Microsoft Azure Blog
Microsoft Azure Blog
Microsoft Security Blog
Microsoft Security Blog
Cisco Talos Blog
Cisco Talos Blog
Jina AI
Jina AI
Last Week in AI
Last Week in AI
Security Latest
Security Latest
The Hacker News
The Hacker News
L
Lohrmann on Cybersecurity
Y
Y Combinator Blog
A
Arctic Wolf
小众软件
小众软件
T
Threat Research - Cisco Blogs
Know Your Adversary
Know Your Adversary
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
I
InfoQ
腾讯CDC
Google DeepMind News
Google DeepMind News
B
Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
C
CXSECURITY Database RSS Feed - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 叶小钗
I
Intezer
NISL@THU
NISL@THU
A
About on SuperTechFans
爱范儿
爱范儿
C
Cybersecurity and Infrastructure Security Agency CISA
D
Darknet – Hacking Tools, Hacker News & Cyber Security
G
Google Developers Blog
J
Java Code Geeks
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Cyberwarzone
Cyberwarzone
AWS News Blog
AWS News Blog
Engineering at Meta
Engineering at Meta
Latest news
Latest news

一个工匠

AI 发展临界点 - 快上车 流式响应(Chat):HTTP 响应体字节流的读取与解析 iPhone 截屏高效翻译 Flash Open Terminal iPhone 侧载安装 Xcode Symbolic Debug AI 指北 投资风险收益评估 Mac 快速修改系统快捷键 Git Record passkey How to pay apple frameworks Mac - Command Line Tools DNS 的 CNAME 是如何工作的 WebRTC 的一些解释 SSE 指南 1.1.1.1、WARP 和 MASQUE 移动端换端方案与场景还原 设备发现 分辨率 - 像素密度 - HiDPI Mac 字体 ESP32 trump - 川普 币圈 - 虚拟货币 【旅游】富士山 【旅游】sakura(樱花) 【旅游】千岛湖 我的 2023 【Swift】中文词语纠错 计算机字符编码与内存编码 - Unicode、UTF8、String 从汇编角度理解 “函数调用栈” 和 “有栈协程” 在 Xcode 中更好的使用 Swift 通过 Swift Package 制作二进制库 Arc 浏览器 - 意外的惊喜 【Swift 三方源码 1】SwiftShell 高效的命令行工具 Swift 脚本开发环境搭建 rime - 小鹤双拼 【旅游】沿海旅游 (威海、青岛) 好记性远远不如烂笔头 高级语言小记 又一年高考 Vision 未来已来 致敬耗子哥 Socket 与 ICMP 对互联网隐私的一些想法 网络代理是如何工作的 (致敬 Surge) 又十年 - 至三十而立 双拼 - 少有的高回报效率技能 Vim 技巧 我的 2022 三年疫情五味杂陈 - 下 老梗 - 浏览器按回车后发生了什么 三年疫情五味杂陈 生活推荐清单 Shell 和进程 有计划的努力 内卷 不确定性 数学可以保障逻辑自洽 这些年我的软件清单 决定做一些改变 函数调用栈之彻底理解 锁 - 共享数据安全指↑ 江苏女有感 碎碎念 - 2022 碎碎念 - 2021 打算记录一些碎碎念 多重性格就是逆天改命 从 Core Foundation 看更大世界 TCP 数据传输过程分析 iOS main 文件中的 @autoreleasepool 的真实用途 多线程的难点在哪里 早起打卡计划 ing 打比方 IM 和 Socket 的关系及 Heart 的必要性 如果我的人生突然只剩下 8 小时... 胡说八道之雪粉 Array 和 LinkedList 的深刻认知 Swift struct (值类型) 与 class (引用类型) 区别及三种传参机制深刻关系和使用 逃离信用卡和花呗 Swift_let_var_struct_class 网聚人的力量 - 网易 绿皮火车 喝酒十之八九会误事 HKong 胡说八道之云服务 互联网网络传输协议理解坑点 CPU 的制作 我心里一直有一个念想 网络基础协议 为什么 32 位操作系统只能用 4G 内存空间 努力和天赋 洗牙 RAC-Api-Summer iOS 内存原理 Block 闭包简查记录 《白夜行》 谈谈事件驱动 老博新开 祭首
Objective-C 和 Runtime
海驴 · 2020-04-08 · via 一个工匠

运行时 (Runtime) 本身是一个非常普通的概念,每个编程语言都会有运行时,非 iOS Objective-C 特有。
概念上理解,运行时,就是程序执行的过程,每个编程语言,只有运行后执行特定的任务才有价值,所以每个编程语言,都有运行时。
而 Objective-C 的 Runtime 显然不仅仅是程序执行过程这么简单,它一举将基于
面向过程的语言 C
而实现的面向对象的语言 Objective-C 变成了动态语言
为什么 Objective-C 的 Runtime 有些难理解,因为运行时从概念上理解非常简单,但是 Java、Python、C 它们的运行时都是不一样的,运行时期间可以做很多事情,所以运行时理解起来还是比较抽象和高阶。
如果把 Runtime 改名为 OCDR(Objective-C Dynamic Resolution,即 Objective-C 动态决议),剥离运行时和运行时库的概念,那么很多人都会轻松掌握 Objective-C Runtime 的核心。而 Objective-C Runtime 本质上的确就在践行动态决议这么一个过程。

Objective-C Runtime 使得函数调用变成了动态消息传递,并且可以在执行过程中对 Object 进行增删改查,所以更准确的说,Runtime 使得 Objective-C 变成了动态语言。

动态语言分析

首先理解一下各类语言的一个区分分界,那就是动态语言静态语言的区分(非动态类型语言和静态类型语言)。
关于编译型语言 & 解释型语言,可以查看 Shell 和进程

静态语言

静态语言比较容易理解,我们通过高级语言写好的代码,经过预处理、编译、汇编、链接后,就形成了机器码(二进制文件)。这个时候,函数名、变量名都保存在符号表中,并拥有对应的虚拟地址。而我们调用一个函数,就是通过机器码调用的,汇编示例如:’call 0X12345678’,这里 call 代表函数调用,0X12345678 代表函数的虚拟地址。
这个机器码执行的过程中,PC 指令寄存器会不断的记录下一行将要执行的指令,然后不停的执行下去 (延伸一个知识点,如果下一行指令不再内存中,操作系统会发现页缺失,然后从硬盘中将对应缺失页的指令拷贝到对应的真实内存中,然后继续执行)。
这里我们可以发现,我们通过高级语言写好的函数,在编译后,就已经不可改变了。程序执行后,CPU 内部的运算单元、控制单元、数据单元,就有条不紊的按照机器码执行就好了。
那么如果我们想改变一个函数的实现呢?比如本身调用的 A 函数,运行期间想调用 B 函数,有办法实现吗?对于静态语言来说,不行!
那么如果我们想改变一个对象的结构体呢?对于静态语言来说,不行!
所以静态语言,对于函数的定义,在编译期就必须是明确的,如果找不到函数的实现,就会编译错误。
举例来说,如 C 语言。不过 C 语言也有一个动态化的能力,就是 hook,可以做一定程度的动态能力,不过这并不是语言级别支持的,只是上层业务的补救。

动态语言

动态语言相比静态语言,也比较容易理解了,在运行时代码可以根据某些条件改变自身结构,如函数的调用,自定义类的生成和对象的创建等。那这样的语言就是动态语言。
举例来说,如 Objective-C 语言。

分析

C 是静态的,Objective-C 是动态的,而 Objective-C 是基于 C 实现的。那 Objective-C 是如何基于 C 实现动态特性的呢?

C 有它的运行时,不过程序执行过程中,CPU 完全掌控机器码执行流程,所以 C 的运行时不能多样化,基本上就是按照程序员写的代码执行顺序执行。这也是 C 快的原因,因为 CPU 直接执行,不用考虑那么多复杂的情况。

那 Python 如何实现动态的呢?Python 也有它的运行时,不过说来巧妙,它不是编译性语言,不会像 C 一样打包成机器码直接执行。Python 是解释性语言,代码执行到哪一行,就即时编译该行形成机器码执行,所以 Python 非常容易实现动态,只要代码运行过程中适当的添加 if 语句对将要执行的对象进行替换,就能很好的实现动态,鸭子 Duck 模型也就来源于此。

那 Objective-C 呢?Objective-C 和 C 一样是编译性语言,项目打包后会经过编译链接等处理变成机器码。
Objective-C 实现动态就要运行时了,因为在运行时做了很多操作,以至于很多人叫它 “运行时系统”。
之所以前面说 Objective-C 的 Runtime 应该改名为” Objective-C Dynamic Resolution,Objective-C 动态决议”,就是因为 Objective-C 的 Runtime 主要实现的两个点都更加符合 “动态决议” 这个命名:

  1. 通过 objc_msgsend (a_object,a_object_method,xx) 这个中间者函数,动态解析被调用的函数。
  2. 通过指针、数组的指针、链表指针、全局数据,间接操作函数列表、属性列表等,动态的对 Object 进行增删改查。

Objective-C Runtime 函数调用动态性

之所以很多人说 Objective-C 的函数调用并非函数调用,而是消息传递,其原因就是 Objective-C 的函数调用很不一样。
在 C 中,我们调用一个函数,会这样写:

1
2
3
Node *head = NULL;
...生成链表...
reverseLinkList(&head);// 反转链表

这里,reverseLinkList 函数的调用,在汇编里面为:call 0x12345678,其中 0x12345678 就是 reverseLinkList 这个符号名的虚拟地址。
这里我们可以发现,reverseLinkList 函数的虚拟地址在编译的时候就已经定下来了,后期无法做到动态性。
再来看一下 Objective-C 的实现,因为 Objective-C 是面向对象的语言,所以我们调用对象的一个方法:

1
2
Person p = Person.new;
[p realAge];

这里,[p realAge] 这个函数的调用,p 有自己的虚拟地址,realAge 函数也有自己的虚拟地址,但是在汇编之前,该函数已经被编译成了这样:
objc_msgsend(p, realAge)。
如此,Objective-C 通过 objc_msgsend 这个完全汇编写成的中间函数,在函数内部通过运行时库对 p 进行 superclass 和 isa 的访问,乃至最后实现三次动态函数转发操作。
所以 Objective-C 实现消息传递,就是依靠 objc_msgsend 这个中间函数来实现,如果 objc_msgsend 的虚拟地址为 0x87654321,那么 [p realAge] 函数被执行的时候,实际汇编为:call 0x87654321。

上面说完了 objc_msgsend 这个中间函数完成动态解析的操作,那它是如何进行 superclass 和 isa 的访问,又是如何访问属性和方法列表的呢?

Objective-C 面向对象的本质:C 的 struct(结构体)

通过 Runtime 源码,我们可以发现,Objective-C 里面一切皆对象,乃至 UILabel、NSTimer 等。而这些对象是以什么样的数据结构存在呢?
都是 struct(结构体)!我们的成员变量,成员属性,方法等,都保存在结构体里面。
所以这里我们可以深刻理解一下,为什么说 Objective-C 是 C 的超集,因为面向对象的 Objective-C 语言完全就是依靠面向过程的 C 语言发展起来的,乃至于我们写的面向对象的代码,最后都会变成面向过程的执行流程
我们可以查看苹果公开的 runtime.h 文件,都是面向过程的函数调用。如:

objc_setAssObjective-CiatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssObjective-CiationPolicy policy)

你看,这完全就是面向过程的编程。

因为 struct 是我们对象的存储结构,所以我们可以通过 * class_getProperty (Class _Nullable cls, const char * _Nonnull name)* 等库函数直接对 struct 内部保存的数据进行访问。
比如我们要访问 a_object 的 a_object_method 方法,那么就要通过 a_object_struct,根据 superclass 找到类 A_struct,然后遍历 A_struct 里面的 methods 数组,找到 a_object_method 方法并执行。

struct 结构体在编译后就已经确定,那如何做到动态性如增加方法呢?struct 的实现是 C 和 C++ 混编的,但大体逻辑不变,都是通过指针实现的。
比如说,struct 里面有一个方法的数组指针,那么我们动态添加方法的时候,只要通过这个数组指针就可以添加到对应的方法数组中,这样就完全不会改变 struct 的结构 (甚至如果数组需要扩容,也和 struct 没有任何关系)。

因为 struct 在编译后就已经确定,所以能做动态性的方案不外乎这几点:

  1. 通过数组指针,如方法列表等
  2. 通过链表指针,如 NSNotification 等
  3. 通过全局对象,如关联对象等

所以指针才是 C 和 Objective-C 最终执行的终极形态,不断通过指针间接的对数据区进行增删改查。
Objective-C 的 Runtime 就是不断通过指针对 struct 结构体进行增删改查数据处理来实现动态特性

Objective-C Runtime 总结

Objective-C Runtime 用的最多的是 objc_msgsend,所有的函数调用都会通过这个消息传递完成。
编译成机器码的函数调用,本身无法实现动态性,通过 objc_msgsend 这个中间层巧妙的实现了。
而 objc_msgsend 的汇编执行又得反过来操作运行时库,又回到了对象的 struct 结构体上面。
Objective-C Runtime 本质就是通过指针操作 struct 的增删改查来实现 Objective-C 的动态特性

C 语言中一切皆指针,基于 C 发展起来的 Objective-C,也是一样。
而指针也是中间层思想的体现。本身无法直接操作的数据,通过指针这个中间层,间接的对数据进行操作。

延伸

神话 Runtime 的结果就是越发觉得自己的无知和不知所以。
Runtime 是啥?从编译上来看,Runtime 不是和我们项目一起打包成 ipa 包的,那 Runtime 还能是啥?
Runtime 说的直白点,仅仅是个动态库而已,也叫运行时库
它和我们项目里面使用的自行编译或者三方编译的动态库一样,也需要在应用启动后加载到进程中(Runtime 和系统 framework 一样,属于系统动态库)。

iOS 和 Mac 使用的可执行文件是 Mach-o,与 Linux 的 ELF 和 Window 的 PE 一样,都是基于 COFF 格式扩展的。
所以 Mach-o 可执行文件的运行流程和 ELF 等都大差不差,当然具体到细节有一些差异。但总体都是差不多的,比如 segment 和 section、section head、内存分区等,都一样。

应用启动后,首先操作系统通过 fork 开辟进程,开启用户态后,然后加载 Mach-o head。head 里面有很多重要信息,如 Magic Number (Mach-o 通过 0xfeedface/0xfeedfacf 表示 32/64 位,ELF 则有’E’’L’’F’标记、32/64 位、版本、大小端等)、版本、支持 CPU、文件类型等。

然后找到 Load Commands 区,这里面有几个重要的信息,其中有三个为 Segment (通过 Segment 将相同读取及可执行权限的 section 汇总到一起)DyldDylib
通过 Segment 可以找到具体执行的指令和数据的 Section,和 ELF 里面的 section head (段表) 类似。应用启动的时候还没有到执行 Segment 那一步,还有一些重要的信息没有完成,其中一个就是动态库的符号解析和重定位。
PC 指令寄存器首先指向了 Dyld (动态链接器),将 Dyld 运行起来后,开始将所有动态库复制到内存(已经在内存的无需重复加入,如系统库)。
所有项目里面所有用到的动态库,都会在这个时候拷贝到内存里面,一个都不会少。像一般项目,系统库都会有 200-500 个动态库要添加,因为已经在内存,所以不会耗费时间(手机重启后打开的第一个应用,会比较耗时,就是因为很多系统库都不在内存中)。
Dyld 将所有需要的动态库拷贝到内存后,Runtime 库也就拷贝好了。所以这是 Runtime 第一次展示的时机。而它第二次展示,就要等很多准备操作之后了。

因为 iOS 在 4.3 系统之后,升级了虚拟地址方案,采用了动态虚拟地址,就是每个应用程序启动后,起始地址都是动态的。那么已经编译好的 Mach-0 就需要重新修正指令地址了。
这里的指令地址修复,分为两个环节,一个是本身 Mach-0 内部的地址修复,如函数调用、数据使用。这个就是 Rebase 阶段,总体是 I / O 耗时,但没有太多复杂的。

第二个环节是对动态库对引用修正。因为对动态库的使用本身就是地址为 0,需要在应用启动后重定位的,所以动态虚拟地址和动态库重定位在第二个环节就一套带走了。
这里有个特殊点,就是动态库也是程序,同样包含指令和数据。所有应用程序对动态库的指令都是共享的,如函数调用,但是对数据都是私有的,需要应用程序自己处理。
第二个环节里面,会把所有需要修正的数据全部修正掉,而函数调用,则是真正运行到这行指令的时候,才能修正。没有运行之前,仅仅做了一层映射,并没有真正处理。
这个映射就是动态库的数据和指令的映射,是一个 section 保存在 Data 区,因为这个区的数据可读写。这个映射就是 got section。
而第二个环节,也就是 Bind 阶段,更多的是 CPU 操作。

Rebase 和 Bind 过后,又到了 Runtime 大显身手的时候了,PC 指令寄存器指向了 Runtime 的指令位置,开始进行 class 的遍历、method 的遍历、load 的执行等等操作。

过后,才是项目的 main 函数的执行,然后是 application 的执行。当然,项目运行起来后的所有 objc_msgsend,都是要走一遍 Runtime 的。也就是 struct 那一套了。

所以 Runtime 是一个动态库,也叫运行时库。这也是我们项目里面要使用 Runtime 的时候,都要先#import <objc/runtime.h>的原因。