天天看點

位元組序(byte order)和位序(bit order)

位元組序(byte order)和位序(bit order)

 在網絡程式設計中經常會提到網絡位元組序和主機序,也就是說當一個對象由多個位元組組成的時候需要注意對象的多個位元組在記憶體中的順序。 

 以前我也基本隻了解過位元組序,但是有一天當我看到ip.h中對IP頭部結構體struct iphdr的定義時,我發現其中竟然對一個位元組中的8個比特位也區分了大小端,這時我就迷糊了,不是說大小端隻有在多個位元組之間才會有區分的嗎,為什麼這裡的定義卻對一個位元組中的比特位也區分大小端呢? 

 下面我們先看一下struct iphdr的定義,後文會解惑為什麼要在一個位元組中區分大小端。

struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u8    ihl:,
        version:;
#elif defined (__BIG_ENDIAN_BITFIELD)
    __u8    version:,
        ihl:;
#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. */
};
           
  1. 位元組序(Byte order) 

     關于位元組序的文章已經有很多了,在我這篇文章中不打算過多的說位元組序,但是也不能完全脫離位元組序因為後面的重點部分比特序跟位元組序也有一定的相似度和聯系。 

     位元組序就是說一個對象的多個位元組在記憶體中如何排序存放,比如我們要想往一個位址a中寫入一個整形資料0x12345678,那麼最後在記憶體中是如何存放這四個位元組的呢? 

     0x12這個位元組值為最高有效位元組,也就是整數值的最高位(在本文中0x12=0x12000000),0x78為最低有效位元組。 

    位元組序(byte order)和位序(bit order)

                圖1:大端位元組序 

    上圖是大端位元組序的示意圖,所謂”大端位元組序”,便是指最高有效位元組落在低位址上的位元組存放方式。 

    位元組序(byte order)和位序(bit order)

                圖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。

  2. 比特序(bit order) 

     位元組序是一個對象中的多個位元組之間的順序問題,比特序就是一個位元組中的8個比特位(bit)之間的順序問題。一般情況下系統的比特序和位元組序是保持一緻的。 

     一個位元組由8個bit組成,這8個bit也存在如何排序的情況,跟位元組序類似的有最高有效比特位、最低有效比特位。 

     比特序1 0 0 1 0 0 1 0在大端系統中最高有效比特位為1、最低有效比特位為0,位元組的值為0x92。在小端系統中最高、最低有效比特位則相反為0、1,位元組的值為0x49。 

     跟位元組序類似,要想保持一個位元組值不變那麼就要使系統能正确的識别最高、最低有效比特位。

  3. 位元組序轉換函數ntohl(s)、htonl(s) 

     在socket程式設計中經常要用到網絡位元組序轉換函數ntohl、htonl來進行主機序和網絡序(大端序)的轉換,在主機序為小端的系統中位元組序列78 56 34 12(val=0x12345678)經過htonl轉換後位元組序列變成12 34 56 78: 

    位元組序(byte order)和位序(bit order)

                圖3:htonl函數 

     位元組序轉換後我在想是不是比特序也一同進行了轉換? 

     為什麼會有這個疑問呢,因為前文可知系統的比特序和位元組序是一緻的,現在位元組序已經從小端變成了大端那麼比特序應該也要一起轉換。而且如果比特序不變化那麼當這些位元組到了目标大端序系統中後每一個位元組的值都會發生變化,因為同樣的比特序列在小端和大端系統中識别的位元組值會不一樣。 

     首先從htonl、ntohl的源碼來看确實隻進行了位元組序的轉換并沒有進行比特序的轉換,再有就是以前socket程式設計的時候隻調用了ntohl、htonl等函數并沒有調用(而且系統也沒有提供)比特序轉換函數,但是最後的結果都是正确的,并沒有發現上面提到的位元組值發生變化的問題。 

     那麼這個”神奇”的事情是怎麼解決的呢,好像系統本身就給我們”悄悄”的解決了我擔心的問題。 

    答案我們下文揭曉。

  4. 比特(bit)的發送和接收順序 

     比特的發送、接收順序是指一個位元組中的bit在網絡電纜中是如何發送、接收的。在以太網(Ethernet)中,是從最低有效比特位到最高有效比特位的發送順序,也就是最低有效比特位首先發送,參考資料:frame。 

     在以太網中這個規定有點奇怪,因為位元組序我們是按照大端序來發送,但是比特序卻是按照小端序的方式來發送,下圖是直接從網上找來的一張圖,主機序本身是大端序: 

    位元組序(byte order)和位序(bit order)

                圖4:比特發送、接受示意圖 

     比特的發送、接收順序對CPU、軟體都是不可見的,因為我們的網卡會給我們處理這種轉換,在發送的時候按照小端序發送比特位,在接收的時候會把接收到的比特序轉換成主機的比特序,下面是一個小端機器發送一個int整型給一個大端機器的示意圖: 

    位元組序(byte order)和位序(bit order)

                圖5:小端->大端比特發送示例 

     因為對網卡對比特序的發送、接收所做的轉換沒有深入的了解是以上圖很有可能會有錯誤之處。 

     現在來回答一下第3節中的那個疑問: 

    • htonl、ntohl函數肯定是不會同步轉換一個位元組中的比特序的,因為如果比特序也發生了轉換的話那麼這個位元組的值也就發生了變化,記住htonl、ntohl隻是位元組序轉換函數。
    • 比特序按照小端的方式發送,首先發送的是最低有效比特位,最後發送的是最高有效比特位,接收端的網卡在接收到比特序列後按照主機的比特序把接收到的”小端序”比特流轉換成主機對應的比特序列。 

       可以假設存在ntohb、htonb(b代表bit)這樣的兩個函數,網卡進行了比特序的轉換,不過是這兩個函數是網卡自動調用的,我們平時不用關注。

    • 按照規則,發送、接收的時候進行比特序的轉換,那麼就能保證在不同的機器之間進行通信不會發生我擔心的位元組值發生變化的問題。
  5. 結構體的位域 

     關于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: ,
                  b: ,
                  c: ;
};

