1. 程式人生 > >位元組序與位元序詳解

位元組序與位元序詳解

位元組序的定義

位元組序就是說一個物件的多個位元組在記憶體中如何排序存放,比如我們要想往一個地址a中寫入一個整形資料0x12345678,那麼最後在記憶體中是如何存放這四個位元組的呢?
 0x12這個位元組值為最高有效位元組,也就是整數值的最高位(在本文中0x12=0x12000000),0x78為最低有效位元組。
 這裡就分為大端位元組序和小端位元組序
 大端位元組序:便是指最高有效位元組落在低地址上的位元組存放方式
 小端位元組序:便是指最高有效位元組落在高地址上的位元組存放方式。
 a a+1 a+2 a+3
0x12 0x34 0x56 0x78 大端位元組序
0x78 0x56 0x34 0x12 小端位元組序

幾種型別的位元組序

參考如下圖的計算機系統:
這裡寫圖片描述
CPU, local bus and internal memory/cache 都可以歸納為CPU,因為他們通常都有同樣的位元組序。

cpu位元組序

小端位元組序CPU包括Intel和DEC,大端位元組序CPU包括Motorola 680x0,Sun Sparc以及IBM(例如Powerpc)。MIPS和ARM可以配置兩種位元組序中的任一種。

外部bus位元組序

bus的位元組序由bus協議定義,假如bus的位元組序和CPU位元組序不同,bus控制器或者橋會執行轉換工作。

裝置位元組序

Kevin’s Theory #1: When a multi-byte data unit travels across the boundary of two reverse endian systems, the conversion is made such that memory contiguousness to the unit is preserved.

網路協議位元組序

網路協議的位元組序定義了網路協議頭中整形域部分的位元組和位元是如何傳送和接收的。我們引入一個術語:線上地址(wire address).低位線上地址的位元或者位元組在高位之間傳輸。
網絡卡通常遵循它們支援的網路協議的位元組序。大多數網路協議是大端位元組序。我們以Ethernet協議和IP協議為例說明。

Ethernet協議位元組序

乙太網(Ethernet)協議是大端位元組序。這意味著一個整型值的高位元組被放置在低線上地址上,同時在低位元組之前被傳輸或者接收。例如arp協議字0x0806,在乙太網頭中的佈局如下:
wire byte offset: 0 1
hex : 08 06
同時注意到乙太網頭中的mac地址被認為是字串,因此不用關心位元組序的問題。I例如mac地址12:34:56:78:9a:bc在線上有如下佈局,位元組12被首先傳輸。
這裡寫圖片描述


乙太網的資料結構如下:

struct ethhdr
{
        unsigned char   h_dest[ETH_ALEN];       
        unsigned char   h_source[ETH_ALEN];     
        unsigned short  h_proto;                
};

h_dest 和h_source 是字串,因此不必考慮位元組序的問題。h_proto是整型值,因此在主機訪問該欄位是需要使用ntohs,在主機填充該欄位時要htons。
至於位元序的傳輸順序下文有介紹。

IP協議位元組序

IP協議位元組序是大端。bit序繼承自CPU的,網絡卡負責線上轉換bit序列。
以下為ip header的結構體:

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;
        __u16   tot_len;
        __u16   id;
        __u16   frag_off;
        __u8    ttl;
        __u8    protocol;
        __u16   check;
        __u32   saddr;
        __u32   daddr;
        /*The options start here. */
};

version 和 ihl 欄位:
根據ip協議,version是ip頭首位元組的高有效4bit ,ihl是低有效bit.
有兩種方法訪問這兩個欄位:
1.直接解析法
假如ver_ihl代表ip頭的首位元組則ipl=ver_ihl&0xf,version=ver_ihl>>4,不論主機是那種位元組序。
2.結構體位域法
定義如上的結構體,假如主機是小端,我們定義ihl在version之前,如果是大端正好相反。應用Kevin’s Theory #2 如果位域A定義在位域B之前,那麼位域A總是出現在低序的位元位。正好可以滿足我們的訪問要求。

編譯位元組序

