計算機存儲的大小端模式解析


http://blog.csdn.net/hackbuteer1/article/details/7722667

原作者文章鏈接,寫得很好,沒必要再重新分析了,看這個就夠了。一般在《計算機組成原理》,或者《微機原理》,或者《匯編語言》等課程中也會有介紹,不過沒有這么詳細透徹罷了。紅色筆記是我的注解

----------------------------------------------------------------------------------------------------------------

先主要介紹了大小端問題的來源,作者應該是基於32位的機器來講解的。

在各種計算機體系結構中,對於字節、字等的存儲機制有所不同,因而引發了計算機 通信領 域中一個很重要的問題,即通信雙方交流的信息單元(比特、字節、字、雙字等等)應該以什么樣的順序進行傳送。如果不達成一致的規則,通信雙方將無法進行正 確的編/譯碼從而導致通信失敗。目前在各種體系的計算機中通常采用的字節存儲機制主要有兩種:Big-Endian和Little-Endian,下面先從字節序說起。


一、什么是字節序

字節序,顧名思義字節的順序,再多說兩句就是大於一個字節類型的數據在內存中的存放順序(一個字節的數據當然就無需談順序的問題了)。其實大部分人在實際的開發中都很少會直接和字節序打交道。唯有在跨平台以及網絡程序中字節序才是一個應該被考慮的問題。

在所有的介紹字節序的文章中都會提到字節序分為兩類:Big-Endian和Little-Endian,引用標准的Big-Endian和Little-Endian的定義如下:
a) Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。
b) Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。(兩個明白一個就ok,大小端是相反的
c) 網絡字節序:TCP/IP各層協議將字節序定義為Big-Endian,因此TCP/IP協議中使用的字節序通常稱之為網絡字節序。

1.1 什么是高/低地址端

首先我們要知道C程序映像中內存的空間布局情況:在《C專 家編程》中或者《Unix環境高級編程》中有關於內存空間布局情況的說明,大致如下圖:

