1. 程式人生 > >大小端位元組序問題

大小端位元組序問題

閱讀檔案格式文件的時候看到關於位元組序(Byte Order)的要求:

For values which span more than a single byte, the multiple byte ordering followed is that of the Big Endian / Motorola standard. The most significant byte will occur first, the least significant byte last


想起以前在組合語言和數字邏輯的時候也有接觸到一些這個概念,已經有點模糊了,搞不清楚哪個是低位在前哪個是高位在前。後來在Wiki和Google的幫助下也算摸清楚了一些Endianness的概念。

一、位元組序的起源

在計算機中,是資料中單獨的可取地址的亞型(words,bytes和bits)在外部儲存器中儲存的順序。通常在提到四字(ddword)、雙字(dword)和字(word)的時候需要考慮其實際的位元組順序,為了簡便起見它的英文也常常表示為Byte Order。

Endianness這個詞源自1726年Jonathan Swift的名著:Gulliver’s Travels(格列佛遊記),在書中有一個故事,大意是指Lilliput(小人國)的領導下了一道指令,規定其人民在剝水煮蛋時必須從little-end(小的那一端)開始。這個規定惹惱了一群覺得應該要從big-end(大的那一刻)開始剝的人。事情發展到後來,竟然演變成一場紛戰。支援小的那端的人被稱為little-endian,反之則被稱為big-endian(在英語中字尾“-ian”表示“xx人”的意思)。1980年Danny Cohen在他的論文“On Holy Wars and a Plea for Peace”中第一次使用了Big-和Little-這兩個術語,最終它們成為了計算機通過網路與其他計算機連線時所要考慮的極其重要的一個問題。

二、位元組序的種類和其表示

那麼為什麼要引入位元組序呢。我們都知道,計算機儲存中最小的單位是位(bit),而8bit構成一個位元組(byte)。在一個32位的CPU中,字長為32bit,也就是4byte,資料要想存放在記憶體中供CPU讀取和寫入,就需要擁有一定的存放順序。這樣不同的CPU可接受的位元組序有可能不同,那麼在設計硬體和軟體時資料的存放問題也需要分開考慮。

資料都有所謂的“有效位(Significant Bit)”,顧名思義它表示了“資料存放有效的位置”,而位元組序的分類就是依賴於有效位來進行劃分的。在一個位元組當中,資料的有效位的順序已經得到了大多數硬體生產商的共識,那就是最高有效位優先(Most Significant Bit First),例如我們用8位二進位制數來表示十進位制數123為01111011,其第一位的0就是最高有效位,而最後一位的1就是最低有效位,在一個位元組當中,幾乎當前所有的硬體都採用了這種直觀的位元組序。

然而情況在離開了單位元組時就有所不同了。不同的硬體產商對於資料佔據多個位元組時擁有怎樣的位元組序有著不同的理解,具體說來分為以下三類:

  • Big-Endian(大位元組序):最高有效位元組優先,更高的位元組有效位佔據著更低地址的記憶體空間,其在記憶體中的表示與直觀吻合,
  • Little-Endian(小位元組序):最低有效位元組優先,更低的位元組有效位佔據著更低地址的記憶體空間,其在記憶體中的表示與直觀相反,以及
  • Mixed-Endian(混合位元組序)或者Middle-Endian(中位元組序):在16位字(word)中的位元組序與32位字(dword)中的位元組序不相同。這種型別的位元組序較為少見。

一些知名的使用Little-Endian的處理器體系結構包括了:x86、6502、Z80、VAX以及PDP-11,使用Big-Endian的處理器通常是Motorola的處理器,例如:6800、68000、PowerPC(即Macintosh在遷移到x86之前所採用的處理器)以及System/370。這也是為什麼在文章開頭提到的文件中使用Big Endian / Motorola standard這樣的詞彙的原因。

更進一步的,像ARM、PowerPC、Alpha、SPARC V9、MIPS、PA-RISC和IA64等體系結構可以支援可切換的位元組序這樣的特性,這個特性可以提高效率或者簡化網路裝置和軟體的邏輯。這種可切換的位元組序被稱為Bi-Endian,用於硬體上意指計算機或者傳遞資料時可以使用兩種不同位元組序中任意一種的能力。

文字不夠直觀,下面以數值0×0A0B0C0Dh為例說明Big-Endian和Little-Endian在記憶體佈局上的不同:

  • Big-Endian在記憶體中的表示

Big-Endian

increasing addresses  →
... 0Ah 0Bh 0Ch 0Dh ...

在這個例子中,最高有效位元組(MSB)為0Ah,儲存在最低地址的記憶體中;次高有效位為0Bh,儲存在接下來的記憶體中,依此類推。這種位元組序與從左向右的順序讀取十六進位制數值非常類似。

以16位元素大小檢視:

increasing addresses  →
... 0A0Bh 0C0Dh ...

最高有效元素現在儲存的是0A0Bh,接下來的元素儲存0C0Dh.

  • Little-Endian在記憶體中的表示

Little-Endian

increasing addresses  →
... 0Dh 0Ch 0Bh 0Ah ...

在這個例子中,最低有效位元組(LSB)的值為0Dh,儲存在最低地址的記憶體,其他位元組依照位元組有效性的遞增依次存放。

用16位元素大小表示

increasing addresses  →
... 0C0Dh 0A0Bh ...

最低有效16位單元儲存的是值0C0Dh,緊接著儲存值0A0Bh

三、位元組序的重要性及其應用

如前所述,不同硬體的體系結構接受不同位元組序的資料表示,因此當同一個檔案在不同的機器中進行讀取和寫入的時候,其所支援的位元組序就顯得尤為關鍵。設想在x86計算機中將(123888)10寫入二進位制檔案中,由於x86支援Little-Endian,所以該數在檔案中儲存為(00003F1E)16。當在PowerPC計算機中讀取該整數時,由於它支援的是Big-Endian,故讀取的結果將是(16158)10,大相徑庭。

