基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

最近随着iOS11的正式发布,手淘/天猫也开始逐步用Xcode 9开始编译。在调试过程中,很多同事发现经常报许多API会报线程使用错误的问题。摸索了下,发现是Xcode 9里面带上了一个叫libMainThreadChecker.dylib的动态库,在运行时提供了主线程检查的功能,今天就从探究苹果的实现开始讲起。

0x1 苹果的实现

把苹果的动态库拖入hopper里面看看,基本上扫一眼以后,比较可疑的是__library_initializer__library_deintializer

我看反汇编,第一直觉就是猜,然后都试一把。

我们来看看其伪代码实现,可以分为几个部分来探究:

1.1 环境变量

从图中不难看出,libMainThreadChecker的运行依赖于许多的环境变量,我们可以在Xcode->Scheme->Arguments里面一个个输入这些变量进行测试,我发现比较重要的是MTC_VERBOSE这个参数,使用后,可以输出究竟对于哪些类进行了线程监控。

...
Swizzling class: UIKeyboardEmojiCollectionViewCell
Swizzling class: UIKeyboardEmojiSectionHeader
Swizzling class: UIPrinterSetupPINScrollView
Swizzling class: UIPrinterSetupPINView
Swizzling class: UIPrinterSetupConnectingView
Swizzling class: UICollectionViewTableHeaderFooterView
Swizzling class: UIPrinterSetupDisplayPINView
Swizzling class: UIStatusBarMapsCompassItemView
Swizzling class: UIStatusBarCarPlayTimeItemView
Swizzling class: UIKeyboardCandidateBarCell
Swizzling class: UIKeyboardCandidateBarCell_SecondaryCandidate
Swizzling class: UIActionSheetiOSDismissActionView
Swizzling class: UIKeyboardCandidateFloatingArrowView
Swizzling class: UIKeyboardCandidateGridOverlayBackgroundView
Swizzling class: UIKeyboardCandidateGridHeaderContainerView
Swizzling class: UIStatusBarBreadcrumbItemView
Swizzling class: UIInterfaceActionGroupView
Swizzling class: UIKeyboardFlipTransitionView
Swizzling class: UIKeyboardAssistantBar
Swizzling class: UITextMagnifier
Swizzling class: UIKeyboardSliceTransitionView
Swizzling class: UIWKSelectionView
Swizzled 10717 methods in 384 classes.

可以看出,苹果会在启动前对于这些类进行所谓的线程监控。

1.2 逻辑

看完了输出,我们来看看其中的逻辑实现,如下所示:

