1. 程式人生 > >大小端(位元組序)位序

大小端(位元組序)位序

位元組序

    位元組序,又稱端序、尾序,英文單詞為Endian,該單詞來源於於喬納森·斯威夫特的小說《格列佛遊記》,小說中的小人國因為吃雞蛋的問題而內戰,戰爭開始是由於以下的原因:我們大家都認為,吃雞蛋前,原始的方法是打破雞蛋較大的一端。可是當今皇帝的祖父小時候吃雞蛋,一次按古法打雞蛋時碰巧將一個手指弄破了,因此他的父親,當時的皇帝,就下了一道敕令,命令全體臣民吃雞蛋時打破雞蛋較小的一端,違令者重罰。老百姓們對這項命令極為反感。歷史告訴我們,由此曾發生過六次叛亂,其中一個皇帝送了命,另一個丟了王位…關於這一爭端,曾出版過幾百本大部著作,不過大端派的書一直是受禁的,法律也規定該派的任何人不得做官。

1980年,Danny Cohen在其著名的論文"On Holy Wars and a Plea for Peace"中,為平息一場關於位元組該以什麼樣的順序傳送的爭論,而引用了該詞。

在電腦科學領域中,位元組序是指存放多位元組資料的位元組(byte)的順序,典型的情況是整數在記憶體中的存放方式和網路傳輸的傳輸順序。有時候也可以用指位序(bit)。為了更好地理解,先看下面這段小程式,這個程式是把一個包含4位數字的字串轉換為16進位制整數來儲存,16進位制整數的每一個位元組儲存一位數字字元。比如:1234,轉換成16進位制整數0x01020304

程式1清單:

#include<stdio.h>

#include<conio.h>

int main( )

{

char input[4] = {0};

int integer   = 0;

int i;

     printf("/r/n請輸入一個位數,每一位的範圍是從到09/r/n");

for(i = 0; i < 4; i++)

     {

         input[i] = getch();

if(input[i] > '9' || input[i] < '0')

         {

              printf("input error!/r/n");

return 1;

         }

         putch(input[i]);

     }

    getch();

     putch('/n');

for(i = 0; i < 4; i++)

     {

         input[i] = input[i] - '0';

     }

     memcpy((void*)&integer, (void*)input, 4);

     printf("轉換後的進位制數是:0xx/r/n", integer);

     getch();

return 0;

}

現在來分析一下這段程式碼

首先,定義了一個字元陣列input,用來接收使用者輸入的4個數字字元;

第二,把4個字元數字轉換成對應的數字;

第三,把轉換的數字複製到整型變數integer中;

最後在螢幕上列印。

如果在PPC或者ARM的機器上編譯執行這個程式,那麼會在螢幕上打印出結果:0x01020304,這與我們的預期一致;但是在X86的機器上則打印出的結果是:0x04030201。這個令人驚訝的結果正是位元組序問題引起。下面來詳細談談這個問題。

 從計算機誕生之後,就有幾種不同的位元組序,典型的是大端序(big endian)和小端序(little endian),當然還有不常見的混合序(middle endian)。這些用來都是描述多位元組資料在記憶體中的存放方式的。以上面的16進位制數0x01020304為例,在計算機中需要用4個位元組來儲存它,01020304各佔一個位元組。按照人類的計數習慣,最左邊的01,稱之為最高有效位(MSBMost Significant Byte,它具有最高權重);最右邊的04稱之為最低有效位(LSB, Least Significant Byte,他具有最低權重);在計算機中需要用4個位元組來儲存它,其中’01’, ’02’,’03’,’04’各佔一個位元組。大端序的計算機儲存這個數值時,按照從低地址到高地址的順序分別儲存MSB到LSB四個位元組,0x01020304的儲存情況如下圖所示:

[轉]位元組序、位序 而小端序的計算機則以相反的順序來儲存它,如下圖所示: 
[轉]位元組序、位序

可以看到,在小端序的計算機中,0x01020304的儲存順序恰好與上面的程式1中相反,這就是最後輸出結果為0x04030201的原因;大端序的計算機中兩者儲存順序一致,所以列印正確。

   我們可以在VC整合環境中來驗證上面的分析。在VS2005中除錯下面的小程式,在return語句處設定斷點,斷住後開啟記憶體視窗檢視&i處的內容。可以直觀的看到在x86(小端序)的機器上整數的存放方式。

程式2清單:

#include <stdio.h>

int main()

{

     int i= 0x01020304;

     printf("i= %#x/r/n",i);

     return 0;

}


[轉]位元組序、位序

再看看如何才能讓程式1在大端序和小端序的機器上都能正確執行呢?一個辦法就是利用預編譯巨集,針對不同的機器定義定義不同的資料結構。下面是一個例子:

程式3清單:

#include <stdio.h>

#include <conio.h>

typedef union

{

     struct{

#ifdef BIG_ENDIAN

     charmsb;

     charmidb1;

     charmidb2;

     charlsb;

#else

     char lsb;

     char midb2;

     char midb1;

     char msb;

#endif

     }bytes;

     int  var;

}INTEGER;

int main()

