這篇文章是我在csdn博客發布的,因為csdn支持markdown所以打算轉移博客,但是markdown編輯器對linux firefox支持度不是很好,因此放棄csdn博客。暫時先搬到這里,等哪天自己的博客站建好了,好一起搬家。
=============================================================
這兩年一直在寫協議分析和報文填充相關內容。因為PC機是小端(Little Endian),網絡序是大端(Big Endian),在寫代碼的時候必須考慮到大小端轉換的問題,否則網卡或者網絡設備會解析錯誤。網上的總結分析甚少,大部分都還處於糾結於大小尾分辨的層次。今天我就來深度分析一下大小尾問題。
首先,不管是哪一種數據存放方式,對於一個單字節的數據,存儲方式都是一樣的。比如uint8_t類,char類在大端和小端都可以不經過轉換直接讀取。
而對於兩字節及以上字節大小的數據,大小端的存儲方式就有區別了。大端(網絡序)的存儲方式與我們的閱讀邏輯是相同的。大端的存儲方式是高位保存在低地址中,小端(常見的PC機)的存儲方式則是高位存高地址。
舉個例子就明白了,比如有個數字0x12345678,存儲到內存至少需要4字節,按照大端的存儲方式,則是0x12 0x34 0x56 0x78,與我們的閱讀邏輯一致;而小端的存儲方式則是0x78 0x56 0x34 0x12。記住大端存儲方式(網絡序)與我們的閱讀邏輯相同就可以了。
那么根據上面的分析,我們就會發現,大小端基於內存的顛倒是以字節為單位的,不是以每一位為單位的,這一點在具體的代碼實現中尤其重要。
假定我們有一個uint16_t類型的數據,在小端機器上面處理(現在的pc機基本都是小端機)需要轉換成大端怎么轉換呢?前一字節與后一字節位置換。
假定我們有一個uint32_t類型的數據,在小端機器上面處理(現在的pc機基本都是小端機)需要轉換成大端怎么轉換呢?前一字節與后一字節置換,中間兩個字節也互相置換。
那么,再進一步,如果有一個結構體,大小32位(4字節),定義如下:
struct test_u32{
uint8_t a;
uint8_t b;
uint8_t c;
uint8_t d;
};
在大小端轉換時,自然是a和d互換,b和c互換。因此,完全可以在小端定義結構體時,就定義成如下結構:
struct test_u32{
uint8_t d;
uint8_t c;
uint8_t b;
uint8_t a;
};
這樣在進行網絡通信時就能省去了大小端轉換這個步驟,在大小端轉換較多的情況下,通過改變結構體定義的方式盡可能省去大小端轉換的步驟,對於數據分析和網絡傳輸的速率提升是明顯的。
那么接下來再考慮一下情況,如果上述結構體在大端機定義如下:
struct test_u32_2{
uint8_t a;
uint16_t b;
uint8_t c;
};
如果只是在小端機定義成如下形式,會有什么影響嗎?
struct test_u32_2{
uint8_t c;
uint16_t b;
uint8_t a;
};
明顯的,a和c由於他們的定義不大於1字節,因此a和c的數據不會有錯。而由於b大於1字節了,所以仍然需要進行大小端轉換。這里就是一個陷阱。
另外在實際的網絡私有協議定義中,不可能所有的數據都是基於8位(1字節)對齊,有些數據可能只占1位,有些數據可能占35位,遇到這種情況該如何處理呢?
首先,如果是如下情況,n個變量瓜分一個字節:
struct test_u8{
uint8_t a:1;
uint8_t b:3;
uint8_t c:4;
};
此時變量a占1位,變量b占3位,變量c占4位,他們共享一個字節。一個字節是8位,在內存中的存儲方式是“abbbcccc”(每個字符代表該位存儲着哪個變量的數據)
對於這種情況,可以直接在小端定義中進行置換:
struct test_u8{
uint8_t c:4;
uint8_t b:3;
uint8_t a:1;
};
在小端如此定義,內存中的數據存儲方式依然是abbbcccc。
那么再討論一個問題,如果在網絡序機器中定義如下結構體,小端中也可以如上一例子直接置換嗎?
struct test_u16{
uint16_t a:3;
uint16_t b:1;
uint16_t c:12;
};
這個時候我會想當然的認為可以置換成如下形式,但是打印結果告訴我出錯了。
struct test_u16{
uint16_t c:12;
uint16_t b:1;
uint16_t a:3;
};
比如我在置換后的結構體中,令a=1,然后按照uint16_t類型輸出該結構體數據,按照預想情況應該是0x2000,但是實際打印數據是0x0100。根據打印結果推原因,出錯理由如下。
1.在大端中,數據保存方式應當如下:aaabcccc cccccccc。那么如果a=1,b=0,c=0,數據當是0010000 00000000,也就是0x2000。但是現在存儲內容成了00000001 00000000,變成了0x0100,那么此時在內存中,數據保存形式為xxxxxaaa xxxxxxxx (x表示未知是b還是c的存儲位),經過測試,如此錯誤定義結構體,使得本來的數據存儲方式變為了ccccbaaa cccccccc 。為什么?這個需要自己畫圖想一下,注意大小端的轉換是以字節為單位轉換而不是以位為單位轉換的。
因此需要修改定義為:
struct test_u16{
uint8_t c_1:4;
uint8_t b:1;
uint8_t a:3;
uint8_t c_2;
};
這時令a=1,b=0,c_1=0,c_2=0,結構體安裝uint16_t輸出便是0x2000。 結論:在大小端處理時遇到不是整自己的變量,定義結構體以uint8_t為單位定義,對於超過一字節又不足兩字節的變量,要拆成兩部分處理。
