計算機以二進制形式將數據存儲在內存中。經常被忽視的一件事是此數據的字節級別的格式。這稱為字節序,它指的是字節的順序。
具體來說,little-endian 是將最低有效字節存儲在更有效字節之前,而 big-endian 是將最高有效字節存儲在較低有效字節之前。
當我們寫一個數字(十六進制)時,即0x12345678,我們首先用最高有效字節(12部分)寫它。從某種意義上說,大端是寫東西的“正常”方式。
這篇文章將只討論整數的字節序,而不是浮點數,因為它變得更加復雜,定義也更少。
為什么這很重要?
關於字節序的一個重要區別是,它只涉及值如何存儲在內存中,而不是我們如何處理值;例如,0x12345678仍然是0x12345678。這里沒有字節序的概念。但是,如果我們談論將這個 4 字節值存儲到內存中,那么並且只有這樣我們才必須指定字節序。
如果我們使用 little-endian 將前面提到的值存儲到內存中,我們將得到以下結果。請注意,每個 2 個十六進制字母代表 1 個字節。
78 56 34 12
如果我們以大端存儲它,我們會得到:
12 34 56 78
最后,這就是字節序很重要的原因。因為不知道數據是如何存儲的會導致交流不同的值。
例如,所有x86_64處理器(Intel/AMD)都使用小端,而IP/TCP使用大端。這意味着為了讓您使用 Internet,您的計算機必須考慮字節順序的差異。
到目前為止看起來很簡單,對吧?
大多數混淆在於小端,所以我們將從那里開始。
提醒一下,little-endianness 是指首先存儲最低有效字節的字節順序。因此,例如,如果我們有 8 字節的值,0x123456789abcdef0我們將按以下方式將其存儲在內存中。(注意:我在值旁邊放了一個偽內存地址,這樣我們就可以說這個值在內存地址處0x00。)
0x00: f0 de bc 9a 78 56 34 12
這里要理解的最重要的事情是我們正在存儲一個 8 字節的值。另一方面,如果我們存儲一個 4 字節的值,我們仍然會翻轉字節順序,但只是針對這 4 個字節。以下面的數組為例。
int a[] = {0x12345678, 0x9abcdef0};
這個數組和 8 字節的數字一樣,總共占用 8 個字節,看起來非常相似。但是,在內存中,我們不會存儲與上面相同的內容,而是以下內容:
0x00: 78 56 34 12
0x04: f0 de bc 9a
請注意這里數組的順序是如何保留的,並且0x12345678單獨的值(前 4 個字節)是小端的。
理解這一點非常重要:我們不會以小端序任意存儲任何 8 字節,而是根據它們占用的大小以小端序存儲各個值。
作為最后一個示例,采用以下字符數組。
char s[] = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0};
正如您可能能夠預測的那樣,它以以下格式存儲。
0x00: 12
0x01: 34
0x02: 56
0x03: 78
0x04: 9a
0x05: bc
0x06: de
0x07: f0
再次,保持數組的順序。
現在,如果我們將其帶回 big-endian,我們可以看到這些示例中的每一個都以相同的方式存儲。
0x00: 12 34 56 78 9a bc de f0
這是因為 big-endian 是按照您看到事物的順序存儲的。我建議你自己證明這一點。
那么為什么每個人都會倒退呢?
不管乍一看似乎有悖常理,小端優先於大端的使用是有正當理由的。廣泛使用 little-endian 的原因不是因為用戶易於理解(您可能已經發現),而是因為計算機易於使用。讓我們來看看為什么。我們將使用這個 8 字節的值0x0000000000000042。當我們將它存儲在 little-endian 中時,我們有以下內容。
0x00: 42 00 00 00 00 00 00 00
在大端中我們會得到。
0x00: 00 00 00 00 00 00 00 42
現在假設我們要運行以下代碼。
// In the case of 64 bit compilers, long long is the same size as long. They are both 8 bytes.unsigned long long x = 0x0000000000000042;unsigned long long * x_p = &x;unsigned int * y_p = (unsigned int *)x_p;unsigned int y = *y_p;
printf("y = %#.8x\n", y); // prints in hex with '0x' and with all leading zeros
我們正在做一些叫做指針向下轉換的事情。我們不會改變內存中的任何東西,只是改變處理器從內存中讀取的方式。
需要注意的重要一點是x_p和y_p將具有相同的值(它們指向相同的位置)。我們會說它們都指向0x00。
當我們運行它時,根據處理器使用的字節順序,我們將得到兩種截然不同的結果。首先,讓我們假設我們使用的是 x86_64 處理器(即小端)。我們得到的正是您期望得到的:y = 0x00000042。這是因為當我們以 4 字節的塊重新解釋內存並得到以下結果時:
0x00: 42 00 00 00
0x04: 00 00 00 00
現在,當我們只在內存位置抓取 4 個字節時,0x00我們從原始 8 字節值中獲得了 4 個最低有效字節。請隨意在您的計算機上嘗試此操作。
正如您所料,Big-endian 的行為非常不同。想象一下,我們在大端處理器上運行此代碼。我們會得到:y = 0x00000000。同樣,如果我們以 4 字節的塊重新解釋內存,我們將看到出現這種情況的原因。
0x00: 00 00 00 00
0x04: 00 00 00 42
該y_p指針(0x00在我們的例子)指向0x00000000。
讓代碼在 big-endian 中運行是很困難的,因為大多數處理器不是 little-endian 就是bi-endian。但是,您可以通過添加字節交換來“模擬”big-endian來更改代碼。
unsigned long long x = __builtin_bswap64(0x0000000000000042);unsigned long long * x_p = &x;unsigned int * y_p = (unsigned int *)x_p;unsigned int y = __builtin_bswap32(*y_p);
printf("y = %#.8x\n", y);
然后你可以在你的電腦上運行它。