C語言再學習-- 大端小端詳解(轉)

參看:詳解大端模式和小端模式

一、什麼是大端和小端

所謂的大端模式,就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。

所謂的小端模式,就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。

簡單來說:大端——高尾端,小端——低尾端


舉個例子,比如數字 0x12 34 56 78在內存中的表示形式爲:

1)大端模式:

低地址 -----------------> 高地址

0x12  |  0x34  |  0x56  |  0x78

2)小端模式:

低地址 ------------------> 高地址

0x78  |  0x56  |  0x34  |  0x12

可見,大端模式和字符串的存儲模式類似。

3)下面是兩個具體例子:

16bit寬的數0x1234在Little-endian模式(以及Big-endian模式)CPU內存中的存放方式(假設從地址0x4000開始存放)爲:

內存地址 小端模式存放內容 大端模式存放內容
0x4000 0x34 0x12
0x4001 0x12 0x34

32bit寬的數0x12345678在Little-endian模式以及Big-endian模式)CPU內存中的存放方式(假設從地址0x4000開始存放)爲:

內存地址 小端模式存放內容 大端模式存放內容
0x4000 0x78 0x12
0x4001 0x56 0x34
0x4002 0x34 0x56
0x4003 0x12 0x78



4)大端小端沒有誰優誰劣,各自優勢便是對方劣勢:

小端模式 :強制轉換數據不需要調整字節內容,1、2、4字節的存儲方式一樣。
大端模式 :符號位的判定固定爲第一個字節,容易判斷正負。


二、數組在大端小端情況下的存儲:

  以unsigned int value = 0x12345678爲例,分別看看在兩種字節序下其存儲情況,我們可以用unsigned char buf[4]來表示value:
  Big-Endian: 低地址存放高位,如下:
高地址
        ---------------
        buf[3] (0x78) -- 低位
        buf[2] (0x56)
        buf[1] (0x34)
        buf[0] (0x12) -- 高位
        ---------------
        低地址
Little-Endian: 低地址存放低位,如下:
高地址
        ---------------
        buf[3] (0x12) -- 高位
        buf[2] (0x34)
        buf[1] (0x56)
        buf[0] (0x78) -- 低位
        --------------
低地址


三、爲什麼會有大小端模式之分呢?

這是因爲在計算機中,我們是以字節爲單位的,每個地址單元都對應着一個字節,一個字節爲 8 bit。但是在C 語言中除了 8 bit 的char之外,還有 16 bit 的 short型,32bit的long型(要看具體的編譯器),另外,對於位數大於8位的處理器,例如16位或者32位的處理器,由於寄存器寬度大於一個字節,那麼必然存在着一個如果將多個字節安排的問題。因此就導致了大端存儲模式和小端存儲模式。例如一個16bit的short型 x ,在內存中的地址爲 0x0010,x 的值爲0x1122,那麼0x11位高字節,0x22位低字節。對於大端模式,就將0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,剛好相反。我們常用的X86結構是小端模式,而KEIL C51則爲大端模式。很多的ARM,DSP都爲小端模式。有些ARM處理器還可以由硬件來選擇是大端模式還是小端模式。


四、如何判斷機器的字節序 (重點)

一般都是通過 union 來測試的,下面這段代碼可以用來測試一下你的編譯器是大端模式還是小端模式:
[cpp] view plain copy
  1. #include <stdio.h>  
  2. int main (void)  
  3. {  
  4.     union  
  5.     {  
  6.         short i;  
  7.         char a[2];  
  8.     }u;  
  9.     u.a[0] = 0x11;  
  10.     u.a[1] = 0x22;  
  11.     printf ("0x%x\n", u.i);  //0x2211 爲小端  0x1122 爲大端  
  12.     return 0;  
  13. }  
  14. 輸出結果:  
  15. 0x2211  
