更新
- 在最新的Xcode 9 Beta版本中,此Bug已被苹果修复:Bug ID 30530580: Crash on “Release” Build Configuration scheme setting for Out Parameters code
二级指针
二级指针,也叫指针的指针,或者Out Parameters,可以用来改变一个指针的地址值,由于在Objective-C里面方法、函数不支持返回多个值,所以经常用二级指针实现这个功能,比如NSFileManager的- (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error方法,就可以让方法在内部创建error后传出。
问题
最近在Debug代码的时候,注释掉了一个带有二级指针参数的方法内部所有代码,然后在Release环境下安装运行,结果居然Crash了,猛然想起好像以前同事也遇到过,仔细检查了下,感觉代码是没有问题的,所以继续深究,新建了一个空的工程,重现了这个EXC_BAD_ACCESS的Crash,代码非常简单,如下:
// 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++代码,如下:
// 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方法:

为了方便分析,把反编译出来的汇编代码分了段:
- 调用
NSMutableArray new创建array变量,保存在r15寄存器上 - 第一次调用
testFunc: - 先
retain,然后release,rbx保存array,r15释放 - 第二次调用
testFunc: - 先
release,再retain,即释放了rbx,然后又retain,这里明显有问题 - 调用
NSlog打印 - 最后release释放array变量
为了进一步验证,用Hopper的反汇编生成Objective-C代码功能进行验证,生成的代码如下:
// 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
总结
汇编面前,没有秘密!=。=