同樣的情況也會出現在網路傳輸當中,當你從支援一種位元組序的機器傳送資料到支援相反位元組序的機器時,將會得到非預期的結果。這種錯誤在網路傳輸當中尤為突出,因為你無法決定傳送你所需檔案機器所支援的位元組序,因為這些機器可能分散在世界各地,不是人為所能控制的。

為了更明確的說明上述問題,考慮下列程式碼:

Listing 1: Example 01 #include <stdio.h>
02 #include <string.h>
03
04 int main (int argc, char* argv[]) {
05     FILE* fp;
06
07    
08     struct {
09         char one[4];
10         int  two;
11         char three[4];
12     } data;
13
14    
15     strcpy (data.one, "foo");
16     data.two = 0×01234567;
17     strcpy (data.three, "bar");
18
19    
20     fp = fopen ("output", "wb");
21     if (fp{
22         fwrite (&data, sizeof (data), 1, fp);
23         fclose (fp);
24     }
25 }

這是一段很簡單的C語言程式碼,作用就是向一個data結構體賦值並且將它寫入檔案當中,從結果Listing 2和Listing 3當中我們就可以看到支援不同位元組序的機器在處理資料時候存在的不同。

Listing 2. hexdump –C output on big-endian machines

00000000 66 6f 6f 00 12 34 56 78 62 61 72 00 |foo..4Vxbar.| 0000000c
00000000 66 6f 6f 00 78 56 34 12 62 61 72 00 |foo.xV4.bar.| 0000000c

注意力好的同學一眼就能發現,在寫整數的時候,資料儲存的順序依賴於不同的機器,而字串卻不受此影響,這是為什麼呢?這就牽涉到位元組序是如何如程式碼進行影響的了。

位元組序並不會影響資料儲存的所有方面,例如對一個整數進行bitwise或者bitshift的操作,你是不需要去注意對應的位元組序的。因為多位元組的順序是由計算機來維護的,對於程式設計師來說,一個整數的最低有效位仍然是最低有效位,最高有效位亦然,並不會由於它在計算機底層儲存模式的改變而影響到有效位的含義。

同樣的,位元組序不會影響到C風格字串在計算機底層的儲存順序,這是為什麼呢?考慮到一個C風格字串的實質是一個包含著許多char的陣列,每一個char在現代計算機中幾乎都是表示計算機中的一個位元組。因此,當讀寫C風格字串時,其最小的元素單位是一個位元組;而且陣列在記憶體單元中地址的排列順序是遞增的,例如定義char str[5];這麼一條語句,假設&str[0]的地址為1000,則&str[1]的地址為1001,依次類推。所以不論從直觀含義或者底層技術來看,字串的儲存都是相對位元組序獨立的,這個特性將應用在接下來的許多小技巧中。

那麼位元組序除了影響到多位元組資料在記憶體中的存放順序以外,在寫程式碼的時候還有什麼需要注意的呢?當對一個數據進行型別轉換的時候,需要記住特定的位元組序很可能影響到型別轉換的結果。假設我們有Listing 4所列的這麼一段程式碼

Listing 4: 強制型別轉換 1 unsigned char endian[2] = {1, 0};

2 short x;

3  
4 x = *(short *endian;

那麼最後得到x的結果是多少呢?是不是簡單的就是endian陣列的第一個元素1呢?答案是錯,x的數值需要根據執行時的環境來決定。讓我們回憶一下C語言的指標指向多大的記憶體以及怎麼去解釋所指的這塊記憶體是由指標所指向的型別來確定的,在上述程式碼中,將endian陣列的首元素指標強制轉換成short *的指標,那麼編譯器在解釋它的時候將不再把它指向的記憶體空間視為1 byte,而是short的長度——2 byte;更重要的是當我們對這個指標解引用的時候將會得到的值會是什麼。再回到上面所提到的字串或者字元陣列在計算機中就是依照陣列順序存放的,那麼這個時候endian陣列佔用了兩個位元組,其記憶體資料為:0100。當該指標強制轉換為指向short的指標並解引用時,計算機將一次讀取兩個位元組,這個時候位元組序就發揮它的影響了。在支援Little-Endian的機器中x的值將是1(讀取為0001),而在支援Big-Endian的機器中x的值就是256(讀取為0100)。因此在對指標進行型別轉換並解引用,特別是在單位元組到多位元組資料的轉換時,要特別注意位元組序是否會使得預期結果出現偏差。

單位元組指標到多位元組指標的轉換其實並不完全像Listing 4所舉例子那樣惱人,它還有其他的用途,例如我們可以使用這個特性在執行時判斷當前計算機所支援的位元組序,這樣可以使得程式設計師在編寫程式碼的時候更加靈活,也使得程式碼更加強健(robust)。基本的思路就是先定義一個int變數1,這個變數在不同的計算機中將有兩種不同的儲存順序:01000000(Little)以及00000001(Big),然後我們將指向這個變數的指標強制轉換為指向字元的指標,再解引用根據它的值是0還是1就可以得出當前機器支援的位元組序的,程式碼很簡單:

Listing 5: 判斷位元組序 1 int i = 1;

2

3 if (*(char*)&i == 0)

4     // Big Endian

5 else

6     // Little Endian

利用char*的這種特性還可以方便的反轉資料順序以適應不同的機器,怎麼編寫這樣的程式碼不如讓你來思考一下?

四、參考文獻

轉載自:http://blog.sina.com.cn/s/blog_4833ae820100jorc.html