c/c++使用位元組時,易出錯點梳理總結

zhangmeng發表於2016-01-07

作為一名 C/C++ 程式設計師,位元組是我們天天都要與之打交道的一個東西,我們甚至和它熟悉到幾乎已經忘記了它存在的地步,不同的人接連掉入位元組埋下的陷阱, 且問題都較為嚴重,有些問題甚至定位需要花費1、2天的時間,甚至不乏一些經驗十分豐富的老員工,這深深引發了我的好奇心,不得不重新認識它、重新學習它。以下總結了幾年來測試過程中使用位元組時的常見問題及收集了一些位元組相關的資料總結了以下內容,文章中若有不妥之處,歡迎大牛指正,大家相互學習,讓產品質量做到更好,大家一起安心喝咖啡。 

一、      什麼是位元組

位元組(Byte)是計算機資訊科技用於計量儲存容量和傳輸容量的一種計量單位,一個位元組等於8位二進位制數。

二、      使用位元組時的易出錯點
1.資料溢位問題

造成資料溢位的原因是資料型別使用不當

舉一個測試中發現的bug看一下資料溢位問題造成的後果

BUG描述:在資料庫中查詢以6513260320690535872為key的某個值,資料庫中找不到這個key,反而多了個3228040640的key 

BUG影響:嚴重,直接影響了來源url,來源關鍵詞的資料,影響幾十萬站長的資料。

BUG分析:第一感覺就是6513260320690535872這個數值太大了,是不是溢位了?於是去程式碼裡看了下,果不其然,是溢位的問題

 

 從程式碼中可以看出pageTrack.page是uint32型別的資料,event->url是uint64位型別的資料,uint32資料型別範圍是0到4294967295;而當event->url等於6513260320690535872並且賦值給pageTrack.page,超出了uint32的最大值, 4294967295。

這樣勢必會導致資料溢位。

上面的數字6513260320690535872太大,不便分析,為了分析方便,下面舉個簡單的例子看一下資料是如何溢位的,以下程式輸出結果為i= 4294967298 、j=2;

把變數i轉換為二進位制後

i: 00000000 00000000 00000000 00000001 00000000 00000000 00000000 00000010

而由於j只有32位儲存空間,只能接收藍色部分資料

00000000 00000000 00000000 00000010,於是轉化為十進位制輸出結果就變成2了。

同一程式內如果程式設計時注意一下很少出現資料溢位問題,造成這種問題的原因主要是兩個程式間傳遞資料,而兩個程式相關的內容是不同的人實現的,負責接收資料模組的開發人員由於對傳送端業務不瞭解,為了減少使用記憶體空間,盲目的定義了數值表示範圍更小的資料型別的變數去接收資料。

 2.主機位元組序與網路位元組序不一致,未進行位元組序一致性轉換帶來的問題

 測試中發現的一個bug:

BUG描述:接收資料時,想得到某一個ip地址的值(10.131.6.202),發現結果是反的

BUG影響:嚴重,資料發給某一廣告系統業務,並經過分析將一些資訊推薦給使用者,ip是廣告中重要的資料,涉及到使用者的地域,地域決定了氣候。

比如冬天,推薦一件衣服,使用者所在地在北方,本應該推薦一件冬季的衣服,然而計算反了的ip所在地在南方,推薦的是一款夏季衣服,廣告精準度可想而知。

BUG原因:伺服器的處理器是intel系列,採用小端位元組排序方式,經過網路傳輸(網路位元組序採用大端位元組序),資料傳輸過程中發生了位序變化。

位元組序問題主要出現在資料在不同平臺之間進行交換時,交換的途徑可能是網路傳輸,也可能是檔案複製。

下面是一些位元組排序的知識:

位元組排序按分為大端和小端

大端(big endian):低地址存放高有效位元組

小端(little endian):低地址存放低有效位元組

現在主流的CPU,intel系列的是採用的little endian的格式存放資料,而motorola系列的CPU採用的是big endian,ARM則同時支援 big和little,

網路程式設計中,TCP/IP統一採用大端方式傳送資料;

大端和小端的方式(下面的例子機器是32位windows的系統,處理器是intel的)

對於一個int型數0x12345678, 為方便說明,這裡採用16進製表示,這個數在不同位元組順序儲存的CPU中儲存順序如下:

高有效位元組——>低有效位元組: 12 34 56 78

低地址位 高低址位

大端: 12 34 56 78

小端: 78 56 34 12
3.位元組對齊使用不當引發的問題

位元組對齊(alignment)是CPU在效能方向所面臨的一個非常重要的問題。有些處理器能自動的處理不對齊資料的訪問,但是,有些處理器卻無法處理。當處理器無法處理對齊問題時,其將引發一個異常(exception),當然從程式的角度來說就是出錯(crash)。

      什麼是對齊?

現代計算機中記憶體空間都是按照byte劃分的,從理論上講似乎對任何型別的變數的訪問可以從任何地址開始,但實際情況是在訪問特定變數的時候經常在特定的記憶體地址訪問,這就需要各型別資料按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。

      為什麼要對齊?

