“字節序”是個鬼


原文出處: https://zhuanlan.zhihu.com/p/21388517

論順序的重要性

做飯的故事

今天女朋友加班,機智的她早已在昨晚準備好食材,回家只需下鍋便可。誰知開會就是個無底洞,到了B1,還有B2,無窮匱也。

辛苦如她,爲了能讓她一回家就喫上熱騰騰的飯菜,我準備親自下廚,奉獻出我的第一次。食材都已備好,我相信沒有那麼難,估摸着應該和我習以爲常的流程處理差不多,開火 | 加食材 | 上配料 | 翻炒 | 出鍋,啊哈,想想還有點小激動。

今天的晚飯是西紅柿炒雞蛋和胡蘿蔔炒肉,實際操作才發現,又遇到了一個大坑……

食材是這樣的:

案板1號(西紅柿炒雞蛋的食材),從左向右依次放着:西紅柿、雞蛋、蔥
案板2號(胡蘿蔔炒肉的食材),從左向右依次放着:蒜、胡蘿蔔絲、肉

食材在案板上整齊劃一依次排開,我是先放西紅柿呢,還是先放雞蛋呢,還是先放蔥呢?簡單溝通後得知,案板上的食材是按順序放好的,我只需要按順序下鍋即可。聽着電視哼着90年代的老歌,三下五除二,兩道菜如期完成。

聞着怪味,我知道第一次就這麼失敗了。

等她回家,一番檢討後,才知道是順序放錯了。每道菜都應該是從右往左依次放食材,即蔥->雞蛋->西紅柿。這是逗我的麼!?一般人所理解的按默認順序不應該是從左往右嘛!

朋友們,到底應該是從左往右還是從右往左?

剝雞蛋的故事

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

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

大端序和小端序

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

看到沒有,僅僅是剝雞蛋就能產生這麼大的分歧,“大端”和“小端”有這麼重要嘛!

字節序

字節

字節(Byte)作爲計算機世界的計量單位,和大家手中的人民幣多少多少“元”一個意思。反正,到了計算機的世界,說字節就對了,使用人家的基本計量單位,這是入鄉隨俗。

比如,一個電影是1G個字節(1GB),一首歌是10M個字節(10MB),一張圖片是1K個字節(1KB)。

字節序

一元錢可以幹嘛?啥也幹不了,公交都不夠坐的。一個字節可以幹嘛?至少可以存一個字符。

當數據太大,一個字節存不下的時候,我們就得使用多個字節了。比如,我有兩個分別需要4個字節存儲的整數,爲了方便說明,使用16進製表示這兩個數,即0x12345678和0x11223344。有的人採用以下方式存儲這個兩個數字:

這個方案看起來不錯,但是,又有人採用了以下方式:

蒙圈了吧,到底該用哪一種方式來存!兩種方案雖有不同,但也有共識,即依次存儲每一個數字,即先存0x12345678,再存0x11223344。大家的分歧在於,對於某一個要表示的值,因爲只能一個字節一個字節的存嘛,我是把值的低位存到低地址,還是把值的高位存到低地址。前者使用的是“小端(Little endian)”字節序,即先存低位的那一端(兩個數字的最低位分別是0x78、0x44),如上圖中的第一個圖;後者使用的是“大端(Big endian)”字節序,即先存高位的那一端(兩個數字的最高位分別是0x12、0x11),如上圖中的第二個圖。

由此也引發了計算機界的大端與小端之爭,不同的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

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

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

END -

「技術分享」某種程度上,是讓作者和讀者,不那麼孤獨的東西。歡迎關注我的微信公衆號:「Kirito的技術分享」


本文分享自微信公衆號 - Kirito的技術分享(cnkirito)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章