union 型數據所佔的空間等於其最大的成員所佔的空間。對 union 型的成員的存取都是相對於該聯合體基地址的偏移量爲 0 處開始,也就是聯合體的訪問不論對哪個變量的存取都是從 union 的首地址位置開始
聯合是一個在同一個存儲空間裏存儲不同類型數據的數據類型。這些存儲區的地址都是一樣的,聯合裏不同存儲區的內存是重疊的,修改了任何一個其他的會受影響。

1. 共用體聲明和共用體變量定義
共用體(參考“共用體”百科詞條)是一種特殊形式的變量,使用關鍵字union來定義
共用體(有些人也叫"聯合")聲明和共用體變量定義與結構體十分相似。其形式爲:
union 共用體名{
數據類型 成員名;
數據類型 成員名;
...
} 變量名;

參看:

共用體表示幾個變量共用一個內存位置,在不同的時間保存不同的數據類型和不同長度的變量。在union中,所有的共用體成員共用一個空間,並且同一時間只能儲存其中一個成員變量的值

下例表示聲明一個共用體foo:

[cpp] view plain copy
  1. union foo{/*“共用”類型“FOO”*/  
  2.     int i;    /*“整數”類型“i”*/  
  3.     char c;   /*“字符”類型“C”*/  
  4.     double k;  /*“雙”精度類型“K”*/  
  5. };  
再用已聲明的共用體可定義共用體變量。例如,用上面說明的共用體定義一個名爲bar的共用體變量, 可寫成:
union foo bar;
在共用體變量bar中, 整型變量 i 和字符變量 c 共用同一內存位置。

當一個共用體被聲明時, 編譯程序自動地產生一個變量, 其長度爲聯合中最大的變量長度的整數倍。以上例而言,最大長度是double數據類型,所以foo的內存空間就是double型的長度。

[cpp] view plain copy
  1. union foo/*“共用”類型“FOO”*/  
  2. {  
  3.     char s[10];    /*“字符”類型的數組“S”下面有“10”個元素*/  
  4.     int i;        /*“整數”類型i*/  
  5. };  
在這個union中,foo的內存空間的長度爲12,是int型的3倍,而並不是數組的長度10。若把int改爲double,則foo的內存空間爲16,是double型的兩倍。

2. 共用體和結構體的區別
1)共用體和結構體都是由多個不同的數據類型成員組成, 但在任何同一時刻, 共用體只存放了一個被選中的成員, 而結構體的所有成員都存在。
2.)對於共用體的不同成員賦值, 將會對其它成員重寫, 原來成員的值就不存在了, 而對於結構體的不同成員賦值是互不影響的。

總結:
恍然大悟,union 聯合之前還是沒有理解透。一開始不太理解,爲什麼給 a[0]、a[1] 賦值,i 沒有定義啊,爲什麼會有值呢,或者值爲什麼不是隨機數呢?現在明白了,我們爲什麼用 union 聯合來測試大小端,在聯合變量 u 中, 短整型變量 i 和字符數組 a 共用同一內存位置。給 a[0]、a[1] 賦值後,i  也是從同一內存地址讀值的。

知道這層關係後,那麼通過強制類型轉換,判斷其實存儲位置,也可以測試大小端了:
[cpp] view plain copy
  1. #include <stdio.h>  
  2. int main (void)  
  3. {  
  4.     short i = 0x1122;  
  5.     char *a = (char*)(&i);  
  6.     printf ("0x%x\n", *(a + 0)); //大端爲 0x11 小端爲 0x22  
  7.     printf ("0x%x\n", *(a + 1));  
  8.     return 0;  
  9. }  
  10. 輸出結果:  
  11. 0x22  
  12. 0x11  

說明:上面兩個例子,可以通過 if 語句來判斷大小端,這裏只是介紹方法。

五、常見的字節序
一般操作系統都是小端,而通訊協議是大端的。
1)常見CPU的字節序
Big Endian : PowerPC、IBM、Sun
Little Endian : x86、DEC
ARM既可以工作在大端模式,也可以工作在小端模式。
2)常見文件的字節序
Adobe PS – Big Endian
BMP – Little Endian
DXF(AutoCAD) – Variable
GIF – Little Endian
JPEG – Big Endian
MacPaint – Big Endian
RTF – Little Endian