{

     int  i          =0;

     char input[5]   ={0};

     INTEGERinteger = {0};

     printf("/r/n請輸入一個位數,每一位的範圍是從到到/r/n");

     for(i= 0; i < 4; i++)

     {

         input[i]= getch();

         if(input[i]> '9' || input[i]< '0')

         {

              printf("inputerror!/r/n");

              return 1;

         }

         putch(input[i]);

     }

    getch();

     putch('/n');

     integer.bytes.msb   =input[0] - '0';

     integer.bytes.midb1= input[1] - '0';

     integer.bytes.midb2= input[2] - '0';

     integer.bytes.lsb   =input[3] - '0';

     printf("轉換後的進位制數是:0xx/r/n", integer.var);

     getch();

     return 0;

}

可以看到,這段程式碼定義了兩套資料結構,通過BIG_ENDIAN這個巨集定義來決定使用哪一套資料結構。這是個笨拙卻有效的方法。下面這個例子則漂亮一些:

程式4清單

#include <stdio.h>

#include <conio.h>

#include <memory.h>

#include <winsock2.h>

int main()

{

     char input[4]= {0};

     int integer   =0;

     int i;

     printf("/r/n請輸入一個位數,每一位的範圍是從到到/r/n");

     for(i= 0; i < 4; i++)

     {

         input[i]= getch();

         if(input[i]> '9' || input[i]< '0')

         {

              printf("inputerror!/r/n");

              return 1;

         }

         putch(input[i]);

     }

    getch();

     putch('/n');

     for(i= 0; i < 4; i++)

     {

         input[i]= input[i] - '0';

     }

     memcpy((void*)&integer,(void*)input, 4);

     integer= ntohl(integer);

     printf("轉換後的進位制數是:0xx/r/n", integer);

     getch();

     return 0;

}

這個程式利用了大端序與人類書寫習慣一致的特點,通過ntohl函式將整數進行轉換。這個函式的功能是將網路序轉換成主機序,在大端機器上它什麼也不做,在小端機器上它會將輸入引數的值轉換成小端序的值。在windows環境下連結時別忘了將Ws2_32.lib庫新增進來。

主機序和網路序

主機序就是指主機的端序。

網路位元組序(網路序)指多位元組資料在網路傳輸中的順序,TCP協議規定網路序是大端序,即高位元組先發送。因此大端序的機器接受到的資料可直接使用,小端序機器則需要轉換後使用。BSD socketAPI中定義了一組轉換函式,用於1632bit整數在網路序和本機位元組序之間的轉換。htonlhtons用於本機序轉換到網路序;ntohlntohs用於網路序轉換到本機序。一般來說,為了保證程式的可移植性,編寫程式碼時,傳送的資料需要使用htonlhtons轉換,接收到的資料要使用ntohlntohs轉換。

注意:不存在對單位元組整數進行轉換的函式”ntohc””htonc”!

位序

位序,一般用於描述序列裝置的傳輸順序。一般說來大部分硬體都是採用小端序(先傳低位),因此,對於一個位元組資料,大部分機器上收發的順序都一樣,不會有問題,這就是為什麼沒有針對單位元組資料的API介面”ntohc””htonc”。當然,也有例外,比如­I2C協議就是採用了大端序。這些細節只有在網路協議的資料鏈路層底端才會碰到,對一般的程式設計師來說很少涉及。

但是在C語言中存在一種特殊的資料結構:位域。它的存在,使得C程式設計師能方便地進行位操作(比如在網路協議中經常出現1bit或者多bit的標示位,它們不是一個完整的位元組)。但同時也引起一些難以察覺的問題,這些問題的根源仍然是前面提到的端序。

與位元組序一樣,一個位元組中的8bit順序在不同端序的機器上並不相同。大端機器上從低地址到高地址順尋分別是msb->lsb,如下圖:

[轉]位元組序、位序 小端序的機器上則正好相反
[轉]位元組序、位序

現代計算機的最小儲存單位是BYTE,無法對bit定址,因此我們無法直接觀察每個位元組內部bit的順序。但是我們仍然可以通過位域來間接觀察位元組內部bit順序,以印證上面的說法。

在C語言中,位域與結構體類似,其語法規定:先宣告的成員位於低地址,後宣告的成員位於高地址。那麼下面的位域中:

typedefstruct OneByte

{

     bt0 : 1;

     bt1 : 1;

     bt2 : 1;

     bt3 : 1;

     bt4 : 1;

     bt5 : 1;

     bt6 : 1;

     bt7 : 1;

}

成員bt0就位於一個位元組中最低地址bit0處,成員bt7就位於一個位元組的最地址bit7處。

我們看看下面的程式。

#include<stdio.h>

typedefstruct OneByte

{

char bt0 : 1;

char bt1 : 1;

char bt2 : 1;

char bt3 : 1;

char bt4 : 1;

char bt5 : 1;

char bt6 : 1;

char bt7 : 1;

} ONE_BYTE;

int main()

{

     ONE_BYTE onebyte = {0};

     onebyte.bt7 = 1;

     printf("onebyte = %#x/r/n", *((unsignedchar *)&onebyte));

return 0;

}

bt7賦值為1後,onebyte在記憶體中是這個樣子的:

[轉]位元組序、位序

而在VC2005中編譯執行的結果如下:

[轉]位元組序、位序