大小端序之爭
1. 前言
在 C 語言中,內(nèi)置的基本類型有 char、short、int、long、double 等,對于整型類型來說,還區(qū)分 signed 和 unsigned。在 Java 語言中,內(nèi)置類型也有 char、short、int、long、double 等,只不過 Java 沒有 unsigned 類型。char 類型在 C 語言是占用 1 字節(jié)長度,而在 Java 語言中占用 2 字節(jié)長度。而其他類型不管在 C 語言中,還是在 Java 語言中,都是占用多個字節(jié)長度。
我們知道 CPU 訪問內(nèi)存是通過地址總線完成的,一塊連續(xù)的內(nèi)存空間是經(jīng)過編址的,每一個地址編號對應 1 字節(jié)長度的內(nèi)存空間,地址空間是從低地址到高地址增長的。如果要在內(nèi)存中存儲 0xAABBCCDD 這樣一個長度為 4 字節(jié)的十六進制整數(shù),需要 4 字節(jié)的內(nèi)存空間。內(nèi)存空間示意如下:
100 101 102 103 -------> 內(nèi)存地址由低到高增長的方向
+----+----+----+----+
| | | | |
+----+----+----+----+
那么 0xAA 是存儲在地址編號為 100 的空間呢?還是存儲在地址編號為 103 的空間呢?這就是本節(jié)要討論的字節(jié)序的問題。
字節(jié)序有大端序(Big-Endian)和小端序(Little-Endian)之分。對于前面提到的十六進制整數(shù) 0xAABBCCDD 來說,如果按照大端序在內(nèi)存中存儲,那么從低地址到高地址的存儲順序依次是 0xAA、0xBB、0xCC、0xDD;如果按照小端序在內(nèi)存中存儲,那么從低地址到高地址的存儲順序依次是 0xDD、0xCC、0xBB、0xAA。
文字描述還是有些抽象,我們通過一張圖來直觀感受一下內(nèi)存字節(jié)序。
2. 計算機的字節(jié)序
在操作系統(tǒng)課程中,我們學過現(xiàn)代操作系統(tǒng)的內(nèi)存管理機制是虛擬內(nèi)存管理機制,對于 32 位系統(tǒng)來說,每一個進程都有 4G( 2^32)字節(jié)長度的虛擬地址空間,也叫線性地址空間。我們先看一張圖。
圖中用內(nèi)存地址 0x90000001 ~ 0x9000000A 表示了 10 字節(jié)的內(nèi)存地址空間,每一個地址代表 1 字節(jié)的內(nèi)存。當一個多字節(jié)整數(shù)存儲在內(nèi)存中時,會涉及到字節(jié)序的問題。
我們首先搞清楚兩個術(shù)語:最高有效位和最低有效位。我們知道,人類習慣的閱讀順序是從左到右,對于一個多位數(shù)字來說,經(jīng)常把它的最左邊叫做高位,把它的最右邊叫做低位。而在計算機中,對于一個多位數(shù)字的描述,也有類似的專業(yè)術(shù)語,把左邊的最高位叫做最高有效位(MSB,most significant bit);把右邊最低位叫做最低有效位(LSB,least significant bit)。
下圖展示了在內(nèi)存中存儲 16 進制整數(shù) 0xAABBCCDD 的不同方式。圖中用內(nèi)存地址 0x90000000 ~ 0x90000003 表示了長度為 4 字節(jié)的內(nèi)存地址空間。
如果按照小端序來存儲,0xAABBCCDD 在內(nèi)存中從低地址到高地址的存儲順序是 0xDD、0xCC、0xBB、0xAA,存儲順序和人類習慣的閱讀順序是相反的。
如果按照大端序來存儲,0xAABBCCDD 在內(nèi)存中從低地址到高地址的存儲順序是 0xAA、0xBB、0xCC、0xDD,存儲順序和人類習慣的閱讀順序是相同的??梢灶惐热祟惖拈喿x順序,更容易理解,也便于記憶。
大小端序是由于 CPU 架構(gòu)的不同導致的,在歷史上 IBM System/360 、Motorola 6800 / 6801、SPARC 是大端序;Intel 架構(gòu)、ARM 架構(gòu)是小端序。另外,JAVA 存儲多字節(jié)整數(shù),也是采用大端序。
通過簡單的程序,很容易測試出來我們當前系統(tǒng)所采用的字節(jié)序類型。
3. 通過 C 程序測試字節(jié)序
通過 C 語言程序來測試字節(jié)序非常簡單,大致思路如下:
- 定義一個整形變量,然后將 0xAABBCCDD 賦值給該變量。
- 按照從低地址到高地址的順序打印此變量的內(nèi)容。
- 將打印結(jié)果的順序和 0xAABBCCDD 的順序進行對比,觀察二者的變化。
代碼片段如下:
1 #include <stdio.h>
2
3 void check_endian()
4 {
5 int n = 0xAABBCCDD;
6
7 unsigned char *ptr_n = (unsigned char*)&n;
8
9 for (int i=0; i < 4; ++i){
10 printf("%X\n", *ptr_n++);
11 }
12 }
代碼中有兩個需要注意的地方:
Tips:
- 需要將 int 型變量 n 的地址賦值給了 unsigned char 型指針變量,如果是賦值給 char 型變量,那么打印結(jié)果是:
FFFFFFDD FFFFFFCC FFFFFFBB FFFFFFAA
原因是 printf 在打印的時候會將 char 提升為 int,0xAA,0xBB 最高位是 1,所以會當做符號位擴展。如果是 unsigned char,會提升為 unsigned int,符號位擴展是 0。
- 打印結(jié)果的時候用 %x 或者 %X 進行格式化輸出。
C 語言程序輸出結(jié)果:
DD
CC
BB
AA
從輸出結(jié)果可以看出我的系統(tǒng)是以小端序來存儲整數(shù)的。
4. Java ByteOrder
我們知道 Java 是平臺無關(guān)的編程語言,它是運行在 Java 虛擬機之上的,而 Java 虛擬機又是運行在 Native 系統(tǒng)上的。那么,如何通過 Java 程序檢測系統(tǒng)本身的字節(jié)序呢?可以通過 java.nio.ByteOrder 類來測試當前 Native 系統(tǒng)的字節(jié)序。調(diào)用 ByteOrder 的 nativeOrder 方法,就能返回系統(tǒng)本身的字節(jié)序。另外,ByteOrder 還定義了兩個 ByteOrder 類型的常量常用:
- ByteOrder.BIG_ENDIAN 表示大端序
- ByteOrder.LITTLE_ENDIAN 表示小端序
檢測程序也很簡單,如下:
public static void testByteOrder(){
System.out.println("The native byte order: " + ByteOrder.nativeOrder());
}
檢測結(jié)果如下:
The native byte order: LITTLE_ENDIAN
5. Java ByteBuffer 的字節(jié)序
那么 JVM 作為一部獨立運行的機器,它的字節(jié)序又是如何呢?通過 Java 程序測試字節(jié)序的思路和 C 程序的一致,代碼片段如下:
public static void checkEndian()
{
int x = 0xAABBCCDD;
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES);
buffer.putInt(x);
byte[] lbytes = buffer.array();
for (byte b : lbytes){
System.out.printf("%X\n", b);
}
}
關(guān)于 JAVA 程序需要說明的是 JAVA 中沒有指針的概念,所以不能通過取地址的方式直接打印內(nèi)存的值。需要借助 JAVA 的 ByteBuffer,將 int 型數(shù)值存儲到 ByteBuffer 中,然后將 ByteBuffer 轉(zhuǎn)換成字節(jié)數(shù)組,通過打印數(shù)組的方式來達到我們的目的。引用 ByteBuffer 需要通過語句 import java.nio.ByteBuffer; 導入ByteBuffer 類。
JAVA 測試結(jié)果:
AA
BB
CC
DD
從輸出結(jié)果可以看出 ByteBuffer 默認是以大端序來存儲整數(shù)的,因為 Java 虛擬機本身采用的就是大端序,ByteBuffer 也要和整個系統(tǒng)保持一致。當然,ByteBuffer 也提供了 ByteBuffer order() 和 ByteBuffer order(ByteOrder bo) 方法,用來獲取和設(shè)置 ByteBuffer 的字節(jié)序。
另外,像一些多字節(jié) Buffer,如 IntBuffer、LongBuffer,它們的字節(jié)序規(guī)則如下:
- 如果多字節(jié) Buffer 是通過數(shù)組(Array)創(chuàng)建的,那么它的字節(jié)序和底層系統(tǒng)的字節(jié)序一致。
- 如果多字節(jié) Buffer 是通過 ByteBuffer 創(chuàng)建的,那么它的字節(jié)序和 ByteBuffer 的字節(jié)序一致。
測試程序如下:
public static void checkByteBuffer(){
ByteBuffer byteBuffer = ByteBuffer.allocate(Long.BYTES);
long [] longNumber = new long[]{
0xAA,0xBB,0xCC,0xDD
};
LongBuffer lbAsArray = LongBuffer.wrap(longNumber);
System.out.println("The byte order for LongBuffer wrap array: " + lbAsArray.order());
LongBuffer lbAsByteBuffer = byteBuffer.asLongBuffer();
System.out.println("The byte order for LongBuffer from ByteBuffer: " + lbAsByteBuffer.order());
}
執(zhí)行結(jié)果:
The byte order for LongBuffer wrap array: LITTLE_ENDIAN
The byte order for LongBuffer from ByteBuffer: BIG_ENDIAN
如果在上面的 checkByteBuffer 方法中,首先將對象 byteBuffer 的字節(jié)序設(shè)置為 ByteOrder.LITTLE_ENDIAN(通過 ByteBuffer 的 order 方法設(shè)置),然后再創(chuàng)建 lbAsByteBuffer 對象,那么 lbAsByteBuffer 的字節(jié)序該是什么呢?
6. 網(wǎng)絡(luò)字節(jié)序
前面兩小節(jié)討論的都是 CPU、Java 虛擬機的字節(jié)序,通常叫做主機(host)字節(jié)序。在網(wǎng)絡(luò)編程中,字節(jié)流在網(wǎng)絡(luò)中傳輸是遵循大端序的,也叫網(wǎng)絡(luò)字節(jié)序。
由于 Java 虛擬機的字節(jié)序和網(wǎng)絡(luò)字節(jié)序是一致的,對于 Java 程序員來說,通常不太關(guān)心字節(jié)序的問題。然而,當 Java 程序和 C 程序進行通信的時候,需要關(guān)心字節(jié)序的問題。
7. 小結(jié)
本文主要是介紹了 CPU 架構(gòu)帶來的多字節(jié)數(shù)值在內(nèi)存中存儲時的字節(jié)序問題,字節(jié)序分為大端序和小端序。在計算機網(wǎng)絡(luò)中,大端序也叫做網(wǎng)絡(luò)字節(jié)序;相應的主機上的存儲順序叫做主機字節(jié)序。
在 Java 程序中,由于 Java 程序是在 Java 虛擬機上運行,Java 虛擬機的字節(jié)序是大端序。然而 Java 虛擬機運行的 Native 系統(tǒng)的字節(jié)序是不確定的,可以通過 java.nio.ByteOrder 的 nativeOrder 方法來確定。
對于 Java 網(wǎng)絡(luò)編程中廣泛應用的 ByteBuffer,則默認是大端序,當然你也可以根據(jù)需要設(shè)置它的字節(jié)序。對于多字節(jié)數(shù)值 Buffer,比如 IntBuffer、LongBuffer,則需要根據(jù)他們創(chuàng)建時所依賴的結(jié)構(gòu),來判定它們的字節(jié)序。
本節(jié)內(nèi)容相對簡單,學習起來也會輕松很多,但是非常重要,需要掌握。