6

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

 3 years ago
source link: http://satanwoo.github.io/2017/09/24/mainthreadchecker1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

0x1 苹果的实现

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

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

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

1.1 环境变量

tram_1.png?raw=true

从图中不难看出,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

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

tram_2.png?raw=true

没事,我换个开源库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的踪影,如下所示:

tram_4.png?raw=true

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK