赞
踩
Tested on iOS 15.0 ~ 16.0
本文完整的示例项目代码放在小号Github上: iCategory-NSString
最近同事线上发现个BUG,某些用户打开相册后会崩溃,Backtrace定位分析到是向PHAssetCollection.localizedTitle
发送消息然后在 objc_msgSend +32
里 EXC_BAD_ACCESS
了。
Apple Developer Forums - PHCollection localizedTitle crash in iOS 14.2.1 有人说把 NSString Category 的 +initialize
换成 +load
就解决了,这样确实也解决了。但如若是第三方实现了 +initialize
而我们改不了呢?或者有没别的解决方法呢?
Crash 的原因是因为 NSString Category 实现了 +initialize
方法后,会导致 NSTaggedPointerString
不启用了,objc_msgSend
里处理 Tagged Pointer 时,访问了未允许的内存地址, 于是就 EXC_BAD_ACCESS
了。
重现步骤较简单:
打开 iPhone/iPad Photos APP,创建几个名字为英文或数字的比较短(字符长度小于11)的相簿Album,比如这里创建一个名叫 'Abc'
的 Album。
写一个 NSString Category,实现 +initialize
方法
NSString+Category.h:
@interface NSString (Category)
@end
NSString+Category.m:
@implementation NSString (Category)
+ (void)initialize {
// empty now ...
}
@end
写个测试代码,向 PHAssetCollection.localizedTitle
发送消息:
for (PHAssetCollection *collection in userAlbums) {
NSString *string = collection.localizedTitle;
NSLog(@">>>>>>>>>>>> album name: %@", [string description]);
}
Crash with EXC_BAD_ACCESS
:
iOS 官方 Photos 库没理由返回个野指针过来吧,这个 0x87a4c650b3180a9e
指针地址64位占满,且最高位是1,像 Tagged Pointer 了,用objc4的相关api来打印来验证一下,确认是否是Tagged Pointer ?
项目里引入 objc-internal.h
(Link),此头文件可从苹果开发者网站获取 objc4 源码 复制出来,注释掉两个宏定义的错误,项目也就可以编译过去了
因为 objc-internal.h
里较多的inline函数,inline函数在编译时就embed了,编译后是没有这些函数symbol的,lldb里是不能直接调用的,所以我们在ObjcUtil.h/m
时Wrap了一层常用的inline函数,方便在lldb里执行
当 Crash on EXC_BAD_ACCESS
触发时:
(lldb) p isTaggedPointer(string)
(bool) $0 = true
(lldb) p [ObjcUtil objcIsTaggedPointer:string] # call _objc_isTaggedPointer
(BOOL) $1 = YES
确实是 Tagged Pointer String 无疑了,事实上 objc_msgSend
里也是根据地址值小 0 来判断是否为 Tagged Pointer。
那么打印一下它的值是0x0000000006362413
,对照ASCII表,十六进制63
代表字母c
,62
代表字母b
,41
代表字母A
,最后的3
代表长度。'Abc'
这个也是之前创建Album时输入的短名字。
(lldb) p/x [ObjcUtil objcGetTaggedPointerValue:string] # _objc_getTaggedPointerValue
(long) $2 = 0x0000000006362413
(lldb) p [ObjcUtil objcDecodeTaggedPointerString:string]
(char *) $4 = "Abc"
上面已经给出结论:在 iOS 15.0 - 15.5(已测试的)版本中, NSString
的任一类别实现了 +initialize
,会导致 NSTaggedPointerString
不启用了。
之前写的一篇文章 objc - Category中调回主类的同名原方法 里,我们知道 Category 的方法会被 objc runtime 处理并放在方法列表的前面
在这里,App的 +[NSString(Category) initialize]
在前就被调用了,而 Foundation.framework 里的 +[NSString initialize]
就会没有被调用,因为 objc runtime 在方法列表里优先找到了 Category 里的同名方法来调用
那么,我们可以在 +[NSString(Category) initialize]
里主动调一下原方法,来验证 NSTaggedPointerString
是在 +[NSString initialize]
里被启用了的这个想法:
NSString+Category.m:
@implementation NSString (Category)
+ (void)initialize
{
// Call original +initialize method on self (NSString class)
[ObjcUtil invokeOriginalMethod:self selector:_cmd];
}
@end
ObjcUtil.m:
@implementation ObjcUtil // call the origin method in the method list + (void)invokeOriginalMethod:(id)target selector:(SEL)selector { Class clazz = [target class]; Class metaClazz = objc_getMetaClass(class_getName(clazz)); NSLog(@"Category ---> class: %@, metaClass: %@", clazz, metaClazz); // Get the instance method list in class uint instCount; Method *instMethodList = class_copyMethodList(clazz, &instCount); for (int i = 0; i < instCount; i++) { NSLog(@"Category instance selector : %d %@", i, NSStringFromSelector(method_getName(instMethodList[i]))); } // Get the class method list in meta class uint metaCount; Method *metaMethodList = class_copyMethodList(metaClazz, &metaCount); for (int i = 0; i < metaCount; i++) { NSLog(@"Category class selector : %d %@", i, NSStringFromSelector(method_getName(metaMethodList[i]))); } NSLog(@"Category ---> instance method count: %d, class method count: %d", instCount, metaCount); // Call original instance method. Note here take the last same name method as the original method for ( int i = instCount - 1 ; i >= 0; i--) { Method method = instMethodList[i]; SEL name = method_getName(method); IMP implementation = method_getImplementation(method); if (name == selector) { NSLog(@"Category instance method found & call original ~~~"); ((void (*)(id, SEL))implementation)(target, name); // id (*IMP)(id, SEL, ...) break; } } free(instMethodList); // Call original class method. Note here take the last same name method as the original method for ( int i = metaCount - 1 ; i >= 0; i--) { Method method = metaMethodList[i]; SEL name = method_getName(method); IMP implementation = method_getImplementation(method); if (name == selector) { NSLog(@"Category class method found & call original ~~~"); ((void (*)(id, SEL))implementation)(target, name); // id (*IMP)(id, SEL, ...) break; } } free(metaMethodList); } @end
方法 +[ObjcUtil invokeOriginalMethod:selector:]
代码长了点,但只是倒序来遍历方法列表,来拿到原同名方法的函数指针来调用。
这里提一下, +initialize
是类方法,那么它对应的原同名方法,是在元类的方法列表中被找到。
此时,再跑一次上面的测试代码,Crash 消失了。
再写了个测试代码更直观的看一下输出:
NSMutableString *mutableString = [NSMutableString stringWithString:@"1"];
for(int i = 0; i < 16; i++){
NSString *str = [NSString stringWithString:mutableString];
NSLog(@"%@, %p, length: %ld", [str class], str, str.length);
[mutableString appendString:@"1"];
}
A. 实现了 +initialize
时,输出:
__NSCFString, 0x2837b4a60, length: 1 __NSCFString, 0x2837b4a60, length: 2 __NSCFString, 0x2837b4a60, length: 3 __NSCFString, 0x2837b4a60, length: 4 __NSCFString, 0x2837b4a60, length: 5 __NSCFString, 0x2837b4a60, length: 6 __NSCFString, 0x2837b4a60, length: 7 __NSCFString, 0x2837b4a60, length: 8 __NSCFString, 0x2837b4a60, length: 9 __NSCFString, 0x2837b4a60, length: 10 __NSCFString, 0x2837b4a60, length: 11 __NSCFString, 0x2837b4a60, length: 12 __NSCFString, 0x2837b4a60, length: 13 __NSCFString, 0x2837b4a60, length: 14 __NSCFString, 0x28398a460, length: 15 __NSCFString, 0x28398a460, length: 16
B. 没实现了 +initialize
(把它名字改一下就可以了:P),或者实现了+initialize
方法,但在里面调用原同名方法,输出:
NSTaggedPointerString, 0xad8f321d31a11442, length: 1 NSTaggedPointerString, 0xad8f321d31b9945a, length: 2 NSTaggedPointerString, 0xad8f321d29399452, length: 3 NSTaggedPointerString, 0xad8f3205a939946a, length: 4 NSTaggedPointerString, 0xad8f2a85a9399462, length: 5 NSTaggedPointerString, 0xad97aa85a939947a, length: 6 NSTaggedPointerString, 0xb517aa85a9399472, length: 7 NSTaggedPointerString, 0xadb3c1d20d52c38a, length: 8 NSTaggedPointerString, 0xa2b3c1d20d52c382, length: 9 NSTaggedPointerString, 0xac6049c3c61ce39a, length: 10 NSTaggedPointerString, 0x906049c3c61ce392, length: 11 __NSCFString, 0x2819fa280, length: 12 __NSCFString, 0x2819fa280, length: 13 __NSCFString, 0x2819fa280, length: 14 __NSCFString, 0x2817ba580, length: 15 __NSCFString, 0x2817ba580, length: 16
长度为11及以下的字符串,都会是 NSTaggedPointerString
。结果是显而易见的,当我们的 NSString
类别实现了 +initialize
方法,会导致 NSTaggedPointerString 不启用了。
把 Foundation.framework/Foundation 拖进 IDA,等 IDA 分析完,看一下 +[NSString initialize]
的汇编代码,发现这里有 enable/disable string tagged pointer 相关描述及逻辑。
iOS 13.3
版本的 Foundation.frameworkiOS 15.5
版本的 Foundation.framework另外提一下,在 $HOME/Library/Developer/Xcode/iOS\ DeviceSupport/[iOS_Version]/Symbols/System/Library/Frameworks/
目录下已经有Xcode通过 dsc_extractor 工具 extract dyld_shared_cache出来的 frameworks 和 dylibs,不用自己从真机拉下来,直接拉进 IDA 反编就好。
IDA 只是静态分析看出个大概,具体点还是得 lldb 打 breakpoint 不断 n 来动态分析,先跟踪 +[NSString initialize]
:
在 +[ObjcUtil invokeOriginalMethod:selector:]
调用前打个断点
br set -s Foundation -name "+[NSString initialize]"
getenv("NSStringDisableTagged");
<objc/runtime.h>
的api class_setSuperclass
,设置 NSTaggedPointerString
的 superclass 为 NSString
:class_setSuperclass(NSTaggedPointerString.class, NSString.class);
CoreFoundation`+[NSTaggedPointerString _setAsTaggedStringClass]
CoreFoundation
库拖时 IDA,看看 +[NSTaggedPointerString _setAsTaggedStringClass]
做些什么:可以看出,先调用了 _objc_taggedPointersEnabled(void)
(source link) 检查 Tagged Pointer 有没有enable了,若是的话,则调用 _objc_registerTaggedPointerClass(objc_tag_index_t tag, Class _Nonnull cls)
(source link) 来注册 Tagged Pointer Class NSTaggedPointerString
。
知道原理后,那么就可在不回调原方法来启用 NSTaggedPointerString 了,也不怕第三方库的 NSString Category实现了 +initialize 了
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。