当前位置:   article > 正文

objc - NSString Category实现+initialize导致EXC_BAD_ACCESS_objc category 的方法闪退

objc category 的方法闪退

Tested on iOS 15.0 ~ 16.0

本文完整的示例项目代码放在小号Github上: iCategory-NSString

背景

最近同事线上发现个BUG,某些用户打开相册后会崩溃,Backtrace定位分析到是向PHAssetCollection.localizedTitle发送消息然后在 objc_msgSend +32EXC_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 了。

重现

重现步骤较简单:

  1. 打开 iPhone/iPad Photos APP,创建几个名字为英文或数字的比较短(字符长度小于11)的相簿Album,比如这里创建一个名叫 'Abc' 的 Album。

  2. 写一个 NSString Category,实现 +initialize 方法

    NSString+Category.h:

    @interface NSString (Category)
    
    @end
    
    • 1
    • 2
    • 3

    NSString+Category.m:

    @implementation NSString (Category)
    
    + (void)initialize {
    	// empty now ...
    }
     
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  3. 写个测试代码,向 PHAssetCollection.localizedTitle发送消息:

     for (PHAssetCollection *collection in userAlbums) {
         NSString *string = collection.localizedTitle;
         NSLog(@">>>>>>>>>>>> album name: %@", [string description]);
     }
    
    • 1
    • 2
    • 3
    • 4
  4. Crash with EXC_BAD_ACCESS:
    在这里插入图片描述

分析

iOS 官方 Photos 库没理由返回个野指针过来吧,这个 0x87a4c650b3180a9e 指针地址64位占满,且最高位是1,像 Tagged Pointer 了,用objc4的相关api来打印来验证一下,确认是否是Tagged Pointer ?

  1. 项目里引入 objc-internal.h (Link),此头文件可从苹果开发者网站获取 objc4 源码 复制出来,注释掉两个宏定义的错误,项目也就可以编译过去了

  2. 因为 objc-internal.h 里较多的inline函数,inline函数在编译时就embed了,编译后是没有这些函数symbol的,lldb里是不能直接调用的,所以我们在ObjcUtil.h/m时Wrap了一层常用的inline函数,方便在lldb里执行

  3. Crash on EXC_BAD_ACCESS 触发时:

	(lldb) p isTaggedPointer(string)
	(bool) $0 = true
	
	(lldb) p [ObjcUtil objcIsTaggedPointer:string]		# call _objc_isTaggedPointer
	(BOOL) $1 = YES
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 确实是 Tagged Pointer String 无疑了,事实上 objc_msgSend 里也是根据地址值小 0 来判断是否为 Tagged Pointer。

  2. 那么打印一下它的值是0x0000000006362413,对照ASCII表,十六进制63代表字母c62代表字母b41代表字母A,最后的3代表长度。'Abc' 这个也是之前创建Album时输入的短名字。

	(lldb) p/x [ObjcUtil objcGetTaggedPointerValue:string]	# _objc_getTaggedPointerValue
	(long) $2 = 0x0000000006362413
	
	(lldb) p [ObjcUtil objcDecodeTaggedPointerString:string]
	(char *) $4 = "Abc"
  • 1
  • 2
  • 3
  • 4
  • 5

验证

上面已经给出结论:在 iOS 15.0 - 15.5(已测试的)版本中, NSString 的任一类别实现了 +initialize,会导致 NSTaggedPointerString 不启用了。

1.

  • 之前写的一篇文章 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

方法 +[ObjcUtil invokeOriginalMethod:selector:] 代码长了点,但只是倒序来遍历方法列表,来拿到原同名方法的函数指针来调用。

这里提一下, +initialize 是类方法,那么它对应的原同名方法,是在元类的方法列表中被找到。

此时,再跑一次上面的测试代码,Crash 消失了。

2.

再写了个测试代码更直观的看一下输出:

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"];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

长度为11及以下的字符串,都会是 NSTaggedPointerString。结果是显而易见的,当我们的 NSString 类别实现了 +initialize 方法,会导致 NSTaggedPointerString 不启用了。

3.

把 Foundation.framework/Foundation 拖进 IDA,等 IDA 分析完,看一下 +[NSString initialize] 的汇编代码,发现这里有 enable/disable string tagged pointer 相关描述及逻辑。

  • iOS 13.3 版本的 Foundation.framework

在这里插入图片描述

  • iOS 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 反编就好。

4.

IDA 只是静态分析看出个大概,具体点还是得 lldb 打 breakpoint 不断 n 来动态分析,先跟踪 +[NSString initialize]
+[ObjcUtil invokeOriginalMethod:selector:] 调用前打个断点

	br set -s Foundation -name "+[NSString initialize]"
  • 1
  • lldb 触发断点后,不断 next ,注意看寄存器的变化,可以看出,这里先查看了系统环境变量有没禁用 String Tagged:
getenv("NSStringDisableTagged");
  • 1
  • 接着调用 <objc/runtime.h> 的api class_setSuperclass,设置 NSTaggedPointerString 的 superclass 为 NSString:
class_setSuperclass(NSTaggedPointerString.class, NSString.class);
  • 1

在这里插入图片描述

  • 接着 next,发现最后调用
CoreFoundation`+[NSTaggedPointerString _setAsTaggedStringClass]
  • 1

在这里插入图片描述

  • 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 了

    声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/509858
推荐阅读
相关标签