深入剖析大小端
1.大小端問題的由來
關(guān)于大端小端名詞的由來,有一個有趣的故事,來自于Jonathan Swift的《格利佛游記》:Lilliput和Blefuscu這兩個強國在過去的36個月中一直在苦戰(zhàn)。戰(zhàn)爭的原因:大家都知道,吃雞蛋的時候,原始的方法是打破雞蛋較大的一端,可以那時的皇帝的祖父由于小時侯吃雞蛋,按這種方法把手指弄破了,因此他的父親,就下令,命令所有的子民吃雞蛋的時候,必須先打破雞蛋較小的一端,違令者重罰。然后老百姓對此法令極為反感,期間發(fā)生了多次叛亂,其中一個皇帝因此送命,另一個丟了王位,產(chǎn)生叛亂的原因就是另一個國家Blefuscu的國王大臣煽動起來的,叛亂平息后,就逃到這個帝國避難。據(jù)估計,先后幾次有11000余人情愿死也不肯去打破雞蛋較小的端吃雞蛋。這個其實諷刺當(dāng)時英國和法國之間持續(xù)的沖突。Danny Cohen一位網(wǎng)絡(luò)協(xié)議的開創(chuàng)者,第一次使用這兩個術(shù)語指代字節(jié)順序,后來就被大家廣泛接受。
當(dāng)然啦,這和我們現(xiàn)在的大小端就有些遠(yuǎn)啦,回歸正題,在計算機內(nèi)存中,通常是以字節(jié)(Byte),也就是 8 個位(Bit)為基本存儲單元(也有以 16 位為基本存儲單元的)。
對于像C++中的char這樣的數(shù)據(jù)類型,占用一個字節(jié)的大小,不會產(chǎn)生什么問題。
但是當(dāng)數(shù)據(jù)類型為int,在32bit的中,它需要占用4個字節(jié)(32bit),這個時候就會產(chǎn)生這4個字節(jié)在寄存器中的存放順序的問題。比如int maxHeight = 0x12345678,&maxHeight = 0x0042ffc4。具體的該怎么存放呢?這個時候就需要理解計算機的大小端的原理了。
2.大小端的原理
大端:(Big-Endian):就是把數(shù)值的高位字節(jié)放在內(nèi)存的低位地址上,把數(shù)值的低位字節(jié)放在內(nèi)存的高位地址上。
小端:(Little-Endian):就是把數(shù)值的高位字節(jié)放在高位的地址上,低位字節(jié)放在低位地址上。
【注】不管是大端法還是小端法存儲,計算機在內(nèi)存中存放數(shù)據(jù)的順序都是從低地址到高地址,所不同的是首先取低字節(jié)的數(shù)據(jù)存放在低地址還是取高字節(jié)數(shù)據(jù)存放在低地址。
大端法和小端法指的是字節(jié)在內(nèi)存中存儲時的排列規(guī)則,而不是數(shù)據(jù)中的位的排列規(guī)則。也有以位序排列的機器,但很少見。另外,再次明確一下,大端法或小端法是數(shù)據(jù)在存儲時的表現(xiàn),而不是在寄存器中參與運算時的表現(xiàn)。
3.大小端比較
我們常用的x86結(jié)構(gòu)都是小端模式,而大部分DSP,ARM也是小端模式,而KEIL C51則為大端模式,而通訊協(xié)議是大端的,因此在網(wǎng)絡(luò)編程時需要經(jīng)字節(jié)序轉(zhuǎn)換。不過有些ARM是可以選擇大小端模式。所以對于上面的maxHeight是應(yīng)該以小端模式來存放,具體情況請看下面兩表。
(1)小端規(guī)則
| 數(shù)據(jù) | 0x78 | 0x56 | 0x34 | 0x12 |
(2)大端規(guī)則
| 數(shù)據(jù) | 0x12 | 0x34 | 0x56 | 0x78 |
通過上面的表格,可以看出來大小端的不同。(注:其實在計算機內(nèi)存中并不存在所謂的數(shù)據(jù)類型,比如char,int等的。這個類型在代碼中的作用就是讓編譯器知道每次應(yīng)該從那個地址起始讀取多少位的數(shù)據(jù),賦值給相應(yīng)的變量。)
4.存放順序
(1)整數(shù)類型內(nèi)部:低地址存儲低位,高地址存儲高位。
#include <stdio.h>union NUM {short int a;char b[2]; }num;int main(int argc ,char **argv ) {num.a=0x1234;printf("%x %x",num.b[0],num.b[1]);return 0; }組合的時候,整數(shù)類型內(nèi)部低地址存儲低位,高地址存儲高位,最后的答應(yīng)結(jié)果應(yīng)該是34 12.
(2)若干個局部變量(在棧中存儲的):先定義的高地址,后定義的低地址
#include<stdio.h>int main(int argc ,char **argv ) {int i,j;printf("%x %x",&i,&j);return 0; }輸出結(jié)果為61ff2c 61ff28,可以看出局部變量是先定義高地址,后定義低地址,其實也就是說棧中存儲是從高地址向地址存儲的。
(3)若干個全局變量或靜態(tài)變量:先定義的低地址,后定義的高地址
#include <stdio.h>int i,j; int a[2];int main(int argc ,char **argv ) {static int m;static int n;printf("%x %x\n",&i,&j);printf("%x %x %x\n",&a,&a[0],&a[1]);printf("%x %x\n",&m,&n);return 0; }結(jié)果如下所示:
407020 407024 407028 407028 40702c 407034 407038可以看出,先定義的低地址,后定義的高地址,而且先定義的全局變量,在定義的靜態(tài)變量。
(4)類、結(jié)構(gòu)體或數(shù)組的元素:先定義的低地址,后定義的高地址。
#include <iostream>class Test{ public:int m;int n; };struct NUM {short int a;char b; };int main(int argc ,char **argv ) {int a[2];struct NUM num;Test t;printf("%x %x %x\n",&a,&a[0],&a[1]);printf("%x %x %x\n",&num, &num.a,&num.b);printf("%x %x %x",&t,&t.m,&t.n);return 0; }結(jié)果如下所示:
61ff28 61ff28 61ff2c 61ff24 61ff24 61ff26 61ff1c 61ff1c 61ff20從上面的結(jié)果來看,a,num,t屬于局部變量,存放在棧區(qū),是由高地址向低地址存儲,而在數(shù)組、結(jié)構(gòu)體、類中則是以低地址開始存儲。
總的來說,具體的地址,需要考慮“棧的高地址到低地址”、“字節(jié)對齊”、“數(shù)組”這樣的特殊情況等等。
5.大小端判斷
大端是指低字節(jié)存儲在高地址;小端存儲是指低字節(jié)存儲在低地址。
方法一:指針法
bool IsBigEndian() {short int a = 0x1234;char b = *(char *)&a; //通過將int強制類型轉(zhuǎn)換成char單字節(jié),通過判斷起始存儲位置。即等于 取b等于a的低地址部分if( b == 0x12){return true;}return false; }方法二:聯(lián)合體
我們可以根據(jù)聯(lián)合體來判斷該系統(tǒng)是大端還是小端。因為聯(lián)合體變量總是從低地址存儲。
typedef enum{false,true }bool; bool IsBigEndian() {union NUM{short int a;char b;}num;num.a = 0x1234;if( num.b == 0x12 ){return true;}return false; }上面的共用體變量un,大小sizeof(num)=2,
注意使用共用體(聯(lián)合)判斷時,不能僅依靠公用類型最大者為其共用體變量大小,應(yīng)遵守對其原則和補齊原則。
對齊原則:
結(jié)構(gòu)體變量中元素是按照定義順序一個一個放到內(nèi)存中去的,但并不是緊密排列的。從結(jié)構(gòu)體存儲的首地址開始,每一個元素放置到內(nèi)存中時,它都會認(rèn)為內(nèi)存是以它自己的大小來劃分的,因此元素放置的位置一定會在自己寬度的整數(shù)倍上開始(以結(jié)構(gòu)體變量首地址為0計算)。
補齊原則:
檢查計算出的存儲單元是否為所有元素中所占內(nèi)存最大的元素的長度的整數(shù)倍,是,則結(jié)束;若不是,則補齊為它的整數(shù)倍。
舉一個例子:
union Un {int i;char arr[5]; };sizeof(union Un)=8
6.大小端轉(zhuǎn)換
對于字?jǐn)?shù)據(jù)(16位):
#define BigtoLittle16(A) ((((uint16)(A) & 0xff00) >> 8) | \(((uint16)(A) & 0x00ff) << 8))對于雙字?jǐn)?shù)據(jù)(32位):
#define BigtoLittle32(A) ((( (uint32)(A) & 0xff000000) >> 24) | \(( (uint32)(A) & 0x00ff0000) >> 8) | \(( (uint32)(A) & 0x0000ff00) << 8) | \(( (uint32)(A) & 0x000000ff) << 24))7.大小端的理解
從軟件的角度理解端模式
從軟件的角度上,不同端模式的處理器進行數(shù)據(jù)傳遞時必須要考慮端模式的不同。如進行網(wǎng)絡(luò)數(shù)據(jù)傳遞時,必須要考慮端模式的轉(zhuǎn)換。在Socket接口編程中,以下幾個函數(shù)用于大小端字節(jié)序的轉(zhuǎn)換。
#define ntohs(n) //16位數(shù)據(jù)類型網(wǎng)絡(luò)字節(jié)順序到主機字節(jié)順序的轉(zhuǎn)換 #define htons(n) //16位數(shù)據(jù)類型主機字節(jié)順序到網(wǎng)絡(luò)字節(jié)順序的轉(zhuǎn)換 #define ntohl(n) //32位數(shù)據(jù)類型網(wǎng)絡(luò)字節(jié)順序到主機字節(jié)順序的轉(zhuǎn)換 #define htonl(n) //32位數(shù)據(jù)類型主機字節(jié)順序到網(wǎng)絡(luò)字節(jié)順序的轉(zhuǎn)換其中互聯(lián)網(wǎng)使用的網(wǎng)絡(luò)字節(jié)順序采用大端模式進行編址,而主機字節(jié)順序根據(jù)處理器的不同而不同,如PowerPC處理器使用大端模式,而Pentuim處理器使用小端模式。
大端模式處理器的字節(jié)序到網(wǎng)絡(luò)字節(jié)序不需要轉(zhuǎn)換,此時ntohs(n)=n,ntohl = n;而小端模式處理器的字節(jié)序到網(wǎng)絡(luò)字節(jié)必須要進行轉(zhuǎn)換,此時ntohs(n) =__swab16(n),ntohl = __swab32(n)。__swab16與__swab32函數(shù)定義如下所示。
#define ___swab16(x) {__u16 __x = (x);((__u16)((((__u16)(__x) & (__u16)0x00ffU) << 8) |(((__u16)(__x) & (__u16)0xff00U) >> 8) )); } #define ___swab32(x) {__u32 __x = (x);((__u32)((((__u32)(__x) & (__u32)0x000000ffUL) << 24) |(((__u32)(__x) & (__u32)0x0000ff00UL) << 8) |(((__u32)(__x) & (__u32)0x00ff0000UL) >> 8) |(((__u32)(__x) & (__u32)0xff000000UL) >> 24) )); }PowerPC處理器提供了lwbrx,lhbrx,stwbrx,sthbrx四條指令用于處理字節(jié)序的轉(zhuǎn)換以優(yōu)化__swab16和__swap32這類函數(shù)。此外PowerPC處理器中的rlwimi指令也可以用來實現(xiàn)__swab16和__swap32這類函數(shù)。
在對普通文件進行處理也需要考慮端模式問題。在大端模式的處理器下對文件的32,16位讀寫操作所得到的結(jié)果與小端模式的處理器不同。單純從軟件的角度理解上遠(yuǎn)遠(yuǎn)不能真正理解大小端模式的區(qū)別。事實上,真正的理解大小端模式的區(qū)別,必須要從系統(tǒng)的角度,從指令集,寄存器和數(shù)據(jù)總線上深入理解,大小端模式的區(qū)別。
從系統(tǒng)的角度理解端模式
先補充兩個關(guān)鍵詞,MSB和LSB:
MSB:MoST Significant Bit ------- 最高有效位
LSB:Least Significant Bit ------- 最低有效位
處理器在硬件上由于端模式問題在設(shè)計中有所不同。從系統(tǒng)的角度上看,端模式問題對軟件和硬件的設(shè)計帶來了不同的影響,當(dāng)一個處理器系統(tǒng)中大小端模式同時存在時,必須要對這些不同端模式的訪問進行特殊的處理。
PowerPC處理器主導(dǎo)網(wǎng)絡(luò)市場,可以說絕大多數(shù)的通信設(shè)備都使用PowerPC處理器進行協(xié)議處理和其他控制信息的處理,這也可能也是在網(wǎng)絡(luò)上的絕大多數(shù)協(xié)議都采用大端編址方式的原因。因此在有關(guān)網(wǎng)絡(luò)協(xié)議的軟件設(shè)計中,使用小端方式的處理器需要在軟件中處理端模式的轉(zhuǎn)變。而Pentium主導(dǎo)個人機市場,因此多數(shù)用于個人機的外設(shè)都采用小端模式,包括一些在網(wǎng)絡(luò)設(shè)備中使用的PCI總線,Flash等設(shè)備,這也要求在硬件設(shè)計中注意端模式的轉(zhuǎn)換。
本文提到的小端外設(shè)是指這種外設(shè)中的寄存器以小端方式進行存儲,如PCI設(shè)備的配置空間,NOR FLASH中的寄存器等等。對于有些設(shè)備,如DDR顆粒,沒有以小端方式存儲的寄存器,因此從邏輯上講并不需要對端模式進行轉(zhuǎn)換。在設(shè)計中,只需要將雙方數(shù)據(jù)總線進行一一對應(yīng)的互連,而不需要進行數(shù)據(jù)總線的轉(zhuǎn)換。
如果從實際應(yīng)用的角度說,采用小端模式的處理器需要在軟件中處理端模式的轉(zhuǎn)換,因為采用小端模式的處理器在與小端外設(shè)互連時,不需要任何轉(zhuǎn)換。而采用大端模式的處理器需要在硬件設(shè)計時處理端模式的轉(zhuǎn)換。大端模式處理器需要在寄存器,指令集,數(shù)據(jù)總線及數(shù)據(jù)總線與小端外設(shè)的連接等等多個方面進行處理,以解決與小端外設(shè)連接時的端模式轉(zhuǎn)換問題。在寄存器和數(shù)據(jù)總線的位序定義上,基于大小端模式的處理器有所不同。
一個采用大端模式的32位處理器,如基于E500內(nèi)核的MPC8541,將其寄存器的最高位msb(most significant bit)定義為0,最低位lsb(lease significant bit)定義為31;而小端模式的32位處理器,將其寄存器的最高位定義為31,低位地址定義為0。與此向?qū)?yīng),采用大端模式的32位處理器數(shù)據(jù)總線的最高位為0,最高位為31;采用小端模式的32位處理器的數(shù)據(jù)總線的最高位為31,最低位為0。
大小端模式處理器外部總線的位序也遵循著同樣的規(guī)律,根據(jù)所采用的數(shù)據(jù)總線是32位,16位和8位,大小端處理器外部總線的位序有所不同。大端模式下32位數(shù)據(jù)總線的msb是第0位,MSB是數(shù)據(jù)總線的第0~ 7的字段;而lsb是第31位,LSB是第24~ 31字段。小端模式下32位總線的msb是第31位,MSB是數(shù)據(jù)總線的第31~ 24位,lsb是第0位,LSB是7~ 0字段。大端模式下16位數(shù)據(jù)總線的msb是第0位,MSB是數(shù)據(jù)總線的第0~ 7的字段;而lsb是第15位,LSB是第8~ 15字段。小端模式下16位總線的msb是第15位,MSB是數(shù)據(jù)總線的第15~ 7位,lsb是第0位,LSB是7~ 0字段。大端模式下8位數(shù)據(jù)總線的msb是第0位,MSB是數(shù)據(jù)總線的第0~ 7的字段;而lsb是第7位,LSB是第0~ 7字段。小端模式下8位總線的msb是第7位,MSB是數(shù)據(jù)總線的第7~ 0位,lsb是第0位,LSB是7~0字段。
由上分析,我們可以得知對于8位,16位和32位寬度的數(shù)據(jù)總線,采用大端模式時數(shù)據(jù)總線的msb和MSB的位置都不會發(fā)生變化,而采用小端模式時數(shù)據(jù)總線的lsb和LSB位置也不會發(fā)生變化。
為此,大端模式的處理器對8位,16位和32位的內(nèi)存訪問(包括外設(shè)的訪問)一般都包含第0~ 7字段,即MSB。小端模式的處理器對8位,16位和32位的內(nèi)存訪問都包含第7~ 0位,小端方式的第7~0字段,即LSB。由于大小端處理器的數(shù)據(jù)總線其8位,16位和32位寬度的數(shù)據(jù)總線的定義不同,因此需要分別進行討論在系統(tǒng)級別上如何處理端模式轉(zhuǎn)換。在一個大端處理器系統(tǒng)中,需要處理大端處理器對小端外設(shè)的訪問。
歡迎訪問我的網(wǎng)站:
BruceOu的嗶哩嗶哩
BruceOu的主頁
BruceOu的博客
CSDN博客
接收更多精彩文章及資源推送,請訂閱我的微信公眾號:
總結(jié)
- 上一篇: c语言打出的王字图形图形,C语言编程宝典
- 下一篇: CSS 样式修改技巧及心得汇总