CFAbsoluteTimeGetCurrent();
   var_270 = intrinsic_movsd(var_270, xmm0);
   *_indirect__main_thread_checker_on_report = dlsym(0xfffffffffffffffd, "__main_thread_checker_on_report");
   if (objc_getClass("UIView") != 0x0) {
           *_XXKitImage = dyld_image_header_containing_address(objc_getClass("UIView"));
           *_CoreFoundationImage = dyld_image_header_containing_address(_CFArrayGetCount);
           rax = objc_getClass("WKWebView");
           rax = dyld_image_header_containing_address(rax);
           *_WebKitImage = rax;
           *_InlineCallsMachHeaders = *_XXKitImage;
           *0x1ec3e8 = *_CoreFoundationImage;
           *0x1ec3f0 = rax;
           *___CATransaction = objc_getClass("CATransaction");
           *___NSGraphicsContext = objc_getClass("NSGraphicsContext");
           *_SEL_currentState = sel_registerName("currentState");
           *_SEL_currentContext = sel_registerName("currentContext");
           *_MyOwnMachHeader = dyld_image_header_containing_address(___library_initializer);
           *_classesToSwizzle = CFArrayCreateMutable(0x0, 0x200, 0x0);
           var_240 = objc_getClass("UIView");
           _FindClassesToSwizzleInImage(*_XXKitImage, &var_240, 0x2);
           if (*_WebKitImage != 0x0) {
                   var_230 = objc_getClass("WKWebView");
                   *(&var_230 + 0x8) = objc_getClass("WKWebsiteDataStore");
                   *(&var_230 + 0x10) = objc_getClass("WKUserScript");
                   *(&var_230 + 0x18) = objc_getClass("WKUserContentController");
                   *(&var_230 + 0x20) = objc_getClass("WKScriptMessage");
                   *(&var_230 + 0x28) = objc_getClass("WKProcessPool");
                   *(&var_230 + 0x30) = objc_getClass("WKProcessGroup");
                   *(&var_230 + 0x38) = objc_getClass("WKContentExtensionStore");
                   _FindClassesToSwizzleInImage(*_WebKitImage, &var_230, 0x8);
           }
           rcx = CFArrayGetCount(*_classesToSwizzle);
           if (rcx != 0x0) {
                   rax = 0x0;
                   var_278 = rcx;
                   do {
                           var_288 = rax;
                           rax = CFArrayGetValueAtIndex(*_classesToSwizzle, rax);
                           var_258 = rax;
                           rbx = objc_getClass(rax);
                           var_290 = dyld_image_header_containing_address(rbx);
                           var_230 = 0x0;
                           var_280 = rbx;
                           r14 = class_copyMethodList(rbx, &var_230);
                           if (var_230 != 0x0) {
                                   rbx = 0x0;
                                   do {
                                           r13 = *(r14 + rbx * 0x8);
                                           r12 = method_getName(r13);
                                           r15 = sel_getName(r12);
                                           if ((((((((((((((((*(int8_t *)r15 != 0x5f) && (dyld_image_header_containing_address(method_getImplementation(r13)) == var_290)) && (((*(int8_t *)_envIgnoreRetainRelease == 0x0) || (((strcmp(r15, "retain") != 0x0) && (strcmp(r15, "release") != 0x0)) && (strcmp(r15, "autorelease") != 0x0))))) && (((*(int8_t *)_envIgnoreDealloc == 0x0) || ((strcmp(r15, "dealloc") != 0x0) && (strcmp(r15, ".cxx_destruct") != 0x0))))) && (((*(int8_t *)_envIgnoreNSObjectThreadSafeMethods == 0x0) || ((((strcmp(r15, "description") != 0x0) && (strcmp(r15, "debugDescription") != 0x0)) && (strcmp(r15, "self") != 0x0)) && (strcmp(r15, "class") != 0x0))))) && (strcmp(r15, "beginBackgroundTaskWithExpirationHandler:") != 0x0)) && (strcmp(r15, "beginBackgroundTaskWithName:expirationHandler:") != 0x0)) && (strcmp(r15, "endBackgroundTask:") != 0x0)) && (strcmp(r15, "lockFocus") != 0x0)) && (strcmp(r15, "lockFocusIfCanDraw") != 0x0)) && (strcmp(r15, "lockFocusIfCanDrawInContext:") != 0x0)) && (strcmp(r15, "unlockFocus") != 0x0)) && (strcmp(r15, "openGLContext") != 0x0)) && (strncmp(r15, "webThread", 0x9) != 0x0)) && (strncmp(r15, "nsli_", 0x5) != 0x0)) && (strncmp(r15, "nsis_", 0x5) != 0x0)) {
                                                   if (*_userSuppressedClasses != 0x0) {
                                                           rax = CFStringCreateWithCStringNoCopy(0x0, var_258, 0x8000100, *_kCFAllocatorNull);
                                                           var_244 = CFSetContainsValue(*_userSuppressedClasses, rax) != 0x0 ? 0x1 : 0x0;
                                                           CFRelease(rax);
                                                   }
                                                   else {
                                                           var_244 = 0x0;
                                                   }
                                                   if (*_userSuppressedSelectors != 0x0) {
                                                           rax = CFStringCreateWithCStringNoCopy(0x0, r15, 0x8000100, *_kCFAllocatorNull);
                                                           var_250 = rax;
                                                           if (CFSetContainsValue(*_userSuppressedSelectors, rax) != 0x0) {
                                                                   var_244 = 0x1;
                                                           }
                                                           CFRelease(var_250);
                                                   }
                                                   if (*_userSuppressedMethods != 0x0) {
                                                           rax = CFStringCreateWithFormat(0x0, 0x0, @"-[%s %s]");
                                                           var_250 = CFSetContainsValue(*_userSuppressedMethods, rax);
                                                           CFRelease(rax);
                                                           rax = var_250 | var_244;
                                                           if (rax == 0x0) {
                                                                   _addSwizzler(r13, r12, var_258, r15, 0x1);
                                                           }
                                                           else {
                                                                   *_userSuppressionsCount = *_userSuppressionsCount + 0x1;
                                                           }
                                                   }
                                                   else {
                                                           if (var_244 != 0x0) {
                                                                   *_userSuppressionsCount = *_userSuppressionsCount + 0x1;
                                                           }
                                                           else {
                                                                   _addSwizzler(r13, r12, var_258, r15, 0x1);
                                                           }
                                                   }
                                           }
                                           rbx = rbx + 0x1;
                                   } while (rbx < var_230);
                           }
                           _objc_flush_caches(var_280);
                           free(r14);
                           rax = var_288 + 0x1;
                           rcx = var_278;
                   } while (rax != rcx);
           }
           *_totalSwizzledClasses = rcx;
           if (*(int8_t *)_envVerbose != 0x0) {
                   rdx = *_totalSwizzledMethods;
                   fprintf(*___stderrp, "Swizzled %zu methods in %zu classes.\n", rdx, rcx);
           }

代码乍一看很多,其实逻辑非常简单,概述如下:

  • 通过获取UIView的类实体(不理解类实体的去看runtime)所在的地址来反推所在的image(二进制产物,基本是动态库),这里基本能猜测是UIKit
  • UIKit中获取所有继承自UIViewUIApplication的类及其子类(这也是你为什么会在刚刚上文提到的输出中发现UIIBApplication这种不知道啥类的原因),过滤到带_的私有类,然后对剩下的类的所有的方法进行Swizzle。
  • 对于需要Swizzle的方法,要额外判断是不是真正属于UIKit这个动态库的。 比如我们在调试的时候,Xcode会加载libViewDebugging.dylib等不会用于用于线上的动态库,里面会给UIView填上很多奇奇怪怪的方法。
  • 过滤如下的方法,以及以nsli_nsis_开头的方法。

    retain
    release
    autorelease
    .cxx_destruct
    description
    debugDescription
    class
    self
    beginBackgroundTaskWithExpiratonHandler
    beginBackgroundTaskWithName:expirationHandler:
    endBackgroundTask:
    opneGLContext:
    lockFocusIfCanDrawInContext:
    lockFocus
    lockFocusIfCanDraw
    unlockFocus
    
  • 可选,如果还要检查WebKit相关的方法,还可以Hook如下这些类的子类:

    WKWebView
    WKWebsiteDataStore
    WKUserScript
    WKUserContentController
    WKScriptMessage
    WKProcessPool
    WKProcessGroup
    WKContentExtensionStore
    

0x2 自己实现

当时看到这,关于苹果的实现我觉得实在是太简单了,即使不用私有API,结合现在Github上的轮子我自己造一个估计1、2个小时就解决了。现在回想起来,自己还是too simple, sometimes native

大致代码获取UIKitUIViewUIApplication所有子类的代码如下:

NSArray *findAllUIKitClasse()
{
    static NSMutableArray *viewClasses = nil;
    if (!viewClasses) return classes;

    uint32_t image_count = _dyld_image_count();
    for (uint32_t image_index = 0; image_index < image_count; image_index++) {
        const my_macho_header *mach_header = (const my_macho_header *)_dyld_get_image_header(image_index);

        const char *image_name = _dyld_get_image_name(image_index);

        NSString *imageName = [NSString stringWithUTF8String:image_name];
        if ([imageName hasSuffix:@"UIKit"]) {

            unsigned int count;
            const char **classes;
            Dl_info info;

            dladdr(mach_header, &info);
            classes = objc_copyClassNamesForImage(info.dli_fname, &count);

            for (int i = 0; i < count; i++) {
                const char *className = (const char *)classes[i];

                NSString *classname = [NSString stringWithUTF8String:className];
                if ([classname hasPrefix:@"_"]) {
                    continue;
                }

                Class cls = objc_getClass(className);
                Class superCls = cls;

                bool isNeedChild = NO;
                while (superCls != [NSObject class]) {

                    if (superCls == NSClassFromString(@"UIView") || superCls == NSClassFromString(@"UIApplication")) {
                        isNeedChild = YES;
                        break;
                    }
                    superCls = class_getSuperclass(superCls);
                }

                if (isNeedChild) {
                    // 备注:需要在这同时对这个类的方法进行Hook。
                    [viewClasses addObject:cls];
                }
            }

            break;
        }

    return viewClasses;
}

2.1 现有方案Hook的缺陷

到这,我们就只差把这些类的方法都Hook掉就行了。传统的Method Swizzling肯定不行,那样我们需要对每个方法对应实现一个新的方法进行替换,工作量太大。所以我们需要一个思路能够中心重定向整个过程。

之前跟着网易iOS大佬刘培庆学习iOS的时候,了解到了AnyMethodLog,听说能监控所有类所有方法的执行,于是我就直接套用了这个框架,嘿嘿,使用起来真方便,看起来大功告成了,Build & Run

卧槽,怎么运行了就启动崩了,一脸懵逼。

没事,我换个开源库BigBang改改。卧槽,还是崩了。这下必须要开下源码分析下原因了。

AnyMethodLog的实现来看,如下所示:

BOOL qhd_replaceMethod(Class cls, SEL originSelector, char *returnType) {
    Method originMethod = class_getInstanceMethod(cls, originSelector);
    if (originMethod == nil) {
        return NO;
    }
    const char *originTypes = method_getTypeEncoding(originMethod);
    IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    if (qhd_isStructType(returnType)) {
        //Reference JSPatch:
        //In some cases that returns struct, we should use the '_stret' API:
        //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
        //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
        NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:originTypes];
        if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
            msgForwardIMP = (IMP)_objc_msgForward_stret;
        }
    }
#endif

    IMP originIMP = method_getImplementation(originMethod);

    if (originIMP == nil || originIMP == msgForwardIMP) {
        return NO;
    }

    //把原方法的IMP换成_objc_msgForward,使之触发forwardInvocation方法
    class_replaceMethod(cls, originSelector, msgForwardIMP, originTypes);

    //把方法forwardInvocation的IMP换成qhd_forwardInvocation
    class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)qhd_forwardInvocation, "v@:@");

    //创建一个新方法,IMP就是原方法的原来的IMP,那么只要在qhd_forwardInvocation调用新方法即可
    SEL newSelecotr = qhd_createNewSelector(originSelector);
    BOOL isAdd = class_addMethod(cls, newSelecotr, originIMP, originTypes);
    if (!isAdd) {
        DEV_LOG(@"class_addMethod fail");
    }

    return YES;
}

    // 中心重定向函数
