什么是little-endian和big-endian的字节排序?

329 阅读6分钟

什么是小英德和大英德的字节排序?

计算机在内存中以二进制存储数据。有一件事经常被忽视,那就是这些数据在字节层面的格式化。这被称为字节性,它指的是字节的排序。

具体来说,小字节(little-endian)是指最不重要的字节存储在更重要的字节之前,而大字节(big-endian)是指最重要的字节存储在不重要的字节之前。

当我们写一个数字(十六进制),即0x12345678 ,我们把最有意义的字节写在前面(12 部分)。从某种意义上说,big-endian是写东西的 "正常 "方式。

本文将只讨论整数而非浮点数的字节数,因为它变得更加复杂,而且定义更少。

为什么这很重要?

关于启始性的一个重要区别是,它只是指数值在内存中的存储方式,而不是我们处理数值的方式;例如,0x12345678 ,还是0x12345678 。这里不存在无符号的概念。然而,如果我们谈论的是将这个4字节的值存储到内存中,那么也只有在这个时候,我们才必须指定无序性。

如果我们使用little-endian将前面提到的值存储到内存中,我们会得到以下结果。请注意,每两个十六进制字母代表一个字节。

78 56 34 12

而如果我们要用大-endian存储,我们会得到。

12 34 56 78

最后,这就是为什么字节数很重要;因为不知道数据是如何存储的会导致交流不同的值。

例如,所有的x86_64处理器(英特尔/AMD)都使用 little-endian,而IP/TCP使用big-endian。这意味着,为了让你使用互联网,你的计算机必须考虑到编码的差异。

到目前为止似乎很简单,对吗?

大部分的混淆在于小序数,因此我们将从这里开始。

作为提醒,小序数指的是字节排序,其中最没有意义的字节被存储在前面。因此,例如,如果我们有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

再次,保持数组的顺序。

现在,如果我们把这个问题带回大-退位,我们可以看到,这些例子中的每一个都是以同样的方式存储的。

0x00: 12 34 56 78 9a bc de f0

这是因为big-endian是按照你看到的顺序来存储的。我建议你自己来证明这一点。

那么,为什么每个人都把它弄反了?

不管一开始看起来多么反常,小-前制比大-前制的使用都有合理的理由。广泛使用小编码的原因不是因为用户容易理解(正如你可能已经猜到的),而是为了方便计算机。让我们来看看原因。我们将使用这个8字节的值0x0000000000000042 。当我们把它存储在小-endian中时,我们有如下的结果。

0x00: 42 00 00 00 00 00 00 00

而在大-endian中,我们会得到。

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_py_p 将有相同的值(它们指向同一个位置)。我们将说它们都指向0x00

当我们运行这个程序时,我们将得到两个非常不同的结果,这取决于处理器使用的编码方式。首先,让我们假设我们使用的是x86_64处理器(即小编码)。我们得到的结果正是你所期望得到的。y = 0x00000042.这是因为当我们以4字节为单位重新解释内存时,得到的结果如下。

0x00: 42 00 00 00
0x04: 00 00 00 00

现在,当我们在内存位置0x00 ,只抓取4个字节时,我们得到了原始8字节值中最没有意义的4个字节。请随意在你的电脑上尝试一下

正如你所期望的那样,Big-endian的表现是非常不同的。想象一下,我们在一个大面值的处理器上运行这段代码。我们会得到。y = 0x00000000.同样,如果我们以4字节为单位重新解释内存,我们将看到为什么会出现这种情况的原因。

0x00: 00 00 00 00
0x04: 00 00 00 42

y_p 指针(在我们的例子中为0x00 )指向0x00000000

让代码在大-序数下运行是很难的,因为大多数处理器都是小-序数或双-序数。然而,你可以通过添加字节交换来改变代码,以 "模拟 "大-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);

然后你就可以在你的电脑上运行了