前言
之前逆向部分的文章基础知识和所需工具已经讲述的差不多了 , 后续准备好实战项目以及汇编和越狱部分内容继续更新, 敬请关注 .
目前准备更新一个底层系列文章 , 从
dyld
加载可执行文件到入口main
函数 , 到类 , 分类 , 协议等等的加载为主线 . 一步步探索底层原理 .本篇文章从方法的本质开始讲述 , 前面少几篇文章 , 后续补上 , 然后会准备一个目录 .
前导知识
Runtime
说到任何关于 OC
本质的东西 , 我们不得不提一下 Runtime
这个东西 .
这里只是简单了解一下 Runtime
, 为我们探索方法本质提供一些帮助 , 后续更新详细的 Runtime
机制和具体使用 .
Runtime 简单介绍
◈ Objective-C
扩展了 C
语言,并加入了面向对象特性和 Smalltalk
式的消息传递机制。而这个扩展的核心是一个用 C
和 编译语言 写的 Runtime
库。它是 Objective-C
面向对象和动态机制的基石和根本。
◈ Objective-C
是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。
◈ 理解 Objective-C
的 Runtime
机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。
Runtime 版本
Runtime
其实有两个版本:'modern'
和'legacy'
。我们现在用的Objective-C 2.0
采用的是现行 ( Modern ) 版的Runtime
系统,只能运行在iOS
和macOS 10.5
之后的 64 位程序中。而macOS
较老的 32 位程序仍采用Objective-C 1
中的(早期) ( Legacy ) 版本的Runtime
系统。这两个版本最大的区别在于 当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。
Runtime
基本是用 C
和汇编写的。你可以在 这里 下到苹果维护的开源代码。Apple
和 GNU
各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。
Runtime API
建议多多阅读 .
Runtime 用处
Runtime
对于我们普通开发者来说主要是根据其动态的机制 , 来实现各种各样的需求 / 效果 . 简单列举一下 :
- 关联对象 ( Objective-C Associated Objects ) 给分类增加属性
- 方法交换 ( Method Swizzling ) 方法添加和替换和
KVO
实现 - 消息转发 ( 热更新 ) 解决Bug ( JSPatch )
- 实现
NSCoding
的自动归档和自动解档 - 实现字典和模型的自动转换 (
MJExtension
) - ...
实际上根据 Runtime
的机制和其提供的 API
, 我们可以自由的运用 从而生成不同的功能 .
简单总结
-
在
C
语言中,将代码转换为可执行程序,一般要经历三个步骤,即编译、链接、运行。在链接的时候,对象的类型、方法的实现就已经确定好了。 -
而在
Objective-C
中 , 由于LLVM
将一些在编译和链接过程中的工作,放到了运行阶段。也就是说,就算是一个编译好的.ipa
包,在程序没运行的时候,也不知道调用一个方法会发生什么。这也为后来大行其道的「热修复」提供了可能 。
这样的设计使 Objective-C
变得灵活,甚至可以让我们在程序运行的时候,去动态修改一个方法的实现。
由此引出我们今天方法本质的探索 -- 消息发送机制
实例对象 - 类对象 - 元类对象
关于这三种对象之前有篇文章里面有较为详细的讲述 , 本篇就不多赘述了 , 本系列文章中会继续更新 类 / 对象的本质 . OC类对象/实例对象/元类解析
方法的本质
探索
说了这么多 , 下面我们去除上帝视角 , 来从零开始一步步探索 OC
方法的完整流程 .
案例
新建一个 Command Line
项目 , 代码如下:
// main.m
#import <Foundation/Foundation.h>
@interface LBObject : NSObject
- (void)eat;
@end
@implementation LBObject
- (void)eat{
NSLog(@"eat");
}
@end
void run(){
NSLog(@"%s",__func__);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
LBObject * obj = [LBObject alloc];
[obj eat]; // OC 方法
run(); // C 函数
}
return 0;
}
编译期 - clang
clang -rewrite-objc main.m -o main.cpp
由于 LLVM
本身就是内置的 clang
, 因此通过该命令我们即可查看编译后,运行前源码转换成了 C
之后的样子 .
打开 main.cpp
, 直接拉到最底下 main
函数实现 .