----------------------- 最高內存地址 0xffffffff(32位2進制數,都是1的情況,16進制就是0xffffffff
棧底



棧頂
-----------------------(和我們通常意義上的畫圖是相反的,棧在內存里是向下增長,且發現內存自上而下,地址是由高到低變換,俗稱的高地址到低地址變化
NULL (空洞)
-----------------------

-----------------------
未初始化的數據
----------------------- 統稱數據段
初始化的數據
-----------------------
正文段(代碼段)
----------------------- 最低內存地址 0x00000000


由圖可以看出內存分布中,棧是向下增長的,而堆是向上增長的。以上圖為例如果我們在棧上分配一個unsigned char buf[4],那么這個數組變量在棧上是如何布局的呢?看下圖:


棧底 (高地址)
----------
buf[3]
buf[2]
buf[1]
buf[0]

----------
棧頂 (低地址)


其實,我們可以自己在編譯器里面創建一個數組,然后分別輸出數組種每個元素的地址,來驗證一下。


1.2 什么是高/低字節

現在我們弄清了高/低地址,接着考慮高/低字節。有些文章中稱低位字節為最低有效位,高位字節為最高有效位。如果我們有一個32位無符號整型0x12345678,那么高位是什么,低位又是什么呢? 其實很簡單。在十進制中我們都說靠左邊的是高位,靠右邊的是低位,在其他進制也是如此。就拿 0x12345678來說,從高位到低位的字節依次是0x12、0x34、0x56和0x78。


高/低地址端和高/低字節都弄清了。我們再來回顧 一下Big-Endian和Little-Endian的定義,並用圖示說明兩種字節序:
以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) -- 低位
--------------
棧 頂 (低地址)

二、各種Endian
2.1 Big-Endian
計算機體系結構中一種描述多字節存儲順序的術語,在這種機制中最重要字節(MSB)存放在最低端的地址 上。采用這種機制的處理器有IBM3700系列、PDP-10、Mortolora微處理器系列和絕大多數的RISC處理器。MSB   the most significant byte


+----------+
| 0x34 |<-- 0x00000021
+----------+
| 0x12 |<-- 0x00000020
+----------+


圖 1:雙字節數0x1234以Big-Endian的方式存在起始地址0x00000020中

在Big-Endian中,對於bit序列中的序號編排方式如下(以雙字節數0x8B8A=(1000 1011 1000 1010)2為例):
bit 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+-----------------------------------------+
val | 1 0 0 0 1 0 1 1 | 1 0 0 0 1 0 1 0 |
+----------------------------------------+


圖 2:Big-Endian的bit序列編碼方式


2.2 Little-Endian
計算機體系結構中 一種描述多字節存儲順序的術語,在這種機制中最不重要字節(LSB)存放在最低端的地址上。采用這種機制的處理器有PDP-11、VAX、Intel系列微處理器和一些網絡通信設備。該術語除了描述多字節存儲順序外還常常用來描述一個字節中各個比特的排放次序。

+----------+
| 0x12 |<-- 0x00000021
+----------+
| 0x34 |<-- 0x00000020
+----------+

圖3:雙字節數0x1234以Little-Endian的方式存在起始地址0x00000020中

在 Little-Endian中,對於bit序列中的序號編排和Big-Endian剛好相反,其方式如下(以雙字節數0x8B8A=(1000 1011 1000 1010)2為例):
bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
+-----------------------------------------+
val | 1 0 0 0 1 0 1 1 | 1 0 0 0 1 0 1 0 |
+-----------------------------------------+
圖 4:Little-Endian的bit序列編碼方式


注意:通常我們說的主機序(Host Order)就是遵循Little-Endian規則。所以當兩台主機之間要通過TCP/IP協議進行通信的時候就需要調用相應的函數進行主機序 (Little-Endian)和網絡序(Big-Endian)的轉換。

采用 Little-endian模式的CPU對操作數的存放方式是從低字節到高字節,而Big-endian模式對操作數的存放方式是從高字節到低字節。 32bit寬的數0x12345678在Little-endian模式CPU內存中的存放方式(假設從地址0x4000開始存放)為:
                                          內存地址     0x4000     0x4001     0x4002     0x4003
                                          存放內容     0x78        0x56        0x34         0x12


而在Big- endian模式CPU內存中的存放方式則為:
                                          內存地址     0x4000     0x4001     0x4002     0x4003
                                          存放內容     0x12         0x34        0x56         0x78


具體的區別如下:


三、Big-Endian和Little-Endian優缺點
Big-Endian優點:

首先提取高位字節,你總是可以由看看在偏移位置為0的字節來確定這個數字是正數還是負數(因為低地址存放高字節)。你不必知道這個數值有多長,或者你也不必過一些字節來看這個數值是否含有符號位。這個數值是以它們被打印出來的順序存放的,所以從二進制到十進制的函數特別有效。因而,對於不同要求的機器,在設計存取方式時就會不同。

Little-Endian優點:

提取一個,兩個,四個或者更長字節數據的匯編指令以與其他所有格式相同的方式進行:首先在偏移地址為0的地方提取最低位的字節,因為地址偏移和字節數是一對一的關系,多重精度的數學函數就相對地容易寫了。

如果你增加數字的值,你可能在左邊增加數字(高位非指數函數需要更多的數字)。 因此, 經常需要增加兩位數字並移動存儲器里所有Big-endian順序的數字,把所有數向右移,這會增加計算機的工作量。不過,使用Little- Endian的存儲器中不重要的字節可以存在它原來的位置,新的數可以存在它的右邊的高位地址里。這就意味着計算機中的某些計算可以變得更加簡單和快速。


四、請寫一個C函數,若處理器是Big_endian的,則返回0;若是Little_endian的,則返回1。(很經典的判斷處理器存儲模式的方法

補充知識點:聯合union

能在同一個存儲空間里(不同時地)存儲不同類型數據的數據類型!是一個新的數據類型!典型應用是:表,使用聯合類型的數組……聯合和結構的建立方式一樣,一個聯合模版和一個聯合變量,可以一步到位的定義。也可以分步定義。

 1 #include <stdio.h>
 2 
 3 #include <stdlib.h>
 4 
 5 //聲明定義基本上類似於結構,但是初始化有些區別
 6 
 7 //聯合只存儲一個值,所以初始化的規則和結構的初始化規則不同
 8 
 9 union hold{
10 
11        int digit;
12 
13        double bigfl;
14 
15        char letter;
16 
17 };
18 
19 int main(void)
20 
21 {
22 
23        union hold fit;//hold類型的聯合變量
24 
25        union hold save[10];//一個save數組,包含10個聯合變量的數組
26 
27        union hold *p;//指向hold類型的聯合變量的指針,指向了聯合的地址
28 
29        system("pause");
30 
31        return 0;
32 
33 }

聯合的初始化問題(三個方法)

       //三種初始化聯合的方法

       //直接初始化聯合的第一個元素

       union hold valB = {10};//指定初始化聯合內的第一個成員變量,digit

       //同類型的聯合之間互相初始化

       union hold valA = valB;//用另外一個聯合初始化一個聯合

       //使用指定初始化項目

       union hold valD = {.bigfl = 118.222};//指定只初始化double類型的bigfl項目

注意:聯合的使用

//聯合的使用類似結構,使用點運算符訪問聯合正在保存的值,因為聯合同一個時間只能存儲一個值!即使空間足夠,也不能同時保存多個值!

       //程序員自己記憶保存的當前值!

       union hold *p;

       valA.digit = 10;

       //對於指向聯合的指針也類似結構指針,使用->運算符訪問成員變量

       p = &valA;//指針指向了聯合變量valA(地址)

       int x = p -> digit;//相當於int x = valA.digit

 

注意:聯合使用的誤區和用處

因為聯合同時只能存儲一個值,那么可以使用聯合結合結構編寫程序,進行獨一無二的選擇問題的時候使用,是最佳的!聯合一定注意,同一時刻只存儲一個值!

       double d;

       union hold fit;//聲明一個聯合變量fit

       fit.letter = 'a';//此時此刻,聯合內存放的是一個char類型字符a

       d = 1.1 * fit.bigfl;//錯誤的語句,因為本語句認為了聯合變量fit存儲的是double類型的成員bigfl

 

總結:結構和聯合的區別

1. 結構和聯合都是由多個不同的數據類型成員組成, 但在任何同一時刻, 聯合中只存放了一個被選中的成員, 而結構的所有成員都存在。 

2. 對於聯合的不同成員賦值, 將會對其它成員重寫,  原來成員的值就不存在了, 而對於結構的不同成員賦值是互不影響的。

3. 當聯合創建之后,聲明一個聯合變量,編譯器自動分配足夠的空間給它,選取聯合內部成員,內存空間最大的一個數據類型。

 

故,我們實現判斷cpu存儲模式的方式,可以利用聯合來解決,下面是原作者給出的解決辦法:

 1 int checkCPU(void)
 2 {
 3     union
 4     {
 5         int a;
 6         char b;
 7     }c;
 8     c.a = 1;
 9     return (c.b == 1);
10 }
 剖析:由於聯合體union的存放順序是所有成員都從低地址開始存放, 1的16進制為:0x00000001。利用該特性就可以輕松地獲得了CPU對內存采用Little- endian還是Big-endian模式讀寫。
 
如果cpu是大端模式,那么返回為假(0),cpu是小端模式存儲,那么返回真(1)。 比如lz的cpu是intel酷睿,按道理是小端模式存儲數據的,那么我試了試,果不其然。
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 int main(void)
 5 {
 6     union {
 7         int a;//作者這里這樣設置就是為了使用不同字節長度的類型來得到不同的數值
 8         char b;
 9     } u;
10 
11     u.a = 1;//給a賦值的同時,b也被覆蓋了,那么因為union是從低地址開始存儲數據,大家再看原作者的解釋,就更加一目了然
12 
13     if (u.b == 1)
14     {
15         puts("lz的電腦的cpu是小端存儲的!");
16     }
17     else
18     {
19         puts("lz的電腦的cpu是打斷存儲的!");
20     }
21 
22     system("pause");
23     return 0;
24 }
 
下面原作者也做了具體解釋

說明:
1  在c中,聯合體(共用體)的數據成員都是從低地址開始存放。


2  若是小端模式,由低地址到高地址,c.a存放為0x01 00 00 00,c.b被賦值為0x01;
  ————————————————————————————
   地址 0x00000000 0x00000001 0x00000002 0x00000003
   c.a  01         00         00         00
   c.b  01         00        
  ————————————————————————————  


3  若是大端模式,由低地址到高地址,c.a存放為0x00 00 00 01,c.b被賦值為0x0;
  ————————————————————————————
   地址 0x00000000 0x00000001 0x00000002 0x00000003
   c.a  00         00         00         01
   c.b  00         00                 
  ————————————————————————————  


4  根據c.b的值的情況就可以判斷cpu的模式了。

舉例,一個16進制數是 0x11 22 33,其存放的位置是
地址0x3000 中存放11
地址0x3001 中存放22
地址0x3002 中存放33
連起來就寫成地址0x3000-0x3002中存放了數據0x112233
而這種存放和表示方式,正好符合大端。

另外一個比較好理解的寫法如下:

 1 bool checkCPU()     // 如果是大端模式,返回真
 2 {
 3     short int test = 0x1234;
 4 
 5     if( *((char *)&test) == 0x12)     // 低地址存放高字節數據
 6         return true;
 7     else
 8         return false;
 9 }
10 
11 int main(void)
12 {
13     if( !checkCPU())
14         cout<<"Little endian"<<endl;
15     else
16         cout<<"Big endian"<<endl;
17 
18     return 0;
19 }

短整型test,32位機器里是2個字節大小存儲空間,0x1234,取地址,強轉為一字節的長度,截取的后8位,那么再次取值,和0x12比較。如果是大端模式,則剛好為0x12,下面原作者又給出了一個方案:其實大同小異。本質一樣。

或者下面兩種寫法也是可以的

 1 int main(void)
 2 {
 3     short int a = 0x1234;
 4     char *p = (char *)&a;
 5 
 6     if( *p == 0x34)
 7         cout<<"Little endian"<<endl;
 8     else
 9         cout<<"Big endian"<<endl;
10 
11     return 0;
12 }
13 
14 int main(void)
15 {
16     short int a = 0x1234;
17     char x0 , x1;
18 
19     x0 = ((char *)&a)[0];
20     x1 = ((char *)&a)[1];
21 
22     if( x0 == 0x34)
23         cout<<"Little endian"<<endl;
24     else
25         cout<<"Big endian"<<endl;
26 
27     return 0;
28 }

再次向原作者致敬!

 

歡迎關注

dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM