4. 逆向工程助力刨根问底
重头戏在于对 objc_setForwardHandler的调用,以及之后的消息转发调用栈。这回不是在 Objective-C Runtime (libobjc.dylib)中啦,而是在 CoreFoundation(CoreFoundation.framework)中。虽然 CF 是开源的,但有意思的是苹果故意在开源的代码中删除了在 CFRuntime.c 文件 __CFInitialize()中调用objc_setForwardHandler的代码。__CFInitialize()函数是在 CF runtime 连接到进程时初始化调用的。从反编译得到的汇编代码中可以很容易跟 C 源码对比出来,我用红色标出了同一段代码的差异。
汇编语言还是比较好理解的,红色标出的那三个指令就是把 __CF_forwarding_prep_0
和forwarding_prep_1
作为参数调用 objc_setForwardHandler
方法(那么之前那两个 DefaultHandler 卵用都没有咯,反正不出意外会被 CF 替换掉):
然而在源码中对应的代码却被删掉啦:
在早期版本的 CF 源码中,还是可以看到 __CF_forwarding_prep_0和forwarding_prep_1
的声明的,但是不会有实现源码,也没有对objc_setForwardHandler的调用。这些细节从函数调用栈中无法看出,只能逆向工程看汇编指令。但从函数调用栈可以看出 __CF_forwarding_prep_0和 forwarding_prep_1这两个 Forward Handler 做了啥:
这个日志场景熟悉得不能再熟悉了,可以看出 _CF_forwarding_prep_0函数调用了forwarding函数,接着又调用了 doesNotRecognizeSelector方法,最后抛出异常。**但是靠这些是无法说服看客的,还得靠逆向工程反编译后再反汇编成伪代码来一探究竟,刨根问底。
**__CF_forwarding_prep_0和 forwarding_prep_1函数都调用了forwarding只是传入参数不同。
forwarding有两个参数,第一个参数为将要被转发消息的栈指针(可以简单理解成 IMP),第二个参数标记是否返回结构体。__CF_forwarding_prep_0第二个参数传入 0
,forwarding_prep_1传入的是 1,从函数名都能看得出来。下面是这两个函数的伪代码:
在 x86_64架构中,rax寄存器一般是作为返回值,rsp寄存器是栈指针。在调用objc_msgSend函数时,参数 arg0(self), arg1(_cmd), arg2, arg3, arg4, arg5分别使用寄存器 rdi, rsi, rdx, rcx, r8, r9的值。在调用 objc_msgSend_stret时第一个参数为 st_addr,其余参数依次后移。为了能够打包出 NSInvocation
实例并传入后续的forwardInvocation:方法,在调用 forwarding
函数之前会先将所有参数压入栈中。因为寄存器 rsp为栈指针指向栈顶,所以 rsp的内容就是 self啦,因为 x86_64是小端,栈增长方向是由高地址到低地址,所以从栈顶往下移动一个指针需要加0x8(64bit)。而将参数入栈的顺序是从后往前的,也就是说 arg0是最后一个入栈的,位于栈顶:
消息转发的逻辑几乎都写在 forwarding函数中了,实现比较复杂,反编译出的伪代码也不是很直观。我对 arigrant.com 的结果完善如下:
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// 调用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass,@selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwarding != receiver) {
if (isStret == 1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 僵尸对象
const char *className = class_getName(receiverClass);
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}
// 调用 methodSignatureForSelector 获取方法签名后再调用forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
}
else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
SEL *registeredSel = sel_getUid(selName);
// selector 是否已经在 Runtime 注册过
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
}
// doesNotRecognizeSelector
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
// The point of no return.
kill(getpid(), 9);
}
这么一大坨代码就是整个消息转发路径的逻辑,概括如下:
- 1、先调用 forwardingTargetForSelector方法获取新的 target 作为 receiver 重新执行 selector,如果返回的内容不合法(为 nil或者跟旧 receiver 一样),那就进入第二步。
- 2、调用 methodSignatureForSelector
获取方法签名后,判断返回类型信息是否正确,再调用 forwardInvocation
执行 NSInvocation对象,并将结果返回。如果对象没实现methodSignatureForSelector
方法,进入第三步。 - 3、调用 doesNotRecognizeSelector方法。
doesNotRecognizeSelector之前其实还有个判断 selector 在 Runtime 中是否注册过的逻辑,但在我们正常发消息的时候不会出此问题。但如果手动创建一个 NSInvocation对象并调用 invoke,并将第二个参数设置成一个不存在的 selector,那就会导致这个问题,并输入日志 “does not match selector known to Objective C runtime”。
较真儿的读者可能会有疑问:何这段逻辑判断干脆用不到却还存在着?难道除了 __CF_forwarding_prep_0和forwarding_prep_1函数还有其他函数也调用 forwarding么?莫非消息转发还有其他路径?其实并不是!
原因是 forwarding调用了 invoking函数,所以上面的伪代码直接把 invoking函数的逻辑也『翻译』过来了。除了forwarding函数,以下方法也会调用invoking函数:
-[NSInvocation invoke]
-[NSInvocation invokeUsingIMP:]
-[NSInvocation invokeSuper]
doesNotRecognizeSelector方法其实在 libobj.A.dylib 中已经废弃了,而是在 CF 框架中实现,而且也不是开源的。从函数调用栈可以发现 doesNotRecognizeSelector之后会抛出异常,而 Runtime 中废弃的实现知识打日志后直接杀掉进程(__builtin_trap())。下面是 CF 中实现的伪代码:
也就是说我们可以 override doesNotRecognizeSelector或者捕获其抛出的异常。在这里还是大有文章可做的。
5、总结
我将整个实现流程绘制出来,过滤了一些不会进入的分支路径和跟主题无关的细节:
6、参考文献
Why objc_msgSend Must be Written in Assembly
Hmmm, What’s that Selector?
A Look Under the Hood of objc_msgSend()
Printing Objective-C Invocations in LLDB
Objective-C
Runtime
Message Forwarding
Messaging
http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/#逆向工程助力刨根问底