对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。例如,假设一个类型为 int 的变量 x 的地址 Ox100,也就是说,地址表达式 &x 的值为 Ox100。那么,(假设数据类型 int 为 32 位表示)x 的 4 个字节将被存储在内存的 Ox100、Ox101、Ox102 和 Ox103 位置。
排列表示一个对象的字节有两个通用的规则。考虑一个 w 位的整数,其位表示为[],其中 是最高有效位,而 是最低有效位。假设 w 是 8 的倍数,这 些位就能被分组成为字节,其中最高有效字节包含位 [],而最低有效字节包含位 [],其他字节包含中间的位。某些机器选择在内存中按照从最低有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效字节的顺序存储。前一种规则——最低有效字节在最前面的方式,称为小端法 (little endian)。后一种规则—-— 最高有效字节在最前面的方式,称为大端法 (big endian)。
假设变量 x 的类型为 int,位于地址 Ox100 处,它的十六进制值为 Ox01234567 。地址范围 Ox100 ~ Ox103 的字节顺序依赖于机器的类型:
注意,在字 Ox01234567 中,高位字节的十六进制值为 Ox01, 而低位字节值为 Ox67。
大多数 Intel 兼容机都只用小端模式。另一方面,IBM 和 Oracle( 从其 2010 年收购 Sun Microsystems 开始)的大多数机器则是按大端模式操作。注意我们说的是“大多数”。这些规则并没有严格按照企业界限来划分。比如, IBM 和 Oracle 制造的个人计算机使用的是 Intel 兼容的处理器,因此使用小端法。许多比较新的微处理器是双端法 (bi-endian),也就是说可以把它们配置成作为大端或者小端的机器运行。然而,实际情况是:一旦选择了特定操作系统,那么字节顺序也就固定下来。比如,用于许多移动电话的 ARM 微处理器,其硬件可以按小端或大端两种模式操作,但是这些芯片上最常见的两种操作系统 —— Android(来自 Google) 和 IOS(来自 Apple)却只能运行于小端模式。 令人吃惊的是,在哪种字节顺序是合适的这个问题上,人们表现得得非常情绪化。实际上,术语 "little endian( 小端)”和 "big endian( 大端)”出自 Jonathan Swift 的《格利佛游记》(Gulliver's Travels) 一书,其中交战的两个派别无法就应该从哪一端(小端还是大端)打开一个半熟的鸡蛋达成一致。就像鸡蛋的问题一样,选择何种字节顺序没有技术上的理由,因此争论沦为关于社会政治论题的争论。只要选择了一种规则并且始终如一地坚持,对千哪种字节排序的选择都是任意的。
对于大多数应用程序员来说,其机器所使用的字节顺序是完全不可见的。无论为哪种类型的机器所编译的程序都会得到同样的结果。不过有时候,字节顺序会成为问题。首先是在不同类型的机器之间通过网络传送二进制数据时,一个常见的问题是当小端法机器产生的数据被发送到大端法机器或者反过来时,接收程序会发现,字里的字节成了反序的。为了避免这类问题,网络应用程序的代码编写必须遵守已建立的关于字节顺序的规则,以确保发送方机器将它的内部表示转换成网络标准,而接收方机器则将网络标准转换为它的内部表示。我们将在第 11 章中看到这种转换的例子。
第二种情况是,当阅读表示整数数据的字节序列时字节顺序也很重要。这通常发生在检查机器级程序时。作为一个示例,从某个文件中摘出了下面这行代码,该文件给出了一个针对 Intel x86-64 处理器的机器级代码的文本表示:
4003d3: 01 05 43 0b 20 00 add %eax,Ox200b43(%rip)
这一行是由反汇编器 (disassembler) 生成的,反汇编器是一种确定可执行程序文件所表示的指令序列的工具。我们将在第 3 章中学习有关这些工具的更多知识,以及怎样解释像这样的行。而现在,我们只是注意这行表述的意思是:十六进制字节串 01 05 43 Ob 20 00 是一条指令的字节级表示,这条指令是把一个字长的数据加到一个值上,该值的存储地址由 Ox200b43 加上当前程序计数器的值得到,当前程序计数器的值即为下一条将要执行指令的地址。如果取出这个序列的最后一个字节: 43 Ob 20 00,并且按照相反的顺序写出,我们得到 00 20 Ob 43 。去掉开头的 00,得到值 Ox200b43,这就是右边的数值。当阅读像此类小端法机器生成的机器级程序表示时,经常会将字节按照相反的顺序显示。书写字节序列的自然方式是最低位字节在左边,而最高位字节在右边,这正好和通常书写数字时最高有效位在左边,最低有效位在右边的方式相反。
字节顺序变得重要的第三种情况是当编写规避正常的类型系统的程序时。在 C 语言中,可以通过使用强制类型转换 (cast) 或联合 (union) 来允许以一种数据类型引用一个对象,而这种数据类型与创建这个对象时定义的数据类型不同。大多数应用编程都强烈不推荐这种编码技巧,但是它们对系统级编程来说是非常有用,甚至是必需的。
下图展示了一段 C 代码,它使用强制类型转换来访问和打印不同程序对象的字节表示。我们用 typedef 将数据类型 byte_pointer 定义为一个指向类型为 "unsigned char” 的对象的指针。这样一个字节指针引用一个字节序列,其中每个字节都被认为是一个非负整数。第一个例程 show_bytes 的输入是一个字节序列的地址,它用一个字节指针以及一个字节数来指示。该字节数指定为数据类型 size_t,表示数据结构大小的首选数据类型。 show_bytes 打印出每个以十六进制表示的字节。C 格式化指令 "%.2x" 表明整数必须用至少两个数字的十六进制格式输出。
#include <stdio.h>
typedef unsigned char *byte_pointer;
void show_bytes(byte_pointer start, int len) {
int i;
for(i=0; i<len; i++) {
printf("%.2x ", start[i]);
}
printf("\\n");
}
void show_int(int x) {
show_bytes((byte_pointer) &x, sizeof(int));
}
void show_float(float x) {
show_bytes((byte_pointer) &x, sizeof(float));
}
void show_pointer(void *x) {
show_bytes((byte_pointer) &x, sizeof(void *));
}
过程 show_int、show_float 和 show_pointer 展示了如何使用程序 show_bytes 来分别输出类型为 int float 和 void* 的 C 程序对象的字节表示。可以观察到它们仅仅传递给 show_bytes 一个指向它们参数 x 的指针 &x,且这个指针被强制类型转换为 "unsigned char *"。这种强制类型转换告诉编译器,程序应该把这个指针看成指向一个字节序列,而不是指向一个原始数据类型的对象。然后,这个指针会被看成是对象使用的最低字节地址。
这些过程使用 C 语言的运算符 sizeof 来确定对象使用的字节数。一般来说,表达式 sizeof (T) 返回存储一个类型为 T 的对象所需要的字节数。使用 sizeof 而不是一个固定的值,是向编写在不同机器类型上可移植的代码迈进了一步。
在几种不同的机器上运行如下图所示的代码,得到后面所示的结果。我们使用了以下几种机器:
- Linux 32:运行 Linux 的 Intel IA32 处理器。
- Windows:运行 Windows 的 Intel IA32 处理器。
- Sun:运行 Solaris 的 Sun Microsystems SPARC 处理器。(这些机器现在由 Oracle 生成。)
- Linux 64:运行 Linux 的 Intel x86-64 处理器。
void test_show_bytes(int val) {
int ival = val;
float fval = (float) ival;
int *pval = &ival;
show_int(ival);
show_float(fval);
show_pointer(pval);
}
机器 | 值 | 类型 | 字节(十六进制) |
---|---|---|---|
Linux 32 | 12345 | int | 3930-0000 |
Windows | 12345 | int | 3930-0000 |
Sun | 12345 | int | 0000-3039 |
Linux 64 | 12345 | int | 3930-0000 |
Linux 32 | 12345.0 | float | 00e4-4046 |
Windows | 12345.0 | float | 00e4-4046 |
Sun | 12345.0 | float | 4640-e400 |
Linux 64 | 12345.0 | float | 00e4-4046 |
Linux 32 | &ival | int * | e4f9-ffbf |
Windows | &ival | int * | b4cc-2200 |
Sun | &ival | int * | efff-fa0c |
Linux 64 | &ival | int * | b811-e5ff-ff7f-0000 |
参数 12 345 的十六进制表示为 Ox00003039。对于 int 类型的数据,除了字节顺序以外,我们在所有机器上都得到相同的结果。特别地,我们可以看到在 Linux 32、Windows、和 Linux 64 上,最低有效字节值 Ox39 最先输出,这说明它们是小端法机器;而在 Sun 最后输出,这说明 Sun 是大端法机器。同样地, float 数据的字节,除了字节顺序以外,也都是相同的。另一方面,指针值却是完全不同的。不同的机器/操作系统配置使用不同的存储分配规则。一个值得注意的特性是 Linux 32、Windows 和 Sun 的机器使用 4 字节地址,而 Linux 64 使用 8 字节地址。
可以观察到,尽管浮点型和整型数据都是对数值 12 345 编码,但是它们有截然不同的字节模式:整型为 Ox00003039,浮点数为 Ox4640E400。一般而言,这两种格式使用不同的编码方法。 如果我们将这些十六进制模式扩展为二进制形式,并且适当地将它们移位,就会发现一个有 13 个相匹配的 的序列,用一串星号标识出来:
0 0 0 0 3 0 3 9
0000-0000-0000-0000-0011-0000-0011-1001
4 6 4 0 E 4 0 0
0100-0110-0100-0000-1110-0100-0000-0000
这并不是巧合。当我们研究浮点数格式时,还将再回到这个例子。
网友评论