CPU的位元組序影響CPU的指令集。不同的GNC C工具鏈為了編譯C程式碼應該使用相應CPU的大小端。例如mips-linux-gcc和mipsel-linux-gcc是被用來分別編譯大端的和小端的MIPs程式碼。

位元序的定義

位元序就是一個位元組中的bit順序問題。一般情況下系統的位元序和位元組序是保持一致的。 對應分為以下情況:
LSB 0 位序:位元組的第0位存放資料的least significant bit,即我們的資料的最低位存放在位元組的第0位。(對應小端位元組序)
MSB 0 位序:節的第0位存放資料的most significant bit,即我們的資料的最高位存放在位元組的第0位。(對應大端位元組序)
LSB是指 least significant bit,MSB是指 most significant bit。
位元序1 0 0 1 0 0 1 0在大端系統中最高有效位元位為1、最低有效位元位為0,位元組的值為0x92。在小端系統中最高、最低有效位元位則相反為0、1,位元組的值為0x49。
跟位元組序類似,要想保持一個位元組值不變那麼就要使系統能正確的識別最高、最低有效位元位。

位元組序與bit序的轉換

位元組序轉換函式ntohl(s)、htonl(s) 。
例如在socket程式設計中經常要用到網路位元組序轉換函式ntohl、htonl來進行主機序和網路序(大端序)的轉換,在主機序為小端的系統中位元組序列78 56 34 12(val=0x12345678)經過htonl轉換後位元組序列變成12 34 56 78
位元組序轉換後我在想是不是位元序也一同進行了轉換?
 為什麼會有這個疑問呢,因為前文可知系統的位元序和位元組序是一致的,現在位元組序已經從小端變成了大端那麼位元序應該也要一起轉換。而且如果位元序不變化那麼當這些位元組到了目標大端序系統中後每一個位元組的值都會發生變化,因為同樣的位元序列在小端和大端系統中識別的位元組值會不一樣。
 首先從htonl、ntohl的原始碼來看確實只進行了位元組序的轉換並沒有進行位元序的轉換,再有就是以前socket程式設計的時候只調用了ntohl、htonl等函式並沒有呼叫(而且系統也沒有提供)位元序轉換函式,但是最後的結果都是正確的,並沒有發現上面提到的位元組值發生變化的問題。
 這是因為系統幫我們自動做了轉換。下面進行詳細分析。
 位元的傳送、接收順序是指一個位元組中的bit在網路電纜中是如何傳送、接收的。在乙太網(Ethernet)中,是從最低有效位元位到最高有效位元位的傳送順序,也就是最低有效位元位首先發送。
 在乙太網中這個規定有點奇怪,因為位元組序我們是按照大端序來發送,但是位元序卻是按照小端序的方式來發送。如下圖所示:
 主機是大端系統:
這裡寫圖片描述
 位元的傳送、接收順序對CPU、軟體都是不可見的,(對諸如PHY的serdes(序列器和解串器)以及網絡卡寫匯流排的硬體設計是非常重要的)因為我們的網絡卡會給我們處理這種轉換,在傳送的時候按照小端序傳送位元位,在接收的時候網絡卡會把接收到的位元序轉換成主機的位元序
 The bit transmission/reception order generally is invisible to the CPU and software, 下面是一個小端機器傳送一個int整型給一個大端機器的示意圖:
這裡寫圖片描述

結構體的位域

對於位域有一個約定:在C語言的結構體中如果包含了位域,如果位域A定義在位域B之前,那麼位域A總是出現在低序的位元位。
參考如下程式碼:

#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;
}

小端機器A上的結果

bit_order->a : 1 
bit_order->b : 6 
bit_order->c : 3 

對應位元組上的bit序如下圖:
這裡寫圖片描述
大端機器上的結果

bit_order->a : 1 
bit_order->b : 7 
bit_order->c : 1

這裡寫圖片描述
 從上面的輸出可以看到同樣的程式碼在不同的機器中輸出了不同的結果,也就是說我們的程式碼在不同的平臺不能直接移植,導致這個問題的原因就是我們前面提到的關於位域的一個約定,定義在前面的位域總是出現在低地址的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;
}