ios平台上一个由字节对齐问题导致的crash

Posted on 2013-01-11 12:26  啊夏  阅读(5481)  评论(0编辑  收藏  举报
最近,我们负责开发的一个产品,一启动就会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到很多相关的文章