以前逆向部分的文章基础知识和所需工具已经讲述的差很少了 , 后续准备好实战项目以及汇编和越狱部份内容继续更新, 敬请关注 .git
目前准备更新一个底层系列文章 , 从
dyld
加载可执行文件到入口main
函数 , 到类 , 分类 , 协议等等的加载为主线 . 一步步探索底层原理 .github本篇文章从方法的本质开始讲述 , 前面少几篇文章 , 后续补上 , 而后会准备一个目录 .objective-c
说到任何关于 OC
本质的东西 , 咱们不得不提一下 Runtime
这个东西 .算法
官方文档缓存
这里只是简单了解一下 Runtime
, 为咱们探索方法本质提供一些帮助 , 后续更新详细的 Runtime
机制和具体使用 . 安全
◈ Objective-C
扩展了 C
语言,并加入了面向对象特性和 Smalltalk
式的消息传递机制。而这个扩展的核心是一个用 C
和 编译语言 写的 Runtime
库。它是 Objective-C
面向对象和动态机制的基石和根本。bash
◈ Objective-C
是一个动态语言,这意味着它不只须要一个编译器,也须要一个运行时系统来动态得建立类和对象、进行消息传递和转发。架构
◈ 理解 Objective-C
的 Runtime
机制能够帮咱们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。app
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
对于咱们普通开发者来讲主要是根据其动态的机制 , 来实现各类各样的需求 / 效果 . 简单列举一下 :
KVO
实现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 -rewrite-objc main.m -o main.cpp
因为 LLVM
自己就是内置的 clang
, 所以经过该命令咱们便可查看编译后,运行前源码转换成了 C
以后的样子 .
打开 main.cpp
, 直接拉到最底下 main
函数实现 .
上图中咱们很清楚的看到 , run
函数在编译期就肯定了函数调用 以及实现 . 而 OC
方法被编译成调用objc_msgSend
函数. 这也就是咱们在 Runtime
所提到的 消息发送机制 .
LLVM
+ Runtime
使用这种作法以此来实现动态的可能 .
所以得出结论 :
OC
方法的本质就是调用 objc_msgSend
等函数 .
为何说 '等函数' , 由于调用类方法 / 父类方法 都会有不一样 . 例如 : objc_msgSendSuper
, objc_msgSend_stret
等等 .
经过编译后代码咱们看到 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
函数要把校验关闭 , 不然编译就报错了.
重头戏终于来了 .
打开 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
, 找到就会返回调用 , 找不到则进入消息查找和消息转发慢速流程 .