最近,我們負責開發的一個產品,一啟動就會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到很多相關的文章