int main(int argc, char *argv[])
{
    unsigned char ch      = ;
    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 ;
}
           

 我們把代碼在gentoo(intel小端機器)、hu-unix(大端機器)兩個機器上面編譯、運作,結果如下:

[email protected] 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上面的結果: 

位元組序(byte order)和位序(bit order)

            圖6:小端機器的位域示例 

 從上圖中我們很容易就能了解gentoo上面的輸出結果,下面是hp-unix上面示意圖: 

位元組序(byte order)和位序(bit order)

            圖7:大端機器的位域示例 

 從上面的輸出可以看到同樣的代碼在不同的機器中輸出了不同的結果,也就是說我們的代碼在不同的平台不能直接移植,導緻這個問題的原因就是我們前面提到的關于位域的一個約定,定義在前面的位域總是出現在低位址的bit位中,因為不同的平台的比特序是不同的,但是我們定義的位域沒有根據平台的大小端進行轉換,最後就導緻了問題。那麼如何解決這個問題,那就是在定義結構體中的位域時判斷平台的大小端:

#include<stdio.h>
#include<asm/byteorder.h>

struct bit_order{
#if defined(__LITTLE_ENDIAN_BITFIELD)
    unsigned char a: ,
                  b: ,
                  c: ;
#elif defined (__BIG_ENDIAN_BITFIELD)
    unsigned char c: ,
                  b: ,
                  a: ;
#else
#error  "Please fix <asm/byteorder.h>"
#endif
};

int main(int argc, char *argv[])
{
    unsigned char ch      = ;
    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 ;
}
           

 到此我們也就解釋了文章開頭關于struct iphdr定義中的那個疑問。 

 最後給大家隆重介紹一篇文章,對我啟發很大,文中的很多知識來自于它:byte order and bit order

繼續閱讀