大小端(位元組序)位序
位元組序
位元組序,又稱端序、尾序,英文單詞為Endian,該單詞來源於於喬納森·斯威夫特的小說《格列佛遊記》,小說中的小人國因為吃雞蛋的問題而內戰,戰爭開始是由於以下的原因:我們大家都認為,吃雞蛋前,原始的方法是打破雞蛋較大的一端。可是當今皇帝的祖父小時候吃雞蛋,一次按古法打雞蛋時碰巧將一個手指弄破了,因此他的父親,當時的皇帝,就下了一道敕令,命令全體臣民吃雞蛋時打破雞蛋較小的一端,違令者重罰。老百姓們對這項命令極為反感。歷史告訴我們,由此曾發生過六次叛亂,其中一個皇帝送了命,另一個丟了王位…關於這一爭端,曾出版過幾百本大部著作,不過大端派的書一直是受禁的,法律也規定該派的任何人不得做官。
在電腦科學領域中,位元組序是指存放多位元組資料的位元組(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請輸入一個位數,每一位的範圍是從到0到9/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個位元組來儲存它,’01’, ’02’,’03’,’04’各佔一個位元組。按照人類的計數習慣,最左邊的’01’,稱之為最高有效位(MSB,Most 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中定義了一組轉換函式,用於16和32bit整數在網路序和本機位元組序之間的轉換。htonl,htons用於本機序轉換到網路序;ntohl,ntohs用於網路序轉換到本機序。一般來說,為了保證程式的可移植性,編寫程式碼時,傳送的資料需要使用htonl、htons轉換,接收到的資料要使用ntohl、ntohs轉換。
注意:不存在對單位元組整數進行轉換的函式”ntohc”和”htonc”!
位序
位序,一般用於描述序列裝置的傳輸順序。一般說來大部分硬體都是採用小端序(先傳低位),因此,對於一個位元組資料,大部分機器上收發的順序都一樣,不會有問題,這就是為什麼沒有針對單位元組資料的API介面”ntohc”和”htonc”。當然,也有例外,比如I2C協議就是採用了大端序。這些細節只有在網路協議的資料鏈路層底端才會碰到,對一般的程式設計師來說很少涉及。
但是在C語言中存在一種特殊的資料結構:位域。它的存在,使得C程式設計師能方便地進行位操作(比如在網路協議中經常出現1bit或者多bit的標示位,它們不是一個完整的位元組)。但同時也引起一些難以察覺的問題,這些問題的根源仍然是前面提到的端序。
與位元組序一樣,一個位元組中的8個bit順序在不同端序的機器上並不相同。大端機器上從低地址到高地址順尋分別是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中編譯執行的結果如下: