反编译分析Xcode8的Bug, release下连续两次调用有二级指针参数的空方法会Crash

二级指针

二级指针,也叫指针的指针,或者Out Parameters,可以用来改变一个指针的地址值,由于在Objective-C里面方法、函数不支持返回多个值,所以经常用二级指针实现这个功能,比如NSFileManager- (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error方法,就可以让方法在内部创建error后传出。

问题

最近在Debug代码的时候,注释掉了一个带有二级指针参数的方法内部所有代码,然后在Release环境下安装运行,结果居然Crash了,猛然想起好像以前同事也遇到过,仔细检查了下,感觉代码是没有问题的,所以继续深究,新建了一个空的工程,重现了这个EXC_BAD_ACCESS的Crash,代码非常简单,如下:

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
// main.m
#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
@interface TestClass : NSObject
@end
@implementation TestClass
+ (void)runTest {
NSMutableArray *array = [NSMutableArray new];
[TestClass testFunc:&array];
[TestClass testFunc:&array];
NSLog(@"array: %@", array);
}
+ (void)testFunc:(NSMutableArray **)array {
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TestClass runTest];
}
return 0;
}

经测试,在Xcode 8.0、以及最新的8.2.1稳定版下,Release环境中,都会Crash,放到iOS工程里面,模拟器、真机也会。但是,在Xcode7下就没有问题。

分析

源码分析

首先,从代码本身上来看,是没有问题的,带有二级指针的方法testFunc:是空的方法,虽然传进来了NSMutableArray **array,但是没有对其赋值。

runTest方法也只是创建了一个NSMutableArray对象,然后取了指针的指针,调用了两次testFunc:,然后打印,按理来说是没有任何副作用的。

所以,直接从代码上看,发现不了问题的原因。

clang -rewrite-objc分析

直接看代码没有用,那就用clang重写出C++代码分析,命令行对main.m执行clang -rewrite-objc main.m,得到C++重写后的代码,在文件的最后,就能找到testFunc:runTest的C++代码,如下:

1
2
3
4
5
6
7
8
9
10
11
// runTest的C++代码
static void _C_TestClass_runTest(Class self, SEL _cmd) {
NSMutableArray *array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("new"));
((void (*)(id, SEL, NSMutableArray **))(void *)objc_msgSend)((id)objc_getClass("TestClass"), sel_registerName("testFunc:"), &array);
((void (*)(id, SEL, NSMutableArray **))(void *)objc_msgSend)((id)objc_getClass("TestClass"), sel_registerName("testFunc:"), &array);
NSLog((NSString *)&__NSConstantStringImpl__var_folders__5_6lqsnn195jj6pw61j66npcnm0000gn_T_main_5a7341_mi_0, array);
}
// testFunc:的C++代码
static void _C_TestClass_testFunc_(Class self, SEL _cmd, NSMutableArray **array) {
}

从C++代码来看,也没有可能会导致EXC_BAD_ACCESS问题的释放、dealloc之类的代码,继续!

Hopper Disassembler反编译分析

重写出C++代码无法解决,就只能让Hopper出马了,毕竟“汇编面前,没有秘密”=。=,以x86_64架构二进制为例。

先来看testFunc:的:

跟预想的一致,没有任何“有效代码”。

再看runTest方法:

为了方便分析,把反编译出来的汇编代码分了段:

  1. 调用NSMutableArray new创建array变量,保存在r15寄存器上
  2. 第一次调用testFunc:
  3. retain,然后release,rbx保存array,r15释放
  4. 第二次调用testFunc:
  5. release,再retain,即释放了rbx,然后又retain,这里明显有问题
  6. 调用NSlog打印
  7. 最后release释放array变量

为了进一步验证,用Hopper的反汇编生成Objective-C代码功能进行验证,生成的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Xcode8 Release环境编译的x86_64二进制反编译后生成的代码
void +[TestClass runTest](void * self, void * _cmd) {
// 创建
r15 = [NSMutableArray new];
// 第一次testFunc:
[TestClass testFunc:r15];
// retain然后release
rbx = [r15 retain];
[r15 release];
// 第二次testFunc:
[TestClass testFunc:rbx];
// 先release !!!
[rbx release];
// 然后retain
rbx = [rbx retain];
// NSLog打印
NSLog(@"array: %@", rbx);
// 释放
[rbx release];
return;
}

release和retain的顺序错误
Hopper生成的代码跟汇编分析基本一致,至此,可以看出,唯一“不正常”的代码就是第5段的“先release,再retain”,这会导致array变量提前释放,以至于后面再次retain无效,NSLog的时候,出现访问已被释放的变量,于是出现EXC_BAD_ACCESS崩溃。

对比Xcode7

现在已经基本可以确定问题了,但是为什么Xcode7就不会Crash?反编译一探究竟。

相同的代码,Xcode7 Release环境编译出可执行二进制文件,反编译后,用Hopper生成Objective-C代码,然后跟Xcode8的做对比,如下:

可以看出,Xcode8编译后,release和retain的顺序错了,导致了array变量提前被释放,继而EXC_BAD_ACCESS错误。而Xcode7编译的一切正常。

问题原因小结

现在可以大胆猜测,EXC_BAD_ACCESS的原因就是Xcode8的编译错误
在编译上面的代码时,Xcode8不能正确的处理array变量作为二级指针传入testFunc空函数后的release、retain顺序,导致array被提前释放,最终产生EXC_BAD_ACCESS错误。

目前已经给Xcode提了Bug report,等待苹果的回复~

所有的代码、Xcode7和8编译出的二进制可执行文件、Hopper的文件,打包放在了Github上:https://github.com/zekunyan/OutParameterPointerCrashOnXcode8

总结

汇编面前,没有秘密!=。=


当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器