各個硬體平臺對儲存空間的處理上有很大的不同。一些平臺對某些特定型別的資料只能從某些特定地址開始存取。其他平臺可能沒有這種情況, 但是最常見的是如果不按照適合其平臺的要求對資料存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設為 32位)如果存放在偶地址開始的地方,那麼一個讀週期就可以讀出,而如果存放在奇地址開始的地方,就可能會需要2個讀週期,並對兩次讀出的結果的高低位元組進行拼湊才能得到該int資料。顯然在讀取效率上下降很多。這也是空間和時間的博弈。

下面舉個例子說明一下為什麼進行位元組對齊能提高執行效率

在一個32位處理器,指定4位元組對齊的條件下,請看下面的例子

struct

{

char c;

int b;

} align_test;

align_test 記憶體佈局示意圖如下:

 
   

說明:每個表格代表一個位元組的空間

下面看一下采用位元組對齊時和不採用位元組對齊時,cpu對於記憶體的訪問次數有何不同,對於32位處理器,其資料匯流排是32位的,因此,cpu從記憶體中存取資料時是一次讀入4個位元組。

首先看一下采用位元組對齊時的情況,可以看出,當cpu需要分別訪問變數a和變數b時,都只需要分別進行一次記憶體存取(32位處理器, cpu從記憶體中存取資料時是一次讀入4個位元組)。

再來看一下不採用位元組對齊時的情況,可以看出,cpu訪問變數a時,只需進行一次記憶體操作,而變數b則可能需要兩次記憶體操作,因為變數b跨越了4位元組的邊界。說有可能,是因為有這種情況,對b進行訪問之前,可能剛好完成了對a的訪問,對a訪問時,b0,b1,b2也同時讀入(或寫出)了,這種情況下,只需要讀入(或寫入)b3即可。

更為糟糕的是如果b邊界不對齊,還得將其合成一個4位元組(一部分來自一個4位元組中的b0,b1,b2 ,另一部分來自另一個4位元組的b3 ),這又增加了程式的複雜性。

從以上來看,採用位元組對齊能提高cpu存取資料的效能,但凡事有利有弊,在提高cpu存取資料的效能的同時,也需要耗費更多的記憶體空間,這也是空間和時間的博弈。

      位元組對齊使用不當時,容易引發的問題

1) #pragma pack(n)和#pragma pack()注意成對使用,否則可能程式執行時會crash掉。

Bug描述: intel系列處理器上,32位機器正常執行,64位機器每次執行都會crash掉

Bug影響: 嚴重,定位bug耗時2天,及重寫程式碼的慘痛代價

Bug原因:#pragma pack(n)和#pragma pack ()未成對使用,

以下程式碼可以看到只用了#pragma pack(n),而沒在rta名稱空間後使用#pragma pack ()。

#pragma pack(n)等價於#pragma pack(push,n),//指定按n位元組對齊

#pragma pack ()等價於#pragma pack(pop),   //取消指定對齊,恢復預設對齊

2)類體或結構體的合理定義

從結果可以看出為結構體A和B分配的記憶體大小分別是8、12


結構體A在變數b後面進行了1個位元組的補齊

結構體B分別在變數a後面進行了3個位元組的補齊及在變數c後面進行了2個進行的補齊

可以看出同樣的類體A和類體B,只是變數a和變數b一個位置的交換,就多耗費4位元組的記憶體空間,想象一下,如果定義一個B類體的陣列,每個陣列有上億或更大的大小,那麼將浪費很多記憶體資源。

位元組對齊的規則比較多這裡不多說了,推薦一篇文章(不是我寫的,呵呵),如有疑問可以聯絡少特,大家一起交流學習。

http://www.cnblogs.com/repository/archive/2011/01/13/1933721.html

      針對位元組對齊,在程式設計中如何考慮?

如果在程式設計的時候要考慮節約空間的話,那麼我們只需要假定結構的首地址是0,然後各個變數按照上面的原則進行排列即可,基本的原則就是把結構中的變數按照型別大小從小到大宣告,儘量減少中間的填補空間.還有一種就是為了以空間換取時間的效率,我們顯示的進行填補空間進行對齊,比如:有一種使用空間換時間做法是顯式的插入reserved成員:

struct A{

char a;

char reserved[3];//使用空間換時間

int b;

}

reserved成員對我們的程式沒有什麼意義,它只是起到填補空間以達到位元組對齊的目的,當然即使不加這個成員通常編譯器也會給我們自動填補對齊,我們自己加上它只是起到顯式的提醒作用.

程式設計時,位元組使用不當帶來的影響多數是嚴重的,上面的幾個bug任何一個暴漏到現網,損失都是慘重的,災難性的。最後再總結下程式設計中使用位元組時,易出錯點:

1.資料溢位問題的考慮
2.主機位元組序與網路位元組序是否一致性的考慮
3.位元組對齊使用不當引發的問題

1)#pragma pack(n)和#pragma pack()注意成對使用

2)類體或結構體中不同型別變數順序的合理安排

該文章轉載自阿里巴巴技術協會(ata)精選集,作者:少特


相關文章