大小端字节序
想起以前在汇编语言和数字逻辑的时候也有接触到一些这个概念,已经有点模糊了,搞不清楚哪个是低位在前哪个是高位在前。后来在Wiki和Google的帮助下也算摸清楚了一些Endianness的概念。
一、字节序的起源
在计算机中,字节序(Endianness)是数据中单独的可取地址的亚型(words,bytes和bits)在外部存储器中存储的顺序。通常在提到四字(ddword)、双字(dword)和字(word)的时候需要考虑其实际的字节顺序,为了简便起见它的英文也常常表示为Byte Order。
Endianness这个词源自1726年Jonathan Swift的名著:Gulliver’s Travels(格列佛游记),在书中有一个故事,大意是指Lilliput(小人国)的领导下了一道指令,规定其人民在剥水煮蛋时必须从little-end(小的那一端)开始。这个规定惹恼了一群觉得应该要从big-end(大的那一刻)开始剥的人。事情发展到后来,竟然演变成一场纷战。支持小的那端的人被称为little-endian,反之则被称为big-endian(在英语中后缀“-ian”表示“xx人”的意思)。1980年Danny Cohen在他的论文“On Holy Wars and a Plea for Peace”中第一次使用了Big-和Little-这两个术语,最终它们成为了计算机通过网络与其他计算机连接时所要考虑的极其重要的一个问题。
二、字节序的种类和其表示
那么为什么要引入字节序呢。我们都知道,计算机存储中最小的单位是位(bit),而8bit构成一个字节(byte)。在一个32位的CPU中,字长为32bit,也就是4byte,数据要想存放在内存中供CPU读取和写入,就需要拥有一定的存放顺序。这样不同的CPU可接受的字节序有可能不同,那么在设计硬件和软件时数据的存放问题也需要分开考虑。
数据都有所谓的“有效位(Significant Bit)”,顾名思义它表示了“数据存放有效的位置”,而字节序的分类就是依赖于有效位来进行划分的。在一个字节当中,数据的有效位的顺序已经得到了大多数硬件生产商的共识,那就是最高有效位优先(Most Significant Bit First),例如我们用8位二进制数来表示十进制数123为01111011,其第一位的0就是最高有效位,而最后一位的1就是最低有效位,在一个字节当中,几乎当前所有的硬件都采用了这种直观的字节序。
然而情况在离开了单字节时就有所不同了。不同的硬件产商对于数据占据多个字节时拥有怎样的字节序有着不同的理解,具体说来分为以下三类:
- Big-Endian(大字节序):最高有效字节优先,更高的字节有效位占据着更低地址的内存空间,其在内存中的表示与直观吻合,
- Little-Endian(小字节序):最低有效字节优先,更低的字节有效位占据着更低地址的内存空间,其在内存中的表示与直观相反,以及
- Mixed-Endian(混合字节序)或者Middle-Endian(中字节序):在16位字(word)中的字节序与32位字(dword)中的字节序不相同。这种类型的字节序较为少见。
一些知名的使用Little-Endian的处理器体系结构包括了:x86、6502、Z80、VAX以及PDP-11,使用Big-Endian的处理器通常是Motorola的处理器,例如:6800、68000、PowerPC(即Macintosh在迁移到x86之前所采用的处理器)以及System/370。这也是为什么在文章开头提到的文档中使用Big Endian / Motorola standard这样的词汇的原因。
更进一步的,像ARM、PowerPC、Alpha、SPARC V9、MIPS、PA-RISC和IA64等体系结构可以支持可切换的字节序这样的特性,这个特性可以提高效率或者简化网络设备和软件的逻辑。这种可切换的字节序被称为Bi-Endian,用于硬件上意指计算机或者传递数据时可以使用两种不同字节序中任意一种的能力。
文字不够直观,下面以数值0x0A0B0C0Dh为例说明Big-Endian和Little-Endian在内存布局上的不同:
- Big-Endian在内存中的表示
increasing addresses → | |||||
0Ah | 0Bh | 0Ch | 0Dh |
在这个例子中,最高有效字节(MSB)为0Ah,储存在最低地址的内存中;次高有效位为0Bh,储存在接下来的内存中,依此类推。这种字节序与从左向右的顺序读取十六进制数值非常类似。
以16位元素大小查看:
increasing addresses → | |||||
0A0Bh | 0C0Dh |
最高有效元素现在保存的是0A0Bh,接下来的元素保存0C0Dh.
- Little-Endian在内存中的表示
increasing addresses → | |||||
0Dh | 0Ch | 0Bh | 0Ah |
在这个例子中,最低有效字节(LSB)的值为0Dh,储存在最低地址的内存,其他字节依照字节有效性的递增依次存放。
用16位元素大小表示
increasing addresses → | |||||
0C0Dh | 0A0Bh |
最低有效16位单元储存的是值0C0Dh,紧接着储存值0A0Bh。
三、字节序的重要性及其应用
如前所述,不同硬件的体系结构接受不同字节序的数据表示,因此当同一个文件在不同的机器中进行读取和写入的时候,其所支持的字节序就显得尤为关键。设想在x86计算机中将(123888)10写入二进制文件中,由于x86支持Little-Endian,所以该数在文件中保存为(0000003F1E)16。当在PowerPC计算机中读取该整数时,由于它支持的是Big-Endian,故读取的结果将是(16158)10,大相径庭。
同样的情况也会出现在网络传输当中,当你从支持一种字节序的机器发送数据到支持相反字节序的机器时,将会得到非预期的结果。这种错误在网络传输当中尤为突出,因为你无法决定发送你所需文件机器所支持的字节序,因为这些机器可能分散在世界各地,不是人为所能控制的。
为了更明确的说明上述问题,考虑下列代码:
02 #include <string.h>
03
04 int main (int argc, char* argv[]) {
05 FILE* fp;
06
07
08 struct {
09 char one[4];
10 int two;
11 char three[4];
12 } data;
13
14
15 strcpy (data.one, “foo”);
16 data.two = 0×01234567;
17 strcpy (data.three, “bar”);
18
19
20 fp = fopen (“output”, “wb”);
21 if (fp) {
22 fwrite (&data, sizeof (data), 1, fp);
23 fclose (fp);
24 }
25 }
这是一段很简单的C语言代码,作用就是向一个data结构体赋值并且将它写入文件当中,从结果Listing 2和Listing 3当中我们就可以看到支持不同字节序的机器在处理数据时候存在的不同。
Listing 2. hexdump –C output on big-endian machines
00000000 66 6f 6f 00 01 23 45 67 62 61 72 00 |foo..#Egbar.| 0000000c |
Listing 3. hexdump -C output on little-endian machines
00000000 66 6f 6f 00 67 45 23 01 62 61 72 00 |foo.gE#.bar.| 0000000c |
注意力好的同学一眼就能发现,在写整数的时候,数据保存的顺序依赖于不同的机器,而字符串却不受此影响,这是为什么呢?这就牵涉到字节序是如何如代码进行影响的了。
字节序并不会影响数据存储的所有方面,例如对一个整数进行bitwise或者bitshift的操作,你是不需要去注意对应的字节序的。因为多字节的顺序是由计算机来维护的,对于程序员来说,一个整数的最低有效位仍然是最低有效位,最高有效位亦然,并不会由于它在计算机底层存储模式的改变而影响到有效位的含义。
同样的,字节序不会影响到C风格字符串在计算机底层的存储顺序,这是为什么呢?考虑到一个C风格字符串的实质是一个包含着许多char的数组,每一个char在现代计算机中几乎都是表示计算机中的一个字节。因此,当读写C风格字符串时,其最小的元素单位是一个字节;而且数组在内存单元中地址的排列顺序是递增的,例如定义char str[5];这么一条语句,假设&str[0]的地址为1000,则&str[1]的地址为1001,依次类推。所以不论从直观含义或者底层技术来看,字符串的存储都是相对字节序独立的,这个特性将应用在接下来的许多小技巧中。
那么字节序除了影响到多字节数据在内存中的存放顺序以外,在写代码的时候还有什么需要注意的呢?当对一个数据进行类型转换的时候,需要记住特定的字节序很可能影响到类型转换的结果。假设我们有Listing 4所列的这么一段代码
4 x = *(short *) endian;
那么最后得到x的结果是多少呢?是不是简单的就是endian数组的第一个元素1呢?答案是错,x的数值需要根据运行时的环境来决定。让我们回忆一下C语言的指针指向多大的内存以及怎么去解释所指的这块内存是由指针所指向的类型来确定的,在上述代码中,将endian数组的首元素指针强制转换成short *的指针,那么编译器在解释它的时候将不再把它指向的内存空间视为1 byte,而是short的长度——2 byte;更重要的是当我们对这个指针解引用的时候将会得到的值会是什么。再回到上面所提到的字符串或者字符数组在计算机中就是依照数组顺序存放的,那么这个时候endian数组占用了两个字节,其内存数据为:0100。当该指针强制转换为指向short的指针并解引用时,计算机将一次读取两个字节,这个时候字节序就发挥它的影响了。在支持Little-Endian的机器中x的值将是1(读取为0001),而在支持Big-Endian的机器中x的值就是256(读取为0100)。因此在对指针进行类型转换并解引用,特别是在单字节到多字节数据的转换时,要特别注意字节序是否会使得预期结果出现偏差。
单字节指针到多字节指针的转换其实并不完全像Listing 4所举例子那样恼人,它还有其他的用途,例如我们可以使用这个特性在运行时判断当前计算机所支持的字节序,这样可以使得程序员在编写代码的时候更加灵活,也使得代码更加强健(robust)。基本的思路就是先定义一个int变量1,这个变量在不同的计算机中将有两种不同的存储顺序:01000000(Little)以及00000001(Big),然后我们将指向这个变量的指针强制转换为指向字符的指针,再解引用根据它的值是0还是1就可以得出当前机器支持的字节序的,代码很简单:
4 // Big Endian
5 else
6 // Little Endian
利用char*的这种特性还可以方便的反转数据顺序以适应不同的机器,怎么编写这样的代码不如让你来思考一下?