另外,Java和所有的網絡通訊協議都是使用Big-Endian的編碼。

六、如何進行大小端轉換(重點)

第一種方法:位操作
[cpp] view plain copy
  1. #include<stdio.h>    
  2.     
  3. typedef unsigned int uint_32 ;    
  4. typedef unsigned short uint_16 ;    
  5.   
  6. //16位  
  7. #define BSWAP_16(x) \  
  8.     (uint_16)((((uint_16)(x) & 0x00ff) << 8) | \  
  9.               (((uint_16)(x) & 0xff00) >> 8) \  
  10.              )  
  11.                
  12. //32位                 
  13. #define BSWAP_32(x) \  
  14.     (uint_32)((((uint_32)(x) & 0xff000000) >> 24) | \  
  15.               (((uint_32)(x) & 0x00ff0000) >> 8) | \  
  16.               (((uint_32)(x) & 0x0000ff00) << 8) | \  
  17.               (((uint_32)(x) & 0x000000ff) << 24) \  
  18.              )    
  19.   
  20. //無符號整型16位    
  21. uint_16 bswap_16(uint_16 x)    
  22. {    
  23.     return (((uint_16)(x) & 0x00ff) << 8) | \  
  24.            (((uint_16)(x) & 0xff00) >> 8) ;    
  25. }    
  26.   
  27. //無符號整型32位  
  28. uint_32 bswap_32(uint_32 x)    
  29. {    
  30.     return (((uint_32)(x) & 0xff000000) >> 24) | \  
  31.            (((uint_32)(x) & 0x00ff0000) >> 8) | \  
  32.            (((uint_32)(x) & 0x0000ff00) << 8) | \  
  33.            (((uint_32)(x) & 0x000000ff) << 24) ;    
  34. }    
  35.   
  36. int main(int argc,char *argv[])    
  37. {    
  38.     printf("------------帶參宏-------------\n");    
  39.     printf("%#x\n",BSWAP_16(0x1234)) ;    
  40.     printf("%#x\n",BSWAP_32(0x12345678));    
  41.     printf("------------函數調用-----------\n");    
  42.     printf("%#x\n",bswap_16(0x1234)) ;    
  43.     printf("%#x\n",bswap_32(0x12345678));    
  44.         
  45.     return 0 ;    
  46. }    
  47. 輸出結果:  
  48. ------------帶參宏-------------  
  49. 0x3412  
  50. 0x78563412  
  51. ------------函數調用-----------  
  52. 0x3412  
  53. 0x78563412  

第二種方法: 

從軟件的角度理解端模式

使用 htonl, htons, ntohl, ntohs 等函數

查看:man htonl
[cpp] view plain copy
  1. NAME  
  2.        htonl, htons, ntohl, ntohs - convert values between host and network byte order  
  3.   
  4. SYNOPSIS  
  5.        #include <arpa/inet.h>  
  6.   
  7.        uint32_t htonl(uint32_t hostlong);  
  8.   
  9.        uint16_t htons(uint16_t hostshort);  
  10.   
  11.        uint32_t ntohl(uint32_t netlong);  
  12.   
  13.        uint16_t ntohs(uint16_t netshort);  
  14.   
  15. DESCRIPTION  
  16.        The htonl() function converts the unsigned integer hostlong from host byte order to network byte order.  
  17.   
  18.        The htons() function converts the unsigned short integer hostshort from host byte order to network byte order.  
  19.   
  20.        The ntohl() function converts the unsigned integer netlong from network byte order to host byte order.  
  21.   
  22.        The ntohs() function converts the unsigned short integer netshort from network byte order to host byte order.  
  23.   
  24.        On  the  i386  the host byte order is Least Significant Byte first, whereas the network byte order, as used on the Internet, is Most  
  25.        Significant Byte first.  
翻譯:
htonl()     //32位無符號整型的主機字節順序到網絡字節順序的轉換(小端->>大端)
htons() 
    //16位無符號短整型的主機字節順序到網絡字節順序的轉換  (小端->>大端)
