最近,我们负责开发的一个产品,一启动就会Crash,但是我们自己在开发机上编译出来的版本确又是正常的。DB不能工作了,很影响我们日常体验开发中的版本,于是组织就派我来解决这个问题了。
第一个猜测,因为最近公司RDM的证书快到期了,于是就怀疑是证书的问题,找了平台那边的同学帮忙查看,确认证书是没有问题。
不过平台那边的编译环境跟我们的开发环境有一点点版本的差异,于是有折腾平台那边的同学帮忙升级环境。结果发现也不是环境的问题。
很自然的就想到了 debug 和 release 版本的问题了,DB版本的都是release,而我们自己开发编译到手机上的都是debug版本。把项目设置修改一下,编译到真机,crash重现。【能重现的bug跑不掉。:)】
找到了问题我就贴下相关的代码。这里有个相当诡异的bug。
1 Byte *bytes = (Byte*)[ipData bytes];
2 //读取总的ip列表组数
3 Byte cIPGroupCount = bytes[0];
4
5 if (cIPGroupCount == 0)
6 {
7 return YES;
8 }
9
10 int idx = sizeof(Byte);
11 for (Byte groupIdx = 0; groupIdx < cIPGroupCount; ++groupIdx)
12 {
13 //先读取一个short位的下发列表类型
14 unsigned short type = NTOHS(*(unsigned short*)(bytes+idx));
15 idx += sizeof(unsigned short);
16
17 //读取当前ip列表组总列表的ip数
18 Byte ipItemCount = (Byte)*(bytes + idx);
19 idx += sizeof(Byte);
20
21 NSMutableArray *ips = [[NSMutableArray alloc] initWithCapacity:ipItemCount];
22 NSMutableArray *ports = [[NSMutableArray alloc] initWithCapacity:ipItemCount];
23
24 for (Byte itemIdx = 0; itemIdx < ipItemCount; itemIdx ++)
25 {
26 //读取ip地址信息(IP 地址字段不需要转换字节序)
27 unsigned int ip = *(unsigned int*)(bytes + idx);
28 idx += sizeof(unsigned int);
29 [ips addObject:[NSNumber numberWithInt:ip]];
30 //读取端口信息
31 unsigned int port = NTOHL(*(unsigned int*)(bytes + idx));
32 idx += sizeof(unsigned int);
33 [ports addObject:[NSNumber numberWithInt:port]];
34 }
35 }
上面的代码其实很简单,就是解析一个2进制格式的数据。这段代码运行也没有问题,但是调整了下顺序后,就导致了 release环境下的crash。 还是先贴代码【只贴变化部分的代码】。
1
2 for (Byte itemIdx = 0; itemIdx < ipItemCount; itemIdx ++)
3 {
4 unsigned int ip = *(unsigned int*)(bytes + idx); //这行代码crash
5 idx += sizeof(unsigned int);
6 //读取端口信息
7 unsigned int port = NTOHL(*(unsigned int*)(bytes + idx));
8 idx += sizeof(unsigned int);
9
10 [ips addObject:[NSNumber numberWithInt:ip]];
11 [ports addObject:[NSNumber numberWithInt:port]];
12 }
把变化的部分加粗显示了,相比上面的代码,只是简单的调换了下代码执行的顺序,没有任何逻辑的修改。有注释的那行代码会crash,xcode给出的错误是字节对齐错误。很郁闷。后来写代码验证了一下,请看下面的分析过程。
整个数据解析部分分两个循环,在循环最外面还有一个字节的读取。于是数据的解析流程如下:
1.【1字节】【读取一个字节的列表总数】
---
2.外循环开始
【2字节】【读取两字节类型信息】
【1字节】【读取一字节的ip总数】
--------
3.内循环开始
*【4字节】【4字节ip地址】
【4字节】【4字节端口号】
当执行上述流程 1 + 2×1(外循环执行一次) + 3×1(内循环执行一次) 的时候,整个偏移量是 (1+2+1+4+4),是4的倍数。这里不管 3(内循环)执行多少次,整个偏移量都是4的倍数。
但是只要 2(外循环)执行次数超过一次,上述流程执行到标记了 “*” 的那一行的时候,偏移量就再也不是4的倍数了。这个时候unsigned int ip = *(unsigned int*)(bytes + idx); 这行代码在relase环境下就会crash。
过程分析完了,我还有一个疑问没有解开,为什么第一段代码在同样的数据下,确没有Crash。我只能猜测是因为一行c的代码间隔执行了一行oc的代码。第2行代码也许是编译器优化导致的。如果对这个问题有研究的同学欢迎交流。
最后给出我现在的解决方案,对于解析这种紧凑格式的2进制数据,在做数据类型转换的时候,最好使用下面的代码来处理,这样就可以避免字节对齐的问题了。
1 //读取ip地址信息(IP 地址字段不需要转换字节序) 2 unsigned int ip = 0; 3 memcpy(&ip, bytes + idx, sizeof(unsigned int)); 4 idx += sizeof(unsigned int);
很奇怪的一个问题,在进行强制数据类型转换的时候,ios平台竟然要求内存字节对齐。而debug环境又不要求。如果两次强制类型转换用oc的代码隔开,release执行又是正确的,所以再次怀疑是xocde在编译的时候,编译器优化导致的。
--------------- 后面的讨论----------
感谢 @springhu 指出错误,需要用memcpy,而不是memccpy【原来我一直理解错了memccpy的用法】。
跟springhu讨论了半天,我们分别单独写了demo工程来模拟上面的case,结果发现在release环境下也并不会crash。问题只出现在我的工程里面。经过一些列的测试,发现这个诡异的问题只出在我的情景代码里面,把解析部分单独封装个函数后,在应用里面调用也是不会出问题的。
unsigned int ip = *(unsigned int*)(bytes + idx); 这种写法理论上是没有任何问题的,在应用里面使用的时候也不需要考虑字节对齐的问题,但是不怕一万,就怕万一啊。就怕编译器好心干坏事。
EXC_ARM_DA_ALIGN 用这个关键字可以google到很多相关的文章