大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组,称为虚拟内存(virtual memory)。内存的每个字节都由一个唯一的数字来标识,称为它的地址(address),所有可能地址的集合就称为虚拟地址空间(virtual address space)。虚拟内存概念参考:
https://www.jianshu.com/p/415618863d52
2.1.1 十六进制表示法
一个字节由8位组成。在二进制表示法中,它的值域是00000000 ₂~ 11111111₂。如果看成十进制整数,它的值域为 010~25510。这两种符号表示法对于描述位模式来说都不是非常方便。常用的是以16为基数,或者叫做十六进制数,来表示位模式。十六进制(简写hex)使用数字 0 ~ 9 以及字符 A~F来表示16个可能的值。用十六进制书写,一个字节的值域 0016~FF16。
练习题2.1 完成下面的数字转换:
A. 将0x39A7F8转换为二进制
十六进制 3 9 A 7 F 8
二进制 0011 1001 1010 0111 1111 1000
B. 将二进制1100100101111011 转化为十六进制
二进制 1100 1001 0111 1011
十六进制 C 9 7 B
C. 将0xD5E4C转化为二进制
十六进制 D 5 E 4 C
二进制 1101 0101 1110 0100 1100
D. 将二进制10011011110011110110101
二进制 100 1101 1110 0111 1011 0101
十六进制 4 D E 7 B 5
2.1.2 字数据大小
每台计算机都有一个字长(word size),指明指针数据的标称大小。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为w位的机器而言,虚拟地址的范围为0~2W-1,程序最多访问2W个字节。
常用的32位字长限制虚拟地址空间为4千兆字节(4GB),刚刚超过4*109字节。扩展到64位字长使得虚拟地址空间为16EB,大约1.84*1019字节。
大多数64位机器也可以运行为32位机器编译的程序,这是一种向后兼容。我们将程序称为“32位程序”或“64位程序”时,区别在于该程序是如何编译的,而不是其运行的机器类型。
计算机和编译器支持多种不同方式编码的数字格式,如不同长度的整数和浮点数。C语言支持整数和浮点数的多种数据格式。如下图展示了C语言各种数据类型分配的字节数。有些数据类型的确切字节数依赖于程序是如何被编译的。数据类型short、int、long可以提供各种数据大小,即使是为64位系统编译,数据类型int通常也只有4个字节。数据类型long一般在32位程序中为4字节,在64位程序中则为8字节。
下图还展示了指针(例如一个被声明为类型为“char *”的变量)使用程序的全字长。大多数机器还支持两种不同的浮点数格式:单精度(float)和双精度(double),分别使用4字节和8字节。
C声明 | 字节数 | ||
有符号 | 无符号 | 32位 | 64位 |
[signed] char | unsigned char | 1 | 1 |
short | unsigned short | 2 | 2 |
int | unsigned | 4 | 4 |
long | unsigned long | 4 | 8 |
int32_t | uint32_t | 4 | 4 |
int64_t | uint64_t | 8 | 8 |
char * | 4 | 8 | |
float | 4 | 4 | |
double | 8 | 8 |
2.1.3 寻址和字节顺序
对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中的最小地址。例如,假设一个类型为int的变量x的地址为0x100,也就是说,地址表达式&x的值为0x100。那么,int(32位,4个字节表示)将被存储在内存的0x100、0x101、0x102和0x103位置。
排列表示一个对象的字节有两种通用规则。一个w位的整数,其位表示为[xw-1,xw-2,''',x1,x0],其中xw-1是最高有效位,x0是最低有效位。假设w是8的倍数,这些位就能被分组称为字节,其中最高有效字节包含位[xw-1,xw-2,''',xw-8],而最低有效字节包含位[x8,''',x1,x0],其他字节包含中间的位。某些机器选择在内存中安照从低有效字节到最高有效字节的顺序存储对象,而另一些机器按照从最高有效字节到最低有效字节的顺序存储。其中,最低有效字节在最前面的方式,称为小端法。最高有效字节在最前面的方式,称为大端法。
假设变量x的类型为int,位于地址0x100处,它的十六进制值为0x01234567。地址范围0x100~0x103的字节顺序依赖于机器的类型:
大端法
0x100 0x101 0x102 0x103
··· | 01 | 23 | 45 | 67 | ··· |
小端法
0x100 0x101 0x102 0x103
··· | 67 | 45 | 23 | 01 | ··· |
注意,在字0x01234567中,高位字节的十六进制值为0x01,而低位字节值位0x67。
大多数Intel兼容机都只用小端模式。
练习题2.5 思考对show_bytes的三次调用:
typedef unsigned char *byte_pointer;
show_bytes(byte_pointer start, size_t len){
size_t i;
for(i = 0; i < len; i++)
printf("%.2f",start[i]);
}
int val = 0x87654321;
byte_pointer valp = (byte_pointer) &val;
show_bytes(valp,1); A:小端法: 21 大端法:87
show_bytes(valp,2); A:小端法: 2143 大端法:8765
show_bytes(valp,3); A:小端法: 214365 大端法:876543
2.1.4 表示字符串
C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。每个字符都由某个标准编码来表示,最常见的是ASCII字符码。因此,我们以参数"12345"和6(包括终止符)来运行例show_bytes,得到结果 31 32 33 34 35 00。请注意,十进制数字x的ASCII码正好是0x3x,而终止字节的十六进制表示为0x00。在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。
练习题2.7 对show_bytes调用
const char *s = "abcdef";
show_bytes((byte_pointer) s,5);
输出 61 62 63 64 65 66
其中字母 'a'~'z' 的ASCII码为 0x61~0x7A。
2.1.5 表示代码
在示例机器上编译时,生成不同字节表示的机器代码。不同的机器类型使用不同且不兼容的指令和编码方式。即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。二进制代码很少能在不同机器和操作系统组合之间移植。
计算机系统的一个基本概念就是,从机器的角度来看,程序进行只是字节序列。
2.1.6 布尔代数简介
二进制值是计算机编码、存储和操作信息的核心。布尔注意到通过逻辑值TRUE(真)和FALSE(假)编码成二进制1和0,能够设计出一种代数,以研究逻辑推理的基本原则。
最简单的布尔代数是在二元集合{0,1}基础上定义。布尔代数的运算,二进制值1和0表示逻辑值TRUE或者FALSE,而运算符~、&、|和^ 分别表示逻辑运算NOT、AND、OR、EXCLUSIVE-OR。
后来创立信息论领域的Claude Shannon 首先建立了布尔代数和数字逻辑之间的联系。他表明了布尔代数可以用来设计和分析机电继电器网络。尽量那时计算机技术已经取得到了相当的发展,但是布尔代数仍在数字系统的设计和分析中扮演者重要的角色。
我们可以将上述4个布尔运算扩展到位向量的运算,位向量就是固定长度为W、由0和1组成的串。位向量的运算可以定义成参数的每个对应元素之间的运算。假设a和b分别表示位向量[aw-1,aw-2,...,a0]和[bw-1,bw-2,...,b0]。我们将a&b也定义为一个长度为w的向量,其中第i个元素等于ai&bi,0<i<w。可以用类似的方式将运算|、^和~扩展到位向量上。
练习题 2.8 填写下表,给出位向量的布尔运算的求值结果
运算 | 结果 |
a | [0110 1001] |
b | [0101 0101] |
~a | [1001 0110] |
~b | [1010 1010] |
a&b | [0100 0001] |
a|b | [0111 1101] |
a^b | [0011 1100] |
位向量一个很有用的应用就是表示有限集合。我们可以用位向量[aw-1,aw-2,...,a0]编码任何子集A ⊆{0,1,...,w-1},其中ai = 1当且仅当i∈A。例如位向量 a = [01101001]表示集合A={0,3,5,6},而b=[01010101]表示集合B = {0,2,4,6}。使用这种编码集合的方法,布尔运算|和&分别对应于集合的并和交,而~对应于集合的补。运算a&b得到位向量[01000001],而A∩B = {0,6}。
在大量实际应用中,我们都能看到用位向量来对集合编码。例如,我们能够通过指定一个位向量掩码,有选择地使能或是屏蔽一些信号,其中某一位位置上位1时,表明信号i是有效的(使能),而0表明该信号是被屏蔽的。因而,这个掩码表示的就是设置为有效信号的集合。
2.1.7 C语言中的位级运算
C语言的一个很有用的特性就是它支持按位布尔运算。事实上,我们在布尔运算中使用的那些符号就是C语言所使用的:|就是OR(或),&就是AND(与),~就是NOT(取反),而^就是EXCLUSIVE-OR(异或)。这些运算能运用到任何“整数”的数据类型上。以下是一些对char数据类型表达式求值的例子:
C的表达式 | 二进制表达式 | 二进制结果 | 十六进制结果 |
~0x41 | ~[0100 0001] | [1011 1110] | 0xBE |
~0x00 | ~[0000 0000] | [1111 1111] | 0xFF |
0x69 & 0x55 | [0110 1001]&[0101 0101] | [0100 0001] | 0x41 |
0x69 | 0x55 | [0110 1001] |[0101 0101] | [0111 1101] | 0x7D |
如示例所示,确定一个位级表达式的最好的方法,就是将十六进制的参数扩展成二进制表达并执行二进制运算,然后再转换回十六进制。
练习题 2.10 对于任一位向量a,有 a^a = 0。应用这一属性,考虑下面的程序:
void inplace_swap(int *x, int *y){
*y = *x ^ *y; /*Step1*/
*x = *x ^ *y; /*Step2*/
*y = *x ^ *y; /*Step3*/
}
填写每一步运行结果:
步骤 | *x | *y |
初始 | a | b |
Step1 | a | a ^ b |
Step2 | a ^ a ^ b = b | a ^ b |
Step3 | a ^ a ^ b = b | a ^ a ^ b ^ a ^ b = a |
位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的位的集合。掩码0xFF(最低的8位为1)表示一个字的低位字节。位级运算x&0xFF生成一个由x的最低有效字节组成的值,而其他的字节就被置为0。比如,对于x = 0x89ABCDEF,其表达式将得到0x000000EF。表达式~0 将生成一个全1的掩码,不管机器的字大小是多少。