|
很多人討厭碰到字節(jié)序問題,跟它打交道就像走迷宮,每次都要犧牲不少腦細胞。即使這一次似乎搞清楚了,下次碰到還是要重新在大腦里構(gòu)建和模擬。這里盡量做一個字節(jié)序問的完整備忘記錄。
主機字節(jié)序
多字節(jié)數(shù)據(jù)在內(nèi)存中的字節(jié)排列順序稱為主機字節(jié)序,主機字節(jié)序基本由CPU硬件決定,某些CPU如X86、Z80等為little-endian;有些如moto6800、sparc等為big-endian;而有些CPU可通過寄存器設(shè)置支持不同字節(jié)序,如ARM、MIPS等,這類平臺上不同OS可能配置了不同字節(jié)序。
理解字節(jié)序需要知道兩個名詞:MSB (Most Significant Byte),代表一個數(shù)中權(quán)值最大的字節(jié);LSB(Least Significant Byte),代表一個數(shù)中權(quán)值最小的字節(jié)。比如十進制數(shù)1234,1權(quán)值為千位,最大,相當于MSB;4為個位,權(quán)值最小,相當于LSB。那么真正以字節(jié)為單位考慮, 4字節(jié)數(shù)0x12345678里那個是MSB/LSB呢?
主機字節(jié)序分Big-Endian和Little-Endian兩種,定義為:
a. Little-Endian,又稱低字節(jié)優(yōu)先,是把LSB放在內(nèi)存低地址端,MSB在內(nèi)存高地址端。舉例,4字節(jié)數(shù)0x12345678在little-endian體系的內(nèi)存中存放(從地址0x1000開始)為:
b. Big-Endian,又稱高字節(jié)優(yōu)先,把MSB放在內(nèi)存低地址端,LSB放在內(nèi)存高地址端。0x12345678此時存放為:
big-endian沿地址增長方向先放高權(quán)位數(shù)字的方式恰好符合一般習慣(按照千,百,十,個位來書寫數(shù)字)。這也是為什么初學者變換endian時總覺得big-endian比較正常。
字節(jié)序問題有時會被擴大化,有人一碰到奇怪問題就懷疑字節(jié)序。其實它只存在于多字節(jié)數(shù)據(jù)在內(nèi)存中的解析,注意:1)多字節(jié)數(shù)據(jù);2)內(nèi)存中的排列。
a. 多字節(jié)數(shù)據(jù)是指諸如long int short等需要多個字節(jié)存儲的數(shù)據(jù)類型,而象char等用一個字節(jié)表示的類型永遠不會有字節(jié)序問題。
b. CPU通用寄存器都是整體操作,不存在單字節(jié)地址以及地址增長方向的概念,因此也沒有MSB/LSB在前或在后的問題,只有內(nèi)存中的多字節(jié)數(shù)據(jù)存儲才會有這兩種差異。從硬件角度看,CPU是直接通過數(shù)據(jù)總線連接內(nèi)存和CPU寄存器,這中間沒有額外字節(jié)序轉(zhuǎn)換的硬件開銷,只是不同總線連接方式導致了不同字節(jié)序,如下圖(from wiki):
信息交換中的字節(jié)序
當不同類型平臺通過某種媒介交換信息時需要考慮字節(jié)序問題,常見介質(zhì)主要指文件和網(wǎng)絡(luò)。通過文件或網(wǎng)絡(luò)讀取來自其它主機的多字節(jié)數(shù)據(jù)時,要警惕字節(jié)序問題。如x86和moto6800主機通過文件交換信息時,如果不轉(zhuǎn)換,數(shù)據(jù)處理就不匹配。在x86 VC下運行如下代碼:
void main( )
{
FILE* fp;
short a = 0x3132; //為ASIIC碼’12’
fp = fopen ("c:test.txt", "wb")
fwrite(&a, sizeof(short), 1, fp);
fclose(fp);
}
代碼中a的值0x3132即'12’,由于x86為little endian體系,內(nèi)存中多字節(jié)LSB(這里即0x32)在前,所以運行后打開test.txt文件,內(nèi)容是'21'。把test.txt文件復制到moto6800機器上,再用fread把文件內(nèi)容讀到short變量里,得到的就不是0x3132而是0x3231了,數(shù)據(jù)就此被顛倒,后續(xù)處理完全錯誤。
假設(shè)我們制定了介質(zhì)層字節(jié)序標準(如big-endian),要通過軟件屏蔽不同endian體系的差異,需要兩步操作,一判斷endian類型:
typedef union {
u16 a;
u8 b[2];
}ENDIAN;
/* judge cpu is big endian(0) or small endian(1) */
bool judge_endian(void)
{
ENDIAN t;
t.a = 0x0001U;
return (bool)(t.b[0] == 1);
}
想想指針怎么做?
判斷完兩端主機的endian之后,要根據(jù)情況做相應(yīng)endian交換,即發(fā)送和接收任何一方只要不符合媒介層標準(big-endian),就要通過移位和位操作,在主機序和媒介序之間轉(zhuǎn)換。一方發(fā)送數(shù)據(jù)前要先將內(nèi)存數(shù)據(jù)由主機字節(jié)序轉(zhuǎn)換為傳輸介質(zhì)層的字節(jié)序,再發(fā)送出去,接收方收到數(shù)據(jù)后,要轉(zhuǎn)換為本地的主機字節(jié)序,再做后續(xù)處理。
比如TCP/IP協(xié)議的socket通信,基于socket通訊的雙方一般選擇Big-Endian為標準,又稱網(wǎng)絡(luò)字節(jié)序。socket定義了一組轉(zhuǎn)換函數(shù),用于多字節(jié)數(shù)在網(wǎng)絡(luò)字節(jié)序和主機字節(jié)序之間的轉(zhuǎn)換。htonl,htons用于主機序轉(zhuǎn)換到網(wǎng)絡(luò)序(如主機本身big endian,函數(shù)實際啥也不做);ntohl,ntohs把網(wǎng)絡(luò)序轉(zhuǎn)換到本機序(同樣)。因此傻瓜式做法,無論發(fā)送方主機字節(jié)序是什么,發(fā)送前都用htons或htonl將多字節(jié)數(shù)據(jù)轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序;同樣無論接收方主機字節(jié)序是什么,都用ntohs或ntohl把接收的網(wǎng)絡(luò)數(shù)據(jù)轉(zhuǎn)換為本地主機字節(jié)序。這樣有了網(wǎng)絡(luò)字節(jié)序標準,socket通信時,只需套用這幾個函數(shù)就能方便地抹平主機間的字節(jié)序差異。
問題在于如果雙方主機都與網(wǎng)絡(luò)字節(jié)序相反,本來是可以直接通信的,卻用htonl/ntohl等轉(zhuǎn)過去又轉(zhuǎn)回來,有點自找麻煩。這就要看軟件的預(yù)期運行環(huán)境,以及怎樣平衡軟件效率和多平臺通用性。
補充SOC內(nèi)部的字節(jié)序
soc中除主CPU外,一些外設(shè)中(如硬件媒體編解碼器)還包含內(nèi)部MCU,它們之間一般通過高速總線共享外部DDR,通過低速總線共享外設(shè)寄存器。這時如果雙方的endian屬性不匹配,對host cpu驅(qū)動及MCU上的固件編寫會造成一定困擾。
總結(jié)
真正理解大小端的本質(zhì)可能要從硬件系統(tǒng)角度,涉及CPU指令集、寄存器、總線以及硬件外設(shè)等。但對程序員來說,能夠了解字節(jié)序問題的存在范圍以及如何實現(xiàn)移植性更高的代碼,這就足夠了。
Tip:對于ARM等可配置的CPU,其編譯工具中會有big/little endian的選項,程序移植時也需要注意,如果跟系統(tǒng)設(shè)置不匹配,編譯得到的目標程序就無法運行。
以下課程可免費試聽C語言、電子、PCB、STM32、Linux、FPGA、JAVA、安卓等。
想學習的你和我聯(lián)系預(yù)約就可以免費聽課了。
宋工企鵝號:3524-6590-88 Tel/WX:173--1795--1908
|
|