探秘iOS运行时之旅(二):objc_msgSend与汇编追踪
2024-02-12 01:03:21
前言
在上一篇博客中,我们对iOS运行时以及编译时这两个概念进行了介绍。同时,我们了解到Objective-C方法的调用本质上是发送消息,而在底层,这些消息的发送是通过objc_msgSend方法来实现的。那么,objc_msgSend究竟是如何工作的呢?
objc_msgSend的本质
objc_msgSend是一个C语言函数,是Objective-C消息发送的核心实现。它的原型为:
id objc_msgSend(id self, SEL op, ...)
其中,self表示接收消息的对象,op表示要发送的消息,...表示可变参数列表。
当我们调用一个Objective-C方法时,编译器会将该方法调用转换为对objc_msgSend函数的调用。例如,当我们调用如下代码时:
[object doSomething];
编译器会将其转换为:
objc_msgSend(object, @selector(doSomething));
objc_msgSend的实现原理
objc_msgSend函数的实现原理非常复杂,但其核心思想是:
- 根据接收者对象和消息选择器,找到要调用的方法实现。
- 将接收者对象和参数压入栈中。
- 调用方法实现。
- 将返回值从栈中弹出,并返回给调用者。
汇编代码追踪objc_msgSend
为了更深入地理解objc_msgSend的实现原理,我们可以使用汇编代码来追踪它的执行过程。
以下是在X86_64架构下objc_msgSend函数的汇编代码:
objc_msgSend:
pushq %rbp
movq %rsp, %rbp
movq %rdi, %r10 // self
movq %rsi, %r11 // selector
leaq 16(%rbp), %rdi // arguments
movq %r11, %rax
movq %rax, %rcx
shrq $3, %rcx
andq $-8, %rcx
shlq $3, %rcx
addq %rcx, %rax
movq (%rax), %rax
movq %rax, %r8 // method
testq %r8, %r8
jz objc_msgSend_not_found
movq %r10, %rdi // self
movq %r8, %rax // method
callq *%rax
addq $16, %rsp
movq %rbp, %rsp
popq %rbp
retq
objc_msgSend_not_found:
movq $-1, %rax
jmp objc_msgSend_return
objc_msgSend_return:
addq $16, %rsp
movq %rbp, %rsp
popq %rbp
retq
从汇编代码中,我们可以看到objc_msgSend函数的执行过程与我们之前介绍的原理基本一致。
首先,函数将接收者对象和消息选择器压入栈中。然后,它通过计算消息选择器的高位字节来确定方法实现的地址。接着,它将方法实现的地址压入栈中,并调用该方法。
方法调用完成后,函数将返回值从栈中弹出,并返回给调用者。
汇编代码追踪示例
为了更好地理解汇编代码追踪objc_msgSend的过程,我们来看一个简单的示例。
假设我们有一个名为Person
的类,其中有一个名为sayHello
的方法。代码如下:
@interface Person : NSObject
- (void)sayHello;
@end
@implementation Person
- (void)sayHello {
NSLog(@"Hello, world!");
}
@end
现在,让我们使用汇编代码来追踪objc_msgSend函数的执行过程。
首先,我们创建一个Person
对象并调用它的sayHello
方法。代码如下:
Person *person = [[Person alloc] init];
[person sayHello];
编译器会将这段代码转换为如下汇编代码:
movq %rax, %rdi
callq objc_msgSend
其中,%rax寄存器中存储着person
对象的地址。
接下来,objc_msgSend函数会将person
对象和sayHello
方法的消息选择器压入栈中。代码如下:
pushq %rdi // self
movq $L_selector_sayHello, %rsi // selector
其中,L_selector_sayHello
是sayHello
方法的消息选择器的地址。
然后,objc_msgSend函数会计算sayHello
方法实现的地址。代码如下:
leaq 16(%rbp), %rdi // arguments
movq %rsi, %rax
movq %rax, %rcx
shrq $3, %rcx
andq $-8, %rcx
shlq $3, %rcx
addq %rcx, %rax
movq (%rax), %rax
其中,16(%rbp)
是参数的地址,%rsi
是消息选择器的地址。
最后,objc_msgSend函数会调用sayHello
方法的实现。代码如下:
movq %r10, %rdi // self
movq %rax, %rax // method
callq *%rax
其中,%r10
是接收者对象person
的地址,%rax
是sayHello
方法实现的地址。
sayHello
方法实现完成后,objc_msgSend函数会将返回值从栈中弹出,并返回给调用者。代码如下:
addq $16, %rsp
movq %rbp, %rsp
popq %rbp
retq
总结
通过汇编代码追踪objc_msgSend函数的执行过程,我们对iOS运行时的消息发送机制有了更深入的理解。
我们可以看到,消息发送实际上是一个非常复杂的过程,涉及到一系列的内存操作和函数调用。然而,通过汇编代码追踪,我们可以逐行剖析这个过程,并理解它的底层原理。
希望这篇文章能够帮助你更好地理解iOS运行时和消息发送机制。