byte order - little-endian and big-endian

什么是 little-endian 和 big-endian

字节序(byte order)又称端序或尾序(Endianness),多字节对象都被存储为连续的字节序列,存储地址内的排列有两个通用规则:

  1. little-endian: 小端序,就是低位字节放在内存的起始地址,即最低有效位在最高有效位的前面。
  2. big-endian: 大端序,就是高位字节放在内存的起始地址,即最高有效位在最低有效位的前面。

示例

比如 16 进制数字0x12345678(10 进制为305419896)在内存中的表示形式为:

大端模式

低地址     ------------------------>  高地址0x12      |  0x34     |  0x56     |  0x7800010010  |  00110100 |  01010110 |  01111000

小端模式

低地址     ------------------------>  高地址0x78      |  0x56     |  0x34     |  0x1201111000  |  01010110 |  00110100 |  00010010
  1. 上面 2 者比较可见,大端模式和直接阅读数字的习惯一致。
  2. 这 2 个模式中,共同点就是每个字节byte都是由8bit组成,而bit只有 2 个值,即01
  3. 比如上面0x78在内存中保存在一个字节中,其表现是一样的,其 8 个 bit 都是自右向左,和数学数字表示法一样,高位在左边,即01111000

big-endian 还是 little-endian

  1. x86 架构的是小端模式的,所以当前大多数的 cpu 是小端模式的。
  2. 多数的 powerpc 架构的 cpu 是大端模式的。
  3. arm 架构的 cpu 可以设置使用大端还是小端模式。
  4. jpeg 是大端模式,而 gif 则是小端模式。
  5. 网络字节序是 tcp/ip 中规定好的一种数据表示格式,它与具体的 cpu 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节序采用big-endian排序方式。

x86 Endianness 示例

x86 是小端序而网络字节序是大端序,以下是一个 IPv4 (4 字节) 0x366410AC 的小端序示例:

0x36 0x64 0x10 0xAC---- ---- ---- ---- |    |    |    |__ 0xAC = 172 |    |    |_______ 0x10 =     16 |    |____________ 0x64 =        100 |_________________ 0x36 =            54                    ---------------------                           172.16.100.54

UNIX Networking Programming 书中的例子

c 语言中可以利用联合体union判断 cpu 对内存采用little-endian还是big-endian模式读写。

int main(void) {    union {        short s;        char c[sizeof(short)];    } un;    un.s = 0x0102;    if (sizeof(short) == 2) {        if (un.c[0] == 1 && un.c[1] == 2) {            printf("big-endian\n");        } else if (un.c[0] == 2 && un.c[1] == 1) {            printf("little-endian\n");        } else {            printf("unknown\n");        }    } else {        printf("sizeof(short) = %d\n", sizeof(short));    }    return 0;}

通过类型强转进行判断