void qhd_forwardInvocation(id target, SEL selector, NSInvocation *invocation) {
    NSArray *argList = qhd_method_arguments(invocation);

    SEL originSelector = invocation.selector;

    NSString *originSelectorString = NSStringFromSelector(originSelector);



    [invocation setSelector:qhd_createNewSelector(originSelector)];
    [invocation setTarget:target];

    [invocation invoke];
}

作者的意图比较简单,主要可以概述为如下几点:

  • 把每个类的forwardInvocation,替换成自己实现的一个C函数。
  • 把需要Hook原来selector获取的method的IMP指向objc_msgForward,通过其触发消息转发,也就是触发forwardInvocation;
  • 对每个需要重定向的selector,生成一个特定的格式的新selector,将其IMP指向原来method的IMP。
  • 对于刚刚重定向的C函数,通过NSInvocation获取要调用的target和selector,再次将这个selector生成特定格式的新selector,反射调用。

为啥能把OC的函数forwardInvocation换成C函数,原因就在于只要补上OC函数隐式的前两个参数self, selector,让其的函数签名一致即可。

读到这,看起来没有啥问题吧?为什么会崩溃呢!!

原因在于这种调用方式,缺少了super上下文。

假设我们现在对UIViewUIButton都Hook了initWithFrame:这个方法,在调用[[UIView alloc] initWithFrame:][[UIButton alloc] initWithFrame:]都会定向到C函数qhd_forwardInvocation中,在UIView调用的时候没问题。但是在UIButton调用的时候,由于其内部实现获取了super initWithFrame:,就产生了循环定向的问题。