C 函数与 OC 方法
上图中我们很清楚的看到 , run
函数在编译期就确定了函数调用 以及实现 . 而 OC
方法被编译成调用objc_msgSend
函数. 这也就是我们在 Runtime
所提到的 消息发送机制 .
LLVM
+ Runtime
使用这种做法以此来实现动态的可能 .
因此得出结论 :
OC
方法的本质就是调用 objc_msgSend
等函数 .
为什么说 '等函数' , 因为调用类方法 / 父类方法 都会有不同 . 例如 : objc_msgSendSuper
, objc_msgSend_stret
等等 .
objc_msgSend
通过编译后代码我们看到 objc_msgSend
函数有两个参数 id
, SEL
. id
显然就是操作哪个对象 . 而通过SEL
与 imp
的机制 , 以此实现了动态调用方法的本质 . 我们称这种机制为 消息发送 .
不同方法调用
调用对象实例方法
LBObject *obj = [LBObject alloc];
[obj eat];
// 实例方法调用底层编译
// 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)
objc_msgSend(obj, sel_registerName("eat"));
调用类方法
objc_msgSend(objc_getClass("LBObject"), sel_registerName("eat"));
调用父类实例方法
struct objc_super lbSuper;
lbSuper.receiver = obj;
lbSuper.super_class = [LBSuper class];
// __OBJC2__ 只需 receiver 和 super_class 即可
objc_msgSendSuper(&lbSuper, @selector(sayHello));
调用父类类方法
struct objc_super myClassSuper;
myClassSuper.receiver = [obj class];
myClassSuper.super_class = class_getSuperclass(object_getClass([obj class]));// 元类
objc_msgSendSuper(&myClassSuper, sel_registerName("test_classFunc"));
小提示
使用 objc_msgSend
函数要把校验关闭 , 否则编译就报错了.

objc_msgSend源码分析
重头戏终于来了 .
-
打开
objc4
源码 . 可编译objc4 源码 , 密码 r5v6 . -
搜索
objc_msgSend
, 直接来到objc-msg-arm64.s
的ENTRY _objc_msgSend
中.
在汇编里面,函数的入口格式是 ENTRY
+ 函数名 , 结束是 END_ENTRY
, 我们这里以 arm64
架构为例
汇编源码
objc_msgSend
是使用汇编来写的 , 为什么呢 ? 个人感觉由于以下原因 :
- 限制
C
语言作为静态语言 , 不可能通过一个函数来实现未知参数个数,类型并且跳转到另一个任意的函数指针的需求 .
- 效率
- 高级代码终究需要转换成汇编语言来从而能够被识别 .
- 安全
- 逆向中我们提到过, 为了防止系统函数被
hook
, 我们经常使用汇编来调用方法和实现函数.
- 逆向中我们提到过, 为了防止系统函数被
源码如下 :
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// person - isa - 类
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
//...
代码太长我就不粘全部了 , 大家自己在源码中看 .
流程分析
整个汇编过程具体代码就不带着分析了 , 后续继续更新逆向时会讲汇编部分 , 到时候会好好分析一下寄存器和汇编指令.
简单总结一下 _objc_msgSend
整个汇编代码过程如下:
1️⃣ : 在
objc_msgSend
中分为两部分 ,第一部分是汇编写的查找缓存的流程 . 直到bl __class_lookupMethodAndLoadCache3
时 , 转到 C 函数继续执行 后续的lookUpImpOrForward
流程.2️⃣ : 获取对象真实的
isa
, 非taggedpointer
的isa
是一个联合体 , 使用位域来存储各种信息 , 这个后续笔者会详细讲述 .3️⃣ : 来到
CacheLookup
过程 , 通过指针偏移找到cache_t
,处理bucket
以及内存哈希表处理 , 通过sel
哈希算法之后的key
找到imp
, 找到则返回 , 找不到JumpMiss
.4️⃣ : 继续来到
__objc_msgSend_uncached
->MethodTableLookup
5️⃣ : 调用
bl __class_lookupMethodAndLoadCache3
, 来到慢速查找流程 .
后续就是 C 函数实现的消息查找以及转发流程 , 由于篇幅问题 , 下篇文章继续讲述完整流程 .
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
总结 :
OC
方法的本质如下 :
在编译期由
LLVM
将方法调用编译成调用objc_msgSend
等函数 , 然后在汇编代码执行缓存查找sel
对应的imp
, 找到就会返回调用 , 找不到则进入消息查找和消息转发慢速流程 .