OC resolveInstanceMethod 执行两次原因分析(反汇编)
2024-02-02 02:53:15
前言
在 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”。