int big_endian() {    int a = 0x1234;    // 通过将 int 强制类型转换成 char 单字节,通过判断起始存储位置。    // 即取 b 等于 a 的低地址部分    char b = *(char *) &a;    if (b == 0x12) {        // 高位存储在起始地址,即为大端        return 1;    }    return 0;}

网络字节序与主机字节序转换函数

  1. htons(): host to network short,将 short 类型数据从主机字节序转换为网络字节序。
  2. ntohs(): network to host short,将 short 类型数据从网络字节序转换为主机字节序。
  3. htonl(): host to network long,将 long 类型数据从主机字节序转换为网络字节序。
  4. ntohl(): network to host long,将 long 类型数据从网络字节序转换为主机字节序。

通常,以s为后缀的函数中,s代表2个字节short,因此用于端口号转换;以l为后缀的函数中,l代表4个字节的long,因此用于 IP 地址转换。

网络字节序与主机字节序转换函数测试代码

#include <stdio.h>#include <stdlib.h>int main(void) {    unsigned short host_port = 0x1234, net_port;    unsigned long host_addr = 0x12345678, net_addr;    printf("0x12345678: %d\n", host_addr); // 305419896    net_port = htons(host_port);    net_addr = htonl(host_addr);    printf("Host ordered port: %#x\n", host_port);    printf("Network ordered port: %#x\n", net_port);    printf("Host ordered address: %#lx\n", host_addr);    printf("Network ordered address: %#lx\n", net_addr);    return 0;}

0x12345678: 305419896
Host ordered port: 0x1234
Network ordered port: 0x3412
Host ordered address: 0x12345678
Network ordered address: 0x78563412

UTF-8 编码和 UTF-16 编码

  1. UTF-8 编码的规则决定了其存储与字节序没有关系,所以 UTF-8 文件头可以有BOM(byte order mark),也可以没有,一般 linux 下面的创建的 UTF-8 文件默认是没有BOM的。
  2. UTF-16 编码则是用 2 个字节来保存一个字符,一个字母a和一个中文字都用 2 个字节保存,所以必须要知道文件保存的字节序,才能正确的读写相应的内容。
  3. 对于 UTF-16 编码的文件,不论是大端模式还是小端模式,存储的第一个字符都是BOM,占 2 个字节,BOM的 unicode 码是0xFEFF,存储在大端模式 unicode 文件里是0xFEFF,存储在小端模式 unicode 文件里是0xFFFE

java 中的字节序

java 中字节存储默认为大端模式,可以查看包含多字节字符的字符串方法String#getBytes()返回的数组内容进行确认。

下面示例中以UTF-8编码的字符串中有个多字节的字符字,将其对应的unicode编码0100111000101101(10 进制20013)用填充到UTF-8编码规则指定的这 3 个字节中:1110**** 10****** 10******,即11100100 10111000 10101101,这 3 字节码对应的 10 进制数为[-28, -72, -83]。

C 语言中"11100100"的有符号数和无符号数转化

以第 1 个字节11100100为例,说明其数值与编码的关系,从下面 C 和 java 代码的运行结果中可以看出:

  1. java 中的 byte 是有符号数,即取值范围为-128 ~ 127
  2. UTF-8 编码是每个字节的无符号数,所以数值都是大于 0 的。
#include <stdio.h>int main(void) {    void print_binary(char *, int);    signed char i_28 = 0b11100100;    unsigned char i228 = 0b11100100;    printf("signed char 0b11100100 is : %d \n", i_28);   // -28    printf("unsigned char 0b11100100 is : %x \n", i228); // e4    printf("unsigned char 0b10111000 is : %x \n", 0b10111000); // b8    printf("unsigned char 0b10101101 is : %x \n", 0b10101101); // ad}

而下面 java 例子字输出的编码就是[-28, -72, -83],因此 java 中默认的字节序是高位在前,即大端序。另外这 3 个字节在 16 进制的表现形式为[e4, b8, ad],即UTF-8编码:%E4%B8%AD,具体可以看下面代码的备注说明,之前我在这篇文章里用 javascript 演示了UNICODE码和UTF-8编码之间的转换关系。

使用 UTF-8 编码并使用 java 默认字节序

@Testpublic void testUTF8ByteSort() throws UnsupportedEncodingException {    byte[] dst;    ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); // 创建字节缓冲区,java 中字节存储默认为大端模式    String abcdef = "abcde 中";          // “中”字 16 进制为: 0x4e2d ,其 unicode 码为: 20013    byteBuffer.put(abcdef.getBytes());  // default charset UTF-8    System.out.println(Arrays.toString(abcdef.getBytes())); // [97, 98, 99, 100, 101, -28, -72, -83]    // 计算机中负数用补码表示:    // -28 的 2 进制表示: ~(28) + 1,即 11100100,而 11100100 再转成 16 进制无符号数,即为 e4    // 同样得到 -72 对应的 16 进制 b8    // -83 对应的 16 进制 ad    String b1 = Integer.toBinaryString(-28);    String b0 = Integer.toBinaryString(-28 & 0xFF);    System.out.println(b1); // 11111111111111111111111111100100    System.out.println(b0); // 11100100    System.out.println(Integer.parseInt(b0, 2)); // 228    System.out.println(Integer.toHexString(Integer.valueOf(b0, 2))); // e4    System.out.println(Integer.toHexString(Integer.valueOf(Integer.toBinaryString(-28 & 0xFF), 2))); // e4    // 每个 byte 的 16 进制数    System.out.println("==========================");    System.out.println(Integer.toHexString(-28 & 0xFF)); // e4    System.out.println(Integer.toHexString(-72 & 0xFF)); // b8    System.out.println(Integer.toHexString(-83 & 0xFF)); // ad    System.out.println("==========================");    // 因此 "中" 字 uri encode 的编码即为: "%e4%b8%ad"    String encode = URLEncoder.encode("中", Charsets.UTF_8.name());    System.out.println(encode);    String decode = URLDecoder.decode("%e4%b8%ad", Charsets.UTF_8.name());    System.out.println(decode);    System.out.println("==========================");    System.out.println(byteBuffer); // java.nio.HeapByteBuffer[pos=8 lim=16 cap=16]    byteBuffer.flip();              // 注意 flip()之后的 pos/limit 值的变化    System.out.println(byteBuffer); // java.nio.HeapByteBuffer[pos=0 lim=8 cap=16]    System.out.println("==========================");    dst = new byte[byteBuffer.limit()];    byteBuffer.get(dst);                        // byteBuffer 中有效的字节    System.out.println(Arrays.toString(dst));   // [97, 98, 99, 100, 101, -28, -72, -83]    System.out.println(new String(dst));        // abcde 中    // 下面打印 ByteBuffer 中所有的字节内容    System.out.println(Arrays.toString(byteBuffer.array())); // [97, 98, 99, 100, 101, -28, -72, -83, 0, 0, 0, 0, 0, 0, 0, 0]    System.out.println(new String(byteBuffer.array()));}

javascript 中"-28"的无符号数 2 进制和 16 进制转化

上面的代码用 java 对-28的 16 进制无符号数的转化过程做了一个演示,下面再用 javascript 演示一下转化过程如下:

// a zero fill right shift converts it's operand to a 32 signed bit integer.var binary = (-28 >>> 0).toString(2);  // coerced to uint32console.log(binary);                   // 11111111111111111111111111100100console.log(parseInt(binary, 2) >> 0); // -28, binary to int32var b0 = -28 & 0xFF;             // -28 用 1 个字节来表示其无符号数即为 228console.log(b0);                 // 228console.log(b0.toString(16));    // e4encodeURIComponent("中");        // '%E4%B8%AD'decodeURIComponent("%e4%b8%ad"); // '中'

使用 UTF-16 编码并使用 java 默认的字节序

当上述字符串使用UTF-16编码格式进行存储时,仍然是高位在前,即大端模式,而且因为 16 进制有字节序的问题,必须声明当前存储的内容的字节序是什么模式的,所以最前面有0xFEFF的 2 个字节来表示BOM

UTF-16编码中所有的字符都是用 2 个字节来表示的,因为字母a也是用[0, 97]这 2 个字节来表示,并且高位0(0x00)在低位97(0x61)之前。因为UTF-162 个字节表示一个字符,那么其最多能表示1 << 16 - 1 = 65535个字符。

因为 java 中不存在无符号数,即没有C语言中unsigned类型的整数,java 中一个字节byte能表示的数值范围为-128 ~ 127,因此 1 个字节0xFE即为有符号数的-2,而0xFF则为有符号数的-1

@Testpublic void testUTF16ByteOrder() {    String abcdef = "abcde 中"; // 中 16 进制为: 0x4e2d ,其 unicode 码为: 20013    // 下面这句输出为:[-2, -1, 0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 78, 45]    // UTF_16 编码中每个字符占 2 个字节存储空间,“中”字的 16 进制为 0x4e2d,按大端模式,高位先存储在第 1 个字节    // 1. 即 0x4e2d 中的 4e 存储在第 1 个字节,4e 的 10 进制数即为 78,2 进制数为: 01001110    // 2. 而 0x4e2d 中的 2d 存储在第 2 个字节,2d 的 10 进制数即为 45,2 进制数为: 00101101    // 3. 内存中的形式如: 0100111000101101    System.out.println(Arrays.toString(abcdef.getBytes(Charsets.UTF_16)));    // 4. 转化 0100111000101101 为 10 进制数: 20013    int charCode = Integer.parseInt("0100111000101101", 2);    System.out.println(charCode); // 20013    System.out.println((int) "中".charAt(0)); // 20013}

java 中设置字符串存储的字节序

通过设置ByteBuffer#order(ByteOrder)方法,设置其以CharBuffer使用时,控制每个char存储的字节序。如果是按byte存储,则使用默认的大端模式存储,这个方法设置就没有意义了。

@Testpublic void testBigEndian() {    ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]);    // byteBuffer.order(ByteOrder.BIG_ENDIAN); // 默认值,不设置    String abcdef = "abcde 中";    CharBuffer charBuffer = byteBuffer.asCharBuffer();    charBuffer.put(abcdef);    charBuffer.rewind();    System.out.println(charBuffer.position()); // 0    System.out.println(charBuffer.limit()); // 8    System.out.println(charBuffer.capacity()); // 16 / 2 = 8; 1 个 char 占 2 个 bytes    // 大端字节序输出为:[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 78, 45]    System.out.println(Arrays.toString(byteBuffer.array()));    System.out.println(new String(byteBuffer.array()));}@Testpublic void testLittleEndian() {    ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]);    byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // 设置字节序    String abcdef = "abcde 中";    CharBuffer charBuffer = byteBuffer.asCharBuffer();    charBuffer.put(abcdef);    charBuffer.rewind();    // 小端字节序输出:[97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 45, 78, 0, 0, 0, 0]    System.out.println(Arrays.toString(byteBuffer.array()));    System.out.println(new String(byteBuffer.array()));}

关于 java 和 C 语言中的 char 类型

简单说一下这 2 个语言中都有char类型,但使用方式不一样,C 语言中的char类型:

  1. 只使用1 个字节,1 个字节也就没有字节序的问题。
  2. unsigned char,数值表示范围为0 ~ 255
  3. signed char,有符号数的表示范围为-128 ~ 127,这个与 java 中的byte相同。

java 语言中的char类型:

  1. 使用2 个字节,所以有字节序的问题,java 默认为大端序。
  2. 类似于C语言的signed short int,可表示的数值范围为0 ~ 65535
  3. java 中没有实现无符号数。
@Testpublic void sizeOfJavaChar() {    // java 中的 char 有点像 C 语言中的 signed short int,256 以内的字符实际上只要一个字节就可以    char ff = 0xFF;    System.out.println((int) ff);  // 255    char ff2 = 0xFFFF;             // 255 以上,65536 以下,则需要 2 个字节    System.out.println((int) ff2); // 65535    // 超过 0xFFFF 就走出数值表示范围了    int i0000 = 0x10000;    System.out.println(i0000);     // 65536    char ff3 = (char) 0x10000;     // 不强转,则已经超出 char 的数值范围了,强转就丢精度了    System.out.println((int) ff3); // 0    short s = 0x0102;    System.out.println(s);        // 258    System.out.println(s >> 8);   // 1 高位    System.out.println(s & 0xFF); // 2 低位    System.out.println((byte) s); // 2 丢失高位}

javascript 中的字节序

下面的 ES6 代码对同一段内存ArrayBuffer,分别建立两种视图TypedArray

  1. 8 位无符号整数(Uint8Array 构造函数)。
  2. 16 位带无符号整数(Uint16Array 构造函数)。

ArrayBuffer是不能直接被访问的,因此需要借助TypedArray。由于两个视图对应的是同一段内存,一个视图修改底层内存,会影响到另一个视图。这个过程有点像C语言中的union联合类型。

var arrayBuffer = new ArrayBuffer(2);var u8 = new Uint8Array(arrayBuffer);   // 同一段内存地址var u16 = new Uint16Array(arrayBuffer); // 同一段内存地址// Determine whether Uint16 is little-endian or big-endian.u16[0] = (1 << 8) + 2; // 即 0x0102,必须加括号,<< 左移操作符优先级低于 +console.log(u8);       // [2, 1]console.log(u16);      // [258]var isLittleEndian = true;if (u8[0] === 0x01 && u8[1] === 0x02) {    isLittleEndian = false;}console.log("is little endian : " + isLittleEndian);u8[0] = 0xFF; // 修改内存低地址的值,查看对 u16 的影响console.log(u8);     // [255, 1]console.log(u16);    // [511],即 0x01FFconsole.log(0x01FF); // 低位由 0x02 改成 0xFF

以上代码在浏览器里运行的结果如下:

Uint8Array(2) [2, 1]
Uint16Array [258]
is little endian : true
Uint8Array(2) [255, 1]
Uint16Array [511]
511

nodeREPL中的运行结果如下:

{ '0': 2, '1': 1 }
{ '0': 258 }
is little endian : true
{ '0': 255, '1': 1 }
{ '0': 511 }
511

以上 javascript 代码都是在x86的 CPU 上运行的,无论是浏览器和还是REPL,表现的结果都是小端序。

MDNTyped array views说明了其字节序是平台相关的,摘录部分内容如下:

Typed array views are in the native byte-order (see Endianness) of your platform. With a DataView you are able to control the byte-order.

使用 DataView 检测平台的字节序

function isLittleEndian() {    var arrayBuffer = new ArrayBuffer(2);    var view = new DataView(arrayBuffer);    view.setInt16(0, 256, true); // 显式以 little endian 写入数据 256,即 0x0100    // 此时 arrayBuffer 里的内存布局应该是 0x00 0x01    var i16 = new Int16Array(arrayBuffer);    // 如果以 little endian 读取,它就是 0x0100,即 256;以 big endian 读取,则是 0x0001,即为 1    return i16[0] === 256;}

javascipt 中有符号数与无符号数转换

通过使用逻辑右移运算符,移动位数为 0,可以将 32 位有符号整数,转化为 32 位无符号整数。

unsigned = signed >>> 0;

通过使用左移运算符,位动位数为 0,可以将 32 位无符号整数,转化为 32 位有符号整数。

signed = unsigned << 0;

javascript 有符号数和无符号数转换测试代码

var signed, unsigned;signed = -1;unsigned = signed >>> 0;console.log("unsigned = " + unsigned); // unsigned = 4294967295signed = unsigned << 0;console.log("signed = " + signed);   // signed = -1

References

  1. Understanding Big and Little Endian Byte Order
  2. 详解大端模式和小端模式
  3. socket 网络字节序以及大端序小端序
  4. 深入浅出: 大小端模式
  5. JavaScript typed arrays
  6. ES6 二进制数组
  7. Javascript Typed Arrays and Endianness
  8. JS 中的二进制操作
  9. Architecture/x86-assembly