返回

OC resolveInstanceMethod 执行两次原因分析(反汇编)

IOS

前言

在 Objective-C 中,resolveInstanceMethod 是一种动态决议机制,允许在运行时为类添加新的方法实现。然而,在某些情况下,resolveInstanceMethod 可能会执行两次,导致程序行为异常。本文将通过反汇编和对 dyld 动态决议流程的分析,深入探讨此问题的原因并提供解决方案。

问题重现

让我们创建一个简单的 Objective-C 类来重现此问题:

#import <objc/runtime.h>

@interface MyClass : NSObject

@end

@implementation MyClass

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod called");
    return NO;
}

@end

int main() {
    MyClass *myObject = [[MyClass alloc] init];
    [myObject performSelector:@selector(unknownMethod)];
    return 0;
}

运行此程序将输出:

resolveInstanceMethod called
resolveInstanceMethod called
dyld: lazy symbol binding failed: Symbol not found: _OBJC_CLASS_$_MyClass
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MyClass unknownMethod]: unrecognized selector sent to instance 0x102b67990'

从输出中可以看到,resolveInstanceMethod 被执行了两次,并导致了 dyld 符号绑定失败和程序崩溃。

反汇编分析

为了了解 resolveInstanceMethod 执行两次的原因,我们使用 Hopper 反汇编器对程序进行了反汇编。在 main 函数中,我们找到了以下汇编代码:

pushq   %rbp
movq    %rsp, %rbp
subq    $16, %rsp
movq    %rdi, %rax
callq   0x100002c48              ; objc_msgSend

objc_msgSend 调用位于 0x100002c48 处的函数。让我们跳到该地址并继续反汇编:

0x100002c48 <objc_msgSend>:
  100002c48:   48 89 f8                   mov    %rdi,%rax
  100002c4b:   48 89 d1                   mov    %rdx,%rcx
  100002c4e:   48 83 78 18 00             cmpq   $0x18,%rax
  100002c53:   77 16                       ja     0x100002c6b
  100002c55:   48 89 48 f8                mov    %rcx,-0x8(%rax)
  100002c59:   48 89 d7                   mov    %rdx,%rdi
  100002c5c:   48 8b 00                   mov    (%rax),%rax
  100002c5f:   48 83 c4 18                addq   $0x18,%rsp
  100002c63:   ff e0                       jmp    *%rax
  100002c65:   90                       nop
  100002c66:   90                       nop
  100002c67:   90                       nop
  100002c68:   90                       nop
  100002c69:   90                       nop
  100002c6a:   90                       nop
  100002c6b:   48 83 ec 10                subq   $0x10,%rsp
  100002c6f:   48 89 48 f0                mov    %rcx,-0x10(%rax)
  100002c73:   48 89 d6                   mov    %rdx,%rsi
  100002c76:   48 83 ec 08                subq   $0x8,%rsp
  100002c7a:   48 89 58 08                mov    %rbx,0x8(%rax)
  100002c7e:   48 89 c3                   mov    %rax,%rbx
  100002c81:   48 8b 00                   mov    (%rax),%rax
  100002c84:   48 83 c4 20                addq   $0x20,%rsp
  100002c88:   55                       push   %rbp
  100002c89:   48 89 e5                   mov    %rsp,%rbp
  100002c8c:   48 83 ec 20                subq   $0x20,%rsp
  100002c90:   48 89 78 10                mov    %rdi,0x10(%rax)
  100002c94:   48 89 70 08                mov    %rsi,0x8(%rax)
  100002c98:   48 89 58 08                mov    %rbx,0x8(%rax)
  100002c9c:   48 89 48 10                mov    %rcx,0x10(%rax)
  100002ca0:   48 89 50 18                mov    %rdx,0x18(%rax)
  100002ca4:   48 8b 48 10                mov    0x10(%rax),%rcx
  100002ca8:   48 8b 50 18                mov    0x18(%rax),%rdx
  100002cac:   48 8b 40 08                mov    0x8(%rax),%rax
  100002cb0:   48 83 c4 20                addq   $0x20,%rsp
  100002cb4:   5d                       pop    %rbp
  100002cb5:   ff e0                       jmp    *%rax
  100002cb7:   90                       nop
  100002cb8:   90                       nop
  100002cb9:   90                       nop
  100002cba:   90                       nop
  100002cbb:   90                       nop
  100002cbc:   90                       nop

我们可以看到,当消息发送到未知选择器时(在这种情况下为 unknownMethod),objc_msgSend 会跳转到 0x100002c6b 处的代码。此代码执行以下操作:

  • 将消息接收者(self)和选择器(@selector(unknownMethod))压入堆栈。
  • 调用 resolveInstanceMethod 方法来查找该选择器的实现。
  • 如果 resolveInstanceMethod 返回 NO(表示找不到实现),则程序将崩溃,因为它尝试调用一个不存在的方法。

反汇编结果表明,resolveInstanceMethod 确实被调用了两次。第一次是在 0x100002c55 处,将消息接收者压入堆栈。第二次是在 0x100002c6f 处,将选择器压入堆栈。

dyld 动态决议流程

为了进一步理解 resolveInstanceMethod 执行两次的原因,我们需要了解 dyld 动态决 resolution 过程。dyld 负责在运行时加载和链接Objective-C 类和方法。

当一个Objective-C程序启动时,dyld 会遍历所有已加载的类并查找对未知选择器的引用。如果找到这样的引用,dyld 会调用 resolveInstanceMethod 方法来查找该选择器的实现。

如果 resolveInstanceMethod 返回一个非 NULL 值,则 dyld 会将该实现链接到该类。此过程称为“动态决 resolution”。