ntohl()  
   //32位無符號整型的網絡字節順序到主機字節順序的轉換  (大端->>小端)
ntohs() 
    //16位無符號短整型的網絡字節順序到主機字節順序的轉換  (大端->>小端)

主機字節順序,X86一般多爲小端(little-endian),網絡字節順序,即大端(big-endian);

舉兩個小例子:
[cpp] view plain copy
  1. //示例一  
  2. #include <stdio.h>  
  3. #icnlude <arpa/inet.h>  
  4. int main (void)  
  5. {  
  6.     union  
  7.     {  
  8.         short i;  
  9.         char a[2];  
  10.     }u;  
  11.     u.a[0] = 0x11;  
  12.     u.a[1] = 0x22;  
  13.     printf ("0x%x\n", u.i);  //0x2211 爲小端  0x1122 爲大端  
  14.     printf ("0x%.x\n", htons (u.i)); //大小端轉換   
  15.     return 0;  
  16. }  
  17. 輸出結果:  
  18. 0x2211  
  19. 0x1122  
[cpp] view plain copy
  1. //示例二  
  2. #include <stdio.h>   
  3. #include <arpa/inet.h>   
  4. struct ST{    
  5.     short val1;    
  6.     short val2;    
  7. };    
  8. union U{    
  9.     int val;    
  10.     struct ST st;    
  11. };    
  12.      
  13. int main(void)    
  14. {    
  15.     int a = 0;    
  16.     union U u1, u2;    
  17.      
  18.     a = 0x12345678;    
  19.     u1.val = a;    
  20.     printf("u1.val is 0x%x\n", u1.val);    
  21.     printf("val1 is 0x%x\n", u1.st.val1);    
  22.     printf("val2 is 0x%x\n", u1.st.val2);    
  23.     printf("after first convert is: 0x%x\n", htonl(u1.val));    
  24.     u2.st.val2 = htons(u1.st.val1);    
  25.     u2.st.val1 = htons(u1.st.val2);    
  26.     printf("after second convert is: 0x%x\n", u2.val);    
  27.     return 0;    
  28. }    
  29. 輸出結果:  
  30. u1.val is 0x12345678  
  31. val1 is 0x5678  
  32. val2 is 0x1234  
  33. after first convert is: 0x78563412  
  34. after second convert is: 0x78563412  

    在對普通文件進行處理也需要考慮端模式問題。在大端模式的處理器下對文件的32,16位讀寫操作所得到的結果與小端模式的處理器不同。單純從軟件的角度理解上遠遠不能真正理解大小端模式的區別。事實上,真正的理解大小端模式的區別,必須要從系統的角度,從指令集,寄存器和數據總線上深入理解,大小端模式的區別


以下內容瞭解:

1、從系統的角度理解端模式

