返回

探秘iOS运行时之旅(二):objc_msgSend与汇编追踪

IOS

前言

在上一篇博客中,我们对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函数的实现原理非常复杂,但其核心思想是:

  1. 根据接收者对象和消息选择器,找到要调用的方法实现。
  2. 将接收者对象和参数压入栈中。
  3. 调用方法实现。
  4. 将返回值从栈中弹出,并返回给调用者。

汇编代码追踪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_sayHellosayHello方法的消息选择器的地址。

然后,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的地址,%raxsayHello方法实现的地址。

sayHello方法实现完成后,objc_msgSend函数会将返回值从栈中弹出,并返回给调用者。代码如下:

addq $16, %rsp
movq %rbp, %rsp
popq %rbp
retq

总结

通过汇编代码追踪objc_msgSend函数的执行过程,我们对iOS运行时的消息发送机制有了更深入的理解。

我们可以看到,消息发送实际上是一个非常复杂的过程,涉及到一系列的内存操作和函数调用。然而,通过汇编代码追踪,我们可以逐行剖析这个过程,并理解它的底层原理。

希望这篇文章能够帮助你更好地理解iOS运行时和消息发送机制。