问题本质的原因是,由于我们对于父类、子类同名的方法都换成了同一个IMP,那么不论是走objc_msgSend抑或是objc_msgSendSuper2,获取到的IMP都是一致的。而在Hook之前,objc_msgSendSuper2拿到的是super_imp, objc_msgSend拿到是imp,从而不会有问题。

2.2 基于桥的全量Hook方法

好,上面的一个小节我们说,如果我们把所有方法都重定向到一个IMP上的时候,就会丧失关于继承关系之间的父子上下文关系,导致重定向循环。所以,我们需要一个思路,能够正确解决上下文的问题。

首先我们来回顾下runtime的消息转发机制:

1. 调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。

2. 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。

3. 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。

4. 调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。

5. 调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。

对于我们来说,我们至少要在第四步之前(确切的是第三步之前),我们就要保留好super上下文。一旦到了forwardInvocation函数,留给我们的又只有self这样的残缺信息了。

哎,我就是卡在这思考了一天,最终我想出了一个思路。

  • 提供一个桩WZQMessageStub,这个桩保留了class和selector,拼接成不一样的函数名,这样就能区分UIButtonUIView的同名initWithFrame:方法,因为不同的selector找到的IMP肯定不一样。
  • NSObject里面实现forwardingTargetForSelector,在消息转发的时候指定把消息全部转发给WZQMessageStub
  • WZQMessageStub实现methodSignatureForSelectorforwardInvocation:方法,承担真正的方法反射调用的职责。

好,思路确定了,难点还剩一个。对于forwardingTargetForSelector这个函数来说,能拿到的参数也是targetselector。在superself调用场景下,这个参数毫无价值,因此我们需要从selector上着手。如果不做任何改变,我们这里拿到的selector肯定是诸如initWithFrame:old selector,所以我们需要在这之前桥一下,可以按照下述流程理解:

每个方法置换到不同的IMP桥上 -> 从桥上反推出当前的调用关系(class和selector-> 构造一个中间态新名字 -> forwardingTargetForSelector(self, 中间态新名字) 

OK,大功告成。具体桥的实现我待会再单独开篇博客讲一讲。

嘿嘿,看起来很简单的任务也学习到了不少新知识。一会把代码开源到Github上。

0x3 遗留问题

我在开启Main Thread Chekcer后,build了一次产物,但是在通过Mach-O文件中Load Commands部分的时候,却没有发现libMainThreadChecker.dylib的踪影,如下所示:

符号断点dlopen也并没有发现这个动态库调用的踪影,所以非常好奇苹果是怎么加载这个动态库的,有大佬知道请赐教。