您的当前位置:首页正文

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

来源:华拓网

4. 逆向工程助力刨根问底

汇编语言还是比较好理解的,红色标出的那三个指令就是把 __CF_forwarding_prep_0
forwarding_prep_1
作为参数调用 objc_setForwardHandler
方法(那么之前那两个 DefaultHandler 卵用都没有咯,反正不出意外会被 CF 替换掉):

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

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

苹果提供的 __CFInitialize() 函数源码

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

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,从函数名都能看得出来。下面是这两个函数的伪代码:

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

Paste_Image.png
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 中实现的伪代码:

CF 中实现的伪代码

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

5、总结

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

消息发送与转发路径流程图

6、参考文献