iOS里的栈限制引发的crash

昨天一个开发说,app一启动就崩溃,而且是添加了一个比较变态的测试数据后引发的,调试后发现崩溃在IDL的解析代码里,结果分析测试发现原来问题出现在iOS栈上。

问题分析

代码如下:

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
-(NSData *) readRaw: (uint8_t) t
{
    int32_t len = 0;
    if ((t & 0xe0) == 0xa0) { // FixArray
        len = t & 0x1f;
    } else if ((t & 0xff) == 0xda) { // raw 16
        len = (uint16_t)[inputStream readShort];
    } else if ((t & 0xff) == 0xdb) {// raw 32
        len = (uint32_t)[inputStream readInt];
    }
    if (len == 0) {
        return nil;
    }
    uint8_t buffer[len];
    [inputStream readBytes:buffer length:len];
    return [NSData dataWithBytes:(const char *)&buffer length: len];
}


// read
-(unsigned int) readBytes: (uint8_t *)buff length: (unsigned int) len
{
    if(len <= 0){
        @throw [NSException exceptionWithName:@"PackException" reason:@"args is illegal" userInfo:nil ];
    }
    if (totalSize - mOffset < len) {
        @throw [NSException exceptionWithName:@"EOFException" reason:@"Not enough bytes remain in buffer" userInfo:nil ];
  }
  [mBuffer getBytes:buff range:NSMakeRange(mOffset, len)];//<===崩溃在这里。
  mOffset += len;
    if (mOffset >= GARBAGE_BUFFER_SIZE) {
      [mBuffer replaceBytesInRange:NSMakeRange(0, mOffset) withBytes:NULL length:0];
      mOffset = 0;
        totalSize = [mBuffer length];
  }
  return len;
}

error错误是bad access,而从-(unsigned int) readBytes: (uint8_t *)buff length: (unsigned int) len函数本来来看基本很难看出来,所以需要往上看一个函数调用,从代码本身来看也还算正常,当然细心的人可能会看到这里有个比较危险的代码,没错就是它。

1
2
3
4
5
if (len == 0) {
    return nil;
}
uint8_t buffer[len];      //<====就是它
[inputStream readBytes:buffer length:len];

len的大小其实是不可控的,是由服务端返回的一个值,用来表示msgpack后面的某个字段的数据的长度。而这里使用了栈申请内存,此时就面临万一len大于一定值后,导致申请内存会破坏栈了。

当然在我们申请内存的时候并不会崩溃,毕竟这里还没有开始写操作,而当你调用[mBuffer getBytes:buff range:NSMakeRange(mOffset, len)];方法的时候,就棉铃将数据mBuffer里的数据copy到buff对象里了,此时就会将栈上的数据给破坏掉了,从而导致崩溃。因此也就解释了为什么崩溃是在后面的方法里。

处理结果

既然知道了是栈申请大内存导致栈被破坏掉了而引发的崩溃,所以自然需要将栈内存改成堆内存就可以了。在不大规模修改代码的情况下,便有了下面的fix代码

 uint8_t *buffer = malloc(len+1);
memset(buffer, 0, sizeof(buffer));
[inputStream readBytes:buffer length:len];
return [NSData dataWithBytesNoCopy:(void *)buffer length:len];

为了尽量少的内存拷贝,首选dataWithBytesNoCopy。

结论

那么到底这个栈有多大呢?平时碰到的,google了下,结论是:

iOS上主线程栈大小1MB,其他线程512K,OSX上主线程栈大小8M

stack size limit

更多详细请看:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html

另外说明下,对于不可知大小的内存申请,本来也不应该通过栈申请内存来做数据处理。