天天看點

[Linux]關于位元組序的解析

剝雞蛋的故事

《格列佛遊記》中記載了兩個征戰的強國,你不會想到的是,他們打仗竟然和剝雞蛋的姿勢有關。

很多人認為,剝雞蛋時應該打破雞蛋較大的一端,這群人被稱作“大端(Big endian)派”。可是當今皇帝的祖父小時候吃雞蛋的時候碰巧将一個手指弄破了。是以,他的父親(當時的皇帝)就下令剝雞蛋必須打破雞蛋較小的一端,違令者重罰,由此産生了“小端(Little endian)派”。

老百姓們對這項指令極其反感,由此引發了6次叛亂,其中一個皇帝送了命,另一個丢了王位。據估計,先後幾次有11000人情願受死也不肯去打破雞蛋較小的一端!

話題扯遠了,不過關于位元組序的big endian和little endian的命名确實來源于此典故;

在了解位元組序前,先來說說我們平時不那麼留意的閱讀習慣常識,如果我不說,你還真的反應不過來,反正我是這樣的。

1.數位閱讀習慣

人類在閱讀數字的時候一般的認知是從左到右這樣閱讀的,比如256,讀作“二百五十六”,大的數位在左邊小的依次往右,這是人類的閱讀習慣

2.文章閱讀習慣

人類在閱讀文章的時候一般的認知也是從左到右閱讀的,比如你正在閱讀的這段文字,我打賭你不會從最後一個字往回閱讀,這又是一個人類的閱讀習慣

開始了,我們都清楚,計算機世界裡面,最小的存儲是位元組(byte),就好像閱讀數字一樣,我們會把大的位數放在左邊

比如:

00010010

它等于十進制的18,很明顯這沒有任何問題,是以說,當一段文字它隻是包含一個數字的情況下,是不會出現位元組序問題的,人類都公認越往左邊就應該儲存位數越高的值,例如UTF-8,解析程式每次隻會取一個位元組出來進行解析,便不會存在位元組序的問題。

問題出現在當一段文字包含好幾個數字或者更多個數字的時候,打個比方,

UTF-16編碼,它是一種由兩個位元組構成的Unicode字元編碼方式,也就是說,無論儲存任何字元,它都要用到兩個位元組:

UTF-16編碼 4E2D 對應的是中文的“中”字,很明顯,要儲存這個“中”字,必須動用兩個位元組,于是問題來了,我是先儲存4E在左邊呢,還是先儲存2D在左邊呢?

就是這丁點事兒,不同計算機廠商和各個計算機技術協會幾乎打起來了,而且一直沒有解決問題,存在着争議;

好啦,來看看我們剛才提到的兩個人類閱讀習慣常識,來結合UTF-16的“中文”兩個字,就能說明問題!

很明顯“中文”不是一個文字,而是一段文字,按照人類對文章的閱讀習慣,肯定是先讀“中”後讀“文”,這個是沒有異議的,全世界都沒有異議,

是以,計算機的記憶體位址是從左往右排序的,左邊起是第一個然後是第二個。。。

 big endian:

大端序認為,按照人類對數字的閱讀習慣,把大的數字儲存在左邊是最合适的

little endian:

小端序認為,這個單元式的位元組段壓根就跟數字不是一回事,應該按照人類閱讀文章的習慣,把小的數字儲存在左邊

 公說公有理婆說婆有理,貌似不同的CPU廠商并沒有達成一緻:

  • x86,MOS Technology 6502,Z80,VAX,PDP-11等處理器為Little endian。
  • Motorola 6800,Motorola 68000,PowerPC 970,System/370,SPARC(除V9外)等處理器為Big endian。
  • ARM, PowerPC (除PowerPC 970外), DEC Alpha, SPARC V9, MIPS, PA-RISC and IA64的位元組序是可配置的。

大端也好,小端也罷,就權當是個人愛好吧,隻要你不影響别人就行,對不?

網絡位元組序

前面的大端和小端都是在說計算機自己,也被稱作主機位元組序。其實,隻要自己能夠自圓其說是沒啥問題的。問題是,網絡的出現使得計算機可以通信了。通信,就意味着相處,相處必須得有共同語言啊,得說國語,要不然就容易會錯意,下了一個小時的小電影發現打不開,了解錯誤了!

但是每個計算機都有自己的主機位元組序啊,還都不依不饒,堅持做自己,怎麼辦?

TCP/IP協定隆重出場,RFC1700規定使用“大端”位元組序為網絡位元組序,其他不使用大端的計算機要注意了,發送資料的時候必須要将自己的主機位元組序轉換為網絡位元組序(即“大端”位元組序),接收到的資料再轉換為自己的主機位元組序。這樣就與CPU、作業系統無關了,實作了網絡通信的标準化。突然覺得,TCP/IP協定好任性啊有木有!

為了程式的相容,你會看到,程式員們每次發送和接受資料都要進行轉換,這樣做的目的是保證代碼在任何計算機上執行時都能達到預期的效果。

這麼常用的操作,BSD Socket提供了封裝好的轉換接口,友善程式員使用。包括從主機位元組序到網絡位元組序的轉換函數:htons、htonl;從網絡位元組序到主機位元組序的轉換函數:ntohs、ntohl。當然,有了上面的理論基礎,也可以編寫自己的轉換函數。

下面的一段代碼可以用來判斷計算機是大端的還是小端的,判斷的思路是确定一個多位元組的值(下面使用的是4位元組的整數),将其寫入記憶體(即指派給一個變量),然後用指針取其首位址所對應的位元組(即低位址的一個位元組),判斷該位元組存放的是高位還是低位,高位說明是Big endian,低位說明是Little endian。

#include <stdio.h>
int main ()
{
  unsigned int x = 0x12345678;
  char *c = (char*)&x;
  if (*c == 0x78) {
    printf("Little endian");
  } else {
    printf("Big endian");
  }
  return 0;
}      

身邊的位元組序

字元編碼方式UTF-16、UTF-32同樣面臨位元組序的問題,因為他們分别使用2個位元組和4個位元組編碼Unicode字元,一旦某個值用多個位元組表示,就必須要考慮存儲的順序了。于是,采用了最簡單粗暴的方式,給檔案頭部寫幾個字元,用來表示是大端呢還是小端:

頭部的字元 編碼 位元組序 FF FE UTF-16/UCS-2 Little endian FE FF UTF-16/UCS-2 Big endian FF FE 00 00 UTF-32/UCS-4 Little endian 00 00 FE FF UTF-32/UCS-4 Big-endian

這裡不得不提一下UTF-8啊,明明人家是單個位元組的,不存在什麼位元組序的問題。微軟為了統一UTF-X,硬生生給他的頭部也加了幾個字元!是的,這幾個字元就是BOM(Byte Order Mark),這就是Windows下的UTF-8。

相信很多人都被UTF-8的BOM給坑過,多了這個BOM的UTF-8檔案,會導緻很多問題啊。比如,寫的Shell腳本,内容為#!/usr/bin/env bash,在UTF-8有BOM和UTF-8無BOM的編碼下,對應的16進制為: 

是以,有BOM的話,Shell解釋器就報錯啦。原因在于,解釋器希望遇到#!/usr/bin/env bash,而使用UTF-8有BOM進行編碼的内容會多了3個位元組的EF BB BF。

對于UTF-8和UTF-8無BOM兩種編碼格式,我們更多的使用UTF-8無BOM。