00Objective-C 消息发送与转发机制原理(二)

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 替换掉):

反编译后的 __CFInitialize() 汇编代码

然而在源码中对应的代码却被删掉啦:

00Objective-C 消息发送与转发机制原理(二)_第1张图片
苹果提供的 __CFInitialize() 函数源码

在早期版本的 CF 源码中,还是可以看到 __CF_forwarding_prep_0和forwarding_prep_1
的声明的,但是不会有实现源码,也没有对objc_setForwardHandler的调用。这些细节从函数调用栈中无法看出,只能逆向工程看汇编指令。但从函数调用栈可以看出 __CF_forwarding_prep_0和 forwarding_prep_1这两个 Forward Handler 做了啥:

00Objective-C 消息发送与转发机制原理(二)_第2张图片
Paste_Image.png

这个日志场景熟悉得不能再熟悉了,可以看出 _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,从函数名都能看得出来。下面是这两个函数的伪代码:

00Objective-C 消息发送与转发机制原理(二)_第3张图片
Paste_Image.png

在 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是最后一个入栈的,位于栈顶:

00Objective-C 消息发送与转发机制原理(二)_第4张图片
Paste_Image.png

消息转发的逻辑几乎都写在 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); 
     
} 

// 调用 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 中实现的伪代码:

00Objective-C 消息发送与转发机制原理(二)_第5张图片
CF 中实现的伪代码

也就是说我们可以 override doesNotRecognizeSelector或者捕获其抛出的异常。在这里还是大有文章可做的。

5、总结

我将整个实现流程绘制出来,过滤了一些不会进入的分支路径和跟主题无关的细节:

00Objective-C 消息发送与转发机制原理(二)_第6张图片
消息发送与转发路径流程图

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/#逆向工程助力刨根问底

你可能感兴趣的:(00Objective-C 消息发送与转发机制原理(二))