基于桥的全量方法Hook方案 - 探究苹果主线程检查实现
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.
最近随着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
中获取所有继承自UIView
和UIApplication
的类及其子类(这也是你为什么会在刚刚上文提到的输出中发现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
大致代码获取UIKit
中UIView
和UIApplication
所有子类的代码如下:
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上下文。
假设我们现在对UIView
、UIButton
都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,拼接成不一样的函数名,这样就能区分UIButton
和UIView
的同名initWithFrame:
方法,因为不同的selector找到的IMP肯定不一样。 - 在
NSObject
里面实现forwardingTargetForSelector
,在消息转发的时候指定把消息全部转发给WZQMessageStub
。 WZQMessageStub
实现methodSignatureForSelector
和forwardInvocation:
方法,承担真正的方法反射调用的职责。
好,思路确定了,难点还剩一个。对于forwardingTargetForSelector
这个函数来说,能拿到的参数也是target
和selector
。在super
和self
调用场景下,这个参数毫无价值,因此我们需要从selector
上着手。如果不做任何改变,我们这里拿到的selector
肯定是诸如initWithFrame:
的old selector
,所以我们需要在这之前桥一下,可以按照下述流程理解:
每个方法置换到不同的IMP桥上 -> 从桥上反推出当前的调用关系(class和selector)-> 构造一个中间态新名字 -> forwardingTargetForSelector(self, 中间态新名字)
OK,大功告成。具体桥的实现我待会再单独开篇博客讲一讲。
嘿嘿,看起来很简单的任务也学习到了不少新知识。一会把代码开源到Github上。
0x3 遗留问题
我在开启Main Thread Chekcer
后,build了一次产物,但是在通过Mach-O
文件中Load Commands
部分的时候,却没有发现libMainThreadChecker.dylib
的踪影,如下所示:
符号断点dlopen
也并没有发现这个动态库调用的踪影,所以非常好奇苹果是怎么加载这个动态库的,有大佬知道请赐教。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK