[轉載]淺談字節序(Endianness)

一、字節序的起源

在計算機中,字節序(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     /* Our example data structure */
08     struct {
09         char one[4];
10         int  two;
11         char three[4];
12     } data;
13
14     /* Fill our structure with data */
15     strcpy (data.one, "foo");
16     data.two = 0×01234567;
17     strcpy (data.three, "bar");
18
19     /* Write it to a file */
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


Listing 3. hexdump -C output on little-endian machines

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*的這種特性還可以方便的反轉數據順序以適應不同的機器,怎麼編寫這樣的代碼不如讓你來思考一下?

四、參考文獻

1. Endianness. http://en.wikipedia.org/wiki/Endianness

2. 關於Endianness翻譯的討論。 http://shu1tong2wen2.wikia.com/wiki/Endianness

3. Writing Endian-independent Code in C. http://www.ibm.com/developerworks/aix/library/au-endianc/index.html?ca=drs

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章