0%

iOS stack overflow导致的EXC_BAD_ACCESS崩溃

最近想学习一下mmap的使用,没想到一来就掉坑里了。

测试代码如下:

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
- (void)test_mmap {
__autoreleasing NSError *err = nil;
// self.videoData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ddcada74ee08d06dda5cd13b4117ad30" ofType:@"mp4"] options:0 error:&er];
// self.videoData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ddcada74ee08d06dda5cd13b4117ad30" ofType:@"mp4"] options:NSDataReadingUncached error:&er];
self.videoData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ddcada74ee08d06dda5cd13b4117ad30" ofType:@"mp4"] options:NSDataReadingMappedIfSafe error:&err]; //1
if (err != nil) {
NSLog(@"er:%@", err);
} else {
NSLog(@"videoData:%lu", (unsigned long)self.videoData.length);
}
// long bufferSize = 4096;
long bufferSize = 1000000;
// long bufferSize = 1024 * 1024 - 30 * 1024;
unsigned char buffer[bufferSize];
[self.videoData getBytes:buffer range:NSMakeRange(0, bufferSize)]; //2
NSData *someData = [[NSData alloc] initWithBytes:buffer length:bufferSize];
NSLog(@"someData:%lu", (unsigned long)someData.length);
[self saveDataToCaches:someData];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.videoData = nil;
});
}

- (void)saveDataToCaches:(NSData *)data {
NSString *cachespath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
NSString *fileName = [NSString stringWithFormat:@"%@", NSDate.date];
NSString *filePath = [cachespath stringByAppendingPathComponent:fileName];
NSError *err;
BOOL ret = [data writeToFile:filePath options:NSDataWritingAtomic error:&err];
if (err) {
NSLog(@"写入错误:%@", err);
}
NSLog(@"ret:%d", ret);
}

1处通过mmap的方式读取一个500多兆的视频数据,2处读取视频中某一段的数据。最开始的时候bufferSize设置的是4096,运行的时候一切正常。正常之后自然想改个其他的值试试,于是就设置bufferSize为视频大小,结果一运行就崩溃了很快啊,然后就是提示EXC_BAD_ACCESS。当时觉得很奇怪怎么会EXC_BAD_ACCESS呢?也没看到哪个地方有过度释放的对象啊。最后一点点修改bufferSize的大小,发现小于1000000代码就可以正常运行,而超过则会EXC_BAD_ACCESS。不过依然搞不懂为啥会这样。百思不得其解之后只能求助Google,不过也不知道怎么搜索关键字,一通搜索之后也没找到有用的东西,期间一度想放弃,不过还是坚持了下来。

直接上结论:上述崩溃的原因就是stack overflow了。

我们可以验证一下,打好断点后分别p &errp &cachespath得到:

1
2
3
4
(lldb) p &err
(NSError **) $0 = 0x000000016f5b97d0
(lldb) p &cachespath
(NSString **) $1 = 0x000000016f4c5470

$0 - $1 = 0xF4360 = 1000288 = 0.954M,基本上快占满整个栈空间了(这里有点马后炮了因为很多人可能都不知道主线程的栈空间有多大,甚至都没有栈溢出这个概念自然也不会往这个方面想了)。因此代码可能会崩溃在后面的任意一行代码,具体是哪一行视栈的剩余空间大小。比如有可能崩溃在

1
NSError *err;  Thread 1: EXC_BAD_ACCESS (code=2, address=0x16f4bfad0)

也有可能会崩溃在:

1
NSString *fileName = [NSString stringWithFormat:@"%@", NSDate.date];  Thread 1: EXC_BAD_ACCESS (code=2, address=0x16ebfbae0)

如果平时不太注意栈空间大小的话,出现这样的崩溃很容易让人摸不着头脑。比如崩溃在fileName这一行,但这一行显然是没有什么问题的。遇到这种情况,我们就不要死死盯在这一行代码上了,因为很有可能是其他地方的代码出了问题,而仅仅在这一行体现出了症状(感觉跟人生病有点相似出现症状的地方有可能并不是真正的病因)。这时应该往上仔细查找,边查找边排除掉那些不太可能出现问题的代码,缩小范围,找出那些最有可能出现问题的代码。当然这依然需要你有扎实的基础,比如这里

1
2
long bufferSize = 1000000;
unsigned char buffer[bufferSize];

谁又能想得到是这里申请了太多的栈空间导致后面的随机崩溃呢?EXC_BAD_ACCESS崩溃有些不是必现的这就导致复现很困难,而有些是崩溃的地方是不固定的。这也是EXC_BAD_ACCESS崩溃让人感到可怕的地方。

解决办法:在堆上申请空间。

1
2
long bufferSize = 1000000;
unsigned char * buffer = malloc(bufferSize * sizeof(unsigned char));

最后记得free。

总结:

1.对于大内存变量或者无法预知大小的变量尽量在堆上申请空间而不是在栈上申请空间,始终牢记栈空间是有大小限制的。

2.尽量使用循环而不是递归。
虽然以前并太不认同这一点,劳资就用递归怎么啦。不过现在看来递归确实更容易导致栈溢出,能用循环的时候尽量用循环,确实困难再用递归。

3.iOS主线程栈空间大小为1MiB,子线程栈空间大小为512KiB。

参考

iOS里的栈限制引发的crash

一个有意思的栈溢出crash

觉得文章有帮助可以打赏一下哦!