字節序(byte order)和位序(bit order)
在網絡編程中經常會提到網絡字節序和主機序,也就是說當一個對象由多個字節組成的時候需要注意對象的多個字節在內存中的順序。
以前我也基本只了解過字節序,但是有一天當我看到ip.h中對IP頭部結構體struct iphdr的定義時,我發現其中竟然對一個字節中的8個比特位也區分了大小端,這時我就迷糊了,不是說大小端只有在多個字節之間才會有區分的嗎,為什么這里的定義卻對一個字節中的比特位也區分大小端呢?
下面我們先看一下struct iphdr的定義,后文會解惑為什么要在一個字節中區分大小端。
struct iphdr { #if defined(__LITTLE_ENDIAN_BITFIELD) __u8 ihl:4, version:4; #elif defined (__BIG_ENDIAN_BITFIELD) __u8 version:4, ihl:4; #else #error "Please fix <asm/byteorder.h>" #endif __u8 tos; __be16 tot_len; __be16 id; __be16 frag_off; __u8 ttl; __u8 protocol; __sum16 check; __be32 saddr; __be32 daddr; /*The options start here. */ };
- 字節序(Byte order)
關於字節序的文章已經有很多了,在我這篇文章中不打算過多的說字節序,但是也不能完全脫離字節序因為后面的重點部分比特序跟字節序也有一定的相似度和聯系。
字節序就是說一個對象的多個字節在內存中如何排序存放,比如我們要想往一個地址a中寫入一個整形數據0x12345678,那么最后在內存中是如何存放這四個字節的呢?
0x12這個字節值為最高有效字節,也就是整數值的最高位(在本文中0x12=0x12000000),0x78為最低有效字節。
圖1:大端字節序
上圖是大端字節序的示意圖,所謂”大端字節序”,便是指最高有效字節落在低地址上的字節存放方式。
圖2:小端字節序
而小端字節序就是最低有效字節落在低地址上的字節存放方式。
0x12345678=0x12000000 + 0x340000 + 0x5600 + 0x78,所以要想保持一個對象的值在大小端系統之間不變,那么就必須確保不同的系統能夠正確的識別最高有效字節和最低有效字節(不能錯誤的識別最高、最低有效字節)。
同樣的字節序12 34 56 78在大端序機器中會識別為0x12345678(0x12000000 + 0x340000 + 0x5600 + 0x78=0x12345678),在小端序機器中識別為0x78563412(0x12 + 0x3400 + 0x5600 00+ 0x78000000=0x78563412)。
所以要想兩者保持一致就必須確保系統能夠正確的識別最高有效字節0x12和最低有效字節0x78,那么在小端系統中字節存放的順序應該為78 56 34 12。 - 比特序(bit order)
字節序是一個對象中的多個字節之間的順序問題,比特序就是一個字節中的8個比特位(bit)之間的順序問題。一般情況下系統的比特序和字節序是保持一致的。
一個字節由8個bit組成,這8個bit也存在如何排序的情況,跟字節序類似的有最高有效比特位、最低有效比特位。
比特序1 0 0 1 0 0 1 0在大端系統中最高有效比特位為1、最低有效比特位為0,字節的值為0x92。在小端系統中最高、最低有效比特位則相反為0、1,字節的值為0x49。
跟字節序類似,要想保持一個字節值不變那么就要使系統能正確的識別最高、最低有效比特位。 - 字節序轉換函數ntohl(s)、htonl(s)
在socket編程中經常要用到網絡字節序轉換函數ntohl、htonl來進行主機序和網絡序(大端序)的轉換,在主機序為小端的系統中字節序列78 56 34 12(val=0x12345678)經過htonl轉換后字節序列變成12 34 56 78:
圖3:htonl函數
字節序轉換后我在想是不是比特序也一同進行了轉換?
為什么會有這個疑問呢,因為前文可知系統的比特序和字節序是一致的,現在字節序已經從小端變成了大端那么比特序應該也要一起轉換。而且如果比特序不變化那么當這些字節到了目標大端序系統中后每一個字節的值都會發生變化,因為同樣的比特序列在小端和大端系統中識別的字節值會不一樣。
首先從htonl、ntohl的源碼來看確實只進行了字節序的轉換並沒有進行比特序的轉換,再有就是以前socket編程的時候只調用了ntohl、htonl等函數並沒有調用(而且系統也沒有提供)比特序轉換函數,但是最后的結果都是正確的,並沒有發現上面提到的字節值發生變化的問題。
那么這個”神奇”的事情是怎么解決的呢,好像系統本身就給我們”悄悄”的解決了我擔心的問題。
答案我們下文揭曉。 - 比特(bit)的發送和接收順序
比特的發送、接收順序是指一個字節中的bit在網絡電纜中是如何發送、接收的。在以太網(Ethernet)中,是從最低有效比特位到最高有效比特位的發送順序,也就是最低有效比特位首先發送,參考資料:frame。
在以太網中這個規定有點奇怪,因為字節序我們是按照大端序來發送,但是比特序卻是按照小端序的方式來發送,下圖是直接從網上找來的一張圖,主機序本身是大端序:
圖4:比特發送、接受示意圖
比特的發送、接收順序對CPU、軟件都是不可見的,因為我們的網卡會給我們處理這種轉換,在發送的時候按照小端序發送比特位,在接收的時候會把接收到的比特序轉換成主機的比特序,下面是一個小端機器發送一個int整型給一個大端機器的示意圖:
圖5:小端->大端比特發送示例
因為對網卡對比特序的發送、接收所做的轉換沒有深入的了解所以上圖很有可能會有錯誤之處。
現在來回答一下第3節中的那個疑問:
- htonl、ntohl函數肯定是不會同步轉換一個字節中的比特序的,因為如果比特序也發生了轉換的話那么這個字節的值也就發生了變化,記住htonl、ntohl只是字節序轉換函數。
- 比特序按照小端的方式發送,首先發送的是最低有效比特位,最后發送的是最高有效比特位,接收端的網卡在接收到比特序列后按照主機的比特序把接收到的”小端序”比特流轉換成主機對應的比特序列。
可以假設存在ntohb、htonb(b代表bit)這樣的兩個函數,網卡進行了比特序的轉換,不過是這兩個函數是網卡自動調用的,我們平時不用關注。 - 按照規則,發送、接收的時候進行比特序的轉換,那么就能保證在不同的機器之間進行通信不會發生我擔心的字節值發生變化的問題。
- 結構體的位域
關於C語言中結構體的位域可以參考這篇文章:http://tonybai.com/2013/05/21/talk-about-bitfield-in-c-again/,對於位域的具體用法、語法參考這篇文章即可有。
對於位域有一個約定:在C語言的結構體中如果包含了位域,如果位域A定義在位域B之前,那么位域A總是出現在低序的比特位。
在計算機中可尋址的最小單位為字節,bit是無法尋址的,但是為了抽象我們可以把計算機的最小尋址單位變成bit,也就是我們可以單獨獲得一個bit位。
我們有如下的一段代碼:
#include<stdio.h> struct bit_order{ unsigned char a: 2, b: 3, c: 3; }; int main(int argc, char *argv[]) { unsigned char ch = 0x79; struct bit_order *ptr = (struct bit_order *)&ch; printf("bit_order->a : %u\n", ptr->a); printf("bit_order->b : %u\n", ptr->b); printf("bit_order->c : %u\n", ptr->c); return 0; }
我們把代碼在gentoo(intel小端機器)、hu-unix(大端機器)兩個機器上面編譯、運行,結果如下:
liuxingen@ V6-Dev ~/station $ ./bitfiled
bit_order->a : 1
bit_order->b : 6
bit_order->c : 3
下面是hp-unix的運行結果
# ./bitfiled
bit_order->a : 1
bit_order->b : 7
bit_order->c : 1
我們先分析一下gentoo上面的結果:
圖6:小端機器的位域示例
從上圖中我們很容易就能理解gentoo上面的輸出結果,下面是hp-unix上面示意圖:
圖7:大端機器的位域示例
從上面的輸出可以看到同樣的代碼在不同的機器中輸出了不同的結果,也就是說我們的代碼在不同的平台不能直接移植,導致這個問題的原因就是我們前面提到的關於位域的一個約定,定義在前面的位域總是出現在低地址的bit位中,因為不同的平台的比特序是不同的,但是我們定義的位域沒有根據平台的大小端進行轉換,最后就導致了問題。那么如何解決這個問題,那就是在定義結構體中的位域時判斷平台的大小端:
#include<stdio.h> #include<asm/byteorder.h> struct bit_order{ #if defined(__LITTLE_ENDIAN_BITFIELD) unsigned char a: 2, b: 3, c: 3; #elif defined (__BIG_ENDIAN_BITFIELD) unsigned char c: 3, b: 3, a: 2; #else #error "Please fix <asm/byteorder.h>" #endif }; int main(int argc, char *argv[]) { unsigned char ch = 0x79; struct bit_order *ptr = (struct bit_order *)&ch; printf("bit_order->a : %u\n", ptr->a); printf("bit_order->b : %u\n", ptr->b); printf("bit_order->c : %u\n", ptr->c); return 0; }
到此我們也就解釋了文章開頭關於struct iphdr定義中的那個疑問。
最后給大家隆重介紹一篇文章,對我啟發很大,文中的很多知識來自於它:byte order and bit order