先補充兩個關鍵詞,MSB和LSB:
  MSB:MoST Significant Bit ------- 最高有效位
        LSB:Least Significant Bit ------- 最低有效位

        處理器在硬件上由於端模式問題在設計中有所不同。從系統的角度上看,端模式問題對軟件和硬件的設計帶來了不同的影響,當一個處理器系統中大小端模式同時存在時,必須要對這些不同端模式的訪問進行特殊的處理。
       PowerPC處理器主導網絡市場,可以說絕大多數的通信設備都使用PowerPC處理器進行協議處理和其他控制信息的處理,這也可能也是在網絡上的絕大多數協議都採用大端編址方式的原因。因此在有關網絡協議的軟件設計中,使用小端方式的處理器需要在軟件中處理端模式的轉變。而Pentium主導個人機市場,因此多數用於個人機的外設都採用小端模式,包括一些在網絡設備中使用的PCI總線,Flash等設備,這也要求在硬件設計中注意端模式的轉換。
       本文提到的小端外設是指這種外設中的寄存器以小端方式進行存儲,如PCI設備的配置空間,NOR FLASH中的寄存器等等。對於有些設備,如DDR顆粒,沒有以小端方式存儲的寄存器,因此從邏輯上講並不需要對端模式進行轉換。在設計中,只需要將雙方數據總線進行一一對應的互連,而不需要進行數據總線的轉換。
       如果從實際應用的角度說,採用小端模式的處理器需要在軟件中處理端模式的轉換,因爲採用小端模式的處理器在與小端外設互連時,不需要任何轉換。而採用大端模式的處理器需要在硬件設計時處理端模式的轉換。大端模式處理器需要在寄存器,指令集,數據總線及數據總線與小端外設的連接等等多個方面進行處理,以解決與小端外設連接時的端模式轉換問題。在寄存器和數據總線的位序定義上,基於大小端模式的處理器有所不同。
       一個採用大端模式的32位處理器,如基於E500內核的MPC8541,將其寄存器的最高位msb(most significant bit)定義爲0,最低位lsb(lease significant bit)定義爲31;而小端模式的32位處理器,將其寄存器的最高位定義爲31,低位地址定義爲0。與此向對應,採用大端模式的32位處理器數據總線的最高位爲0,最高位爲31;採用小端模式的32位處理器的數據總線的最高位爲31,最低位爲0。         
       大小端模式處理器外部總線的位序也遵循着同樣的規律,根據所採用的數據總線是32位,16位和8位,大小端處理器外部總線的位序有所不同。大端模式下32位數據總線的msb是第0位,MSB是數據總線的第0~7的字段;而lsb是第31位,LSB是第24~31字段。小端模式下32位總線的msb是第31位,MSB是數據總線的第31~24位,lsb是第0位,LSB是7~0字段。大端模式下16位數據總線的msb是第0位,MSB是數據總線的第0~7的字段;而lsb是第15位,LSB是第8~15字段。小端模式下16位總線的msb是第15位,MSB是數據總線的第15~7位,lsb是第0位,LSB是7~0字段。大端模式下8位數據總線的msb是第0位,MSB是數據總線的第0~7的字段;而lsb是第7位,LSB是第0~7字段。小端模式下8位總線的msb是第7位,MSB是數據總線的第7~0位,lsb是第0位,LSB是7~0字段。
         由上分析,我們可以得知對於8位,16位和32位寬度的數據總線,採用大端模式時數據總線的msb和MSB的位置都不會發生變化,而採用小端模式時數據總線的lsb和LSB位置也不會發生變化。
         爲此,大端模式的處理器對8位,16位和32位的內存訪問(包括外設的訪問)一般都包含第0~7字段,即MSB。小端模式的處理器對8位,16位和32位的內存訪問都包含第7~0位,小端方式的第7~0字段,即LSB。由於大小端處理器的數據總線其8位,16位和32位寬度的數據總線的定義不同,因此需要分別進行討論在系統級別上如何處理端模式轉換。在一個大端處理器系統中,需要處理大端處理器對小端外設的訪問。

2、實際中的例子
       雖然很多時候,字節序的工作已由編譯器完成了,但是在一些小的細節上,仍然需要去仔細揣摩考慮,尤其是在以太網通訊、MODBUS通訊、軟件移植性方面。這裏,舉一個MODBUS通訊的例子。在MODBUS中,數據需要組織成數據報文,該報文中的數據都是大端模式,即低地址存高位,高地址存低位。假設有一16位緩衝區m_RegMW[256],因爲是在x86平臺上,所以內存中的數據爲小端模式:m_RegMW[0].low、m_RegMW[0].high、m_RegMW[1].low、m_RegMW[1].high……
爲了方便討論,假設m_RegMW[0] = 0x3456; 在內存中爲0x56、0x34。
       現要將該數據發出,如果不進行數據轉換直接發送,此時發送的數據爲0x56,0x34。而Modbus是大端的,會將該數據解釋爲0x5634而非原數據0x3456,此時就會發生災難性的錯誤。所以,在此之前,需要將小端數據轉換成大端的,即進行高字節和低字節的交換,此時可以調用步驟五中的函數BigtoLittle16(m_RegMW[0]),之後再進行發送纔可以得到正確的數據
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章