深入理解计算机系统第二章学习总结
我们对计算机系统的探索是从学习计算机本身开始的,它由处理器和
存储器子系统组成。在核心部分,我们需要方法来表示基本数据类型,比
如整数和实数运算的近似值。然后,我们考虑机器级指令如何操作这样的 数据,以及编译器如何将 C 程序翻译成这样的指令。接下来,研究几种实
现处理器的方法,帮助我们更好地了解硬件资源是如何被用来执行指令。 一旦理解了编译器和机器级代码,我们就能通过编写最高性能的 C 程序,
来分析如何最大化程序的性能。
二进制的历史
现代计算机存储和处理的信息以二值信号表示。这些微不足道的二进制数字,或者称为位 (bit),奠定了数字革命的基础。大家熟悉并且使用了 1000 多年的十进制(以十为基数)起源于 印度,12 世纪被阿拉伯数学家改进,并在 13 世纪被意大利数学家 Leonardo Pisano(公元 11701250,更为大家所熟知的名字是 Fibonacci)带到西方。对于有 10 个手指的人类来说,使用十进 制表示法是很自然的事情,但是当构造存储和处理信息的机器时,二进制的值工作得更好。二值 信号能够很容易地被表示、存储和传输,例如,可以表示为穿孔卡片上有洞或无洞、导线上的电压或低电压,或者顺时针或逆时针的磁场。对二值信号进行存储和执行计算的电子电路非常简 单和可靠,制造商能够在一个单独的硅片上集成数百万甚至数十亿个这样的电路。
三种数字表示
无符号(unsigned)编码基于传统的二进制表示法,表示 大于或者等于零的数字。补码(two’s-complement)编码是表示有符号整数的最常见的方式,有 符号整数就是可以为正或者为负的数字。浮点数(?oating-point)编码是表示实数的科学记数法 的以二为基数的版本。
溢出(over?ow)的例子:
现在的大多数计算机 (使用 32 位来表示数据类型int),计算表达式200*300*400*500会得出结果 -884 901 888。 这违背了整数运算的特性,计算一组正数的乘积不应产生一个为负的结果。
整数的计算机运算满足人们所熟知的真正整数运算的定律。
利用乘法的结 合律和交换律,计算下面任何一个 C 表达式,都会得出结果 -884 901 888 : (500*400)*(300*200) ((500*400)*300)*200 ((200*500)*300)*400 400*(200*(300*500))
计算机可能没有产生期望的结果,但是至少结果是一致的!
C 编程语言的演变
前面提到过,C 编程语言是贝尔实验室的 Dennis Ritchie 最早开发出来的,目的是和 Unix 操 作系统一起使用(Unix 也是贝尔实验室开发的)。在那个时候,大多数系统程序,例如操作系 统,为了访问不同数据类型的低级表示,都必须用大量的汇编代码编写。比如说,像malloc 库函数提供的内存分配那样的功能,用当时的其他高级语言是无法编写的。
信息存储
大多数计算机使用8位的块,或者字节(byte),作为 最小的可寻址的存储器单位,而不是在存储器中访问单独 的位。机器级程序将存储器视为一个非常大的字节数组, 称为虚拟存储器(virtual memory)。存储器的每个字节都由 一个唯一的数字来标识,称为它的地址(address),所有可 能地址的集合称为虚拟地址空间(virtual address space)。顾 名思义,这个虚拟地址空间只是一个展现给机器级程序的 概念性映像。实际的实现(见第9章)是将随机访问存储器(RAM)、磁盘存储器、特殊硬件和操作 系统软件结合起来,为程序提供一个看上去统一的字节数组。
十六进制表示法
一个字节由 8 位组成。在二进制表示法中,它的值域是 000000002 ~ 111111112 ;如果用十 进制整数表示,它的值域就是 010 ~ 25510。两种表示法对于描述位模式来说都不是非常方便。二 进制表示法太冗长,而十进制表示法与位模式的互相转化又很麻烦。替代的方法是,以 16 为基 数,或者叫十六进制(hexadecimal)数,来表示位模式。十六进制(简写为“hex”)使用数字 ‘0’~‘9’,以及字符‘A’~‘F’来表示 16 个可能的值。图 2-2 展示了 16 个十六进制数字 对应的十进制值和二进制值。用十六进制书写,一个字节的值域为 0016 ~ FF16。
比如,假设给你一个数字0x173A4C,可以通过展开每个十六进制数字,将它转换为二进 制格式,如下所示 : 十六进制 1 7 3 A 4 C 二进制 0001 0111 0011 1010 0100 1100 这样就得到了二进制表示000101110011101001001100。 反过来,如果给定一个二进制数字 1111001010110110110011,你可以首先把它分为每 4 位一 组,再把它转换为十六进制。不过要注意,如果位的总数不是 4 的倍数,最左边的一组可以少于 4 位,前面用 0 补足,然后将每个 4 位组转换为相应的十六进制数字 : 二进制 11 1100 1010 1101 1011 0011 十六进制 3 C A D B 3
十进制和十六进制表示之间的转换
将一个十进制数 字 x 转换为十六进制,可以反复地用 16 除 x,得到一个商 q 和一个余数 r,也就是 x = q×16 + r。 然后,我们用十六进制数字表示的 r 作为最低位数字,并且通过对 q 反复进行这个过程得到剩下 的数字。例如,考虑十进制 314156 的转换 : 314156 = 19634×16 + 12 (C) 19634 = 1227×16 + 2 ( 2) 1227 = 76×16 + 11 (B) 76 = 4×16 + 12 (C) 4 = 0×16 + 4 (4) 从这里,我们能读出十六进制表示为0x4CB2C。 反过来,将一个十六进制数字转换为十进制数字,我们可以用相应的 16 的幂乘以每个十六 进制数字。比如,给定数字0x7AF,我们计算它对应的十进制值为 7×162 + 10×16 + 15=7×256 + 10×16+15=1792+160+15=1967。
字
每台计算机都有一个字长(word size),指明整数和指针数据的标称大小(nominal size)。因 为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的 最大大小。也就是说,对于一个字长为 w 位的机器而言,虚拟地址的范围为 0 ~ 2w-1,程序最多 访问 2w 个字节。
数据大小
计算机和编译器支持多种不同方式编码的数字格式,如整数和浮点数,以及其他长度的数 字。比如,许多机器都有处理单个字节的指令,也有处理表示为 2 字节、4 字节或者 8 字节整数 的指令,还有些指令支持表示为 4 字节和 8 字节的浮点数。 C 语言支持整数和浮点数的多种数据格式。C 的数据类型char表示一个单独的字节。尽管 “char”是由于它被用来存储文本串中的单个字符这一事实而得名,但它也能用来存储整数值。 C 的数据类型int之前还能加上限定词short、long,以及最近的long long,以提供各种 大小的整数表示
寻址和字节顺序
对于跨越多字节的程序对象,我们必须建立两个规则 :这个对象的地址是什么,以及在存 储器中如何排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对 象的地址为所使用字节中最小的地址。例如,假设一个类型为int的变量x的地址为0x100, 也就是说,地址表达式&x的值为0x100。那么,x的 4 个字节将被存储在存储器的0x100、 0x101、0x102和0x103位置。某些机器选择在存储器中按照从最低有效字节到最高 有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效字节的顺序存储。前 一种规则—最低有效字节在最前面的方式,称为小端法(little endian)。大多数 Intel 兼容机都 采用这种规则。后一种规则—最高有效字节在最前面的方式,称为大端法(big endian)。大多 数 IBM 和 Sun Microsystems 的机器都采用这种规则。注意我们说的是“大多数”。这些规则并没 有严格按照企业界限来划分。比如,IBM 和 Sun 制造的个人计算机使用的是 Intel 兼容的处理器, 因此用的就是小端法。许多比较新的微处理器使用双端法(bi-endian),也就是说可以把它们配 置成作为大端或者小端的机器运行。
字节顺序变得可见的第三种情况是当编写规避正常的类型系统的程序时。在 C 语言中,可 以使用强制类型转换(cast)来允许以一种数据类型引用一个对象,而这种数据类型与创建这个 对象时定义的数据类型不同。大多数应用编程都强烈不推荐这种编码技巧,但是它们对系统级编 程来说是非常有用,甚至是必需的。
给 C 语言初学者 :使用typedef命名数据类型:
C 语言中的typedef声明提供了一种给数据类型命名的方式。这能够极大地改善代码的可 读性,因为深度嵌套的类型声明很难读懂。
给 C 语言初学者 :使用printf格式化输出:
printf函数(还有它的同类fprintf和sprintf)提供了一种打印信息的方式,这种 方式对格式化细节有相当大的控制能力。第一个参数是格式串(format string),而其余的 参数都是要打印的值。在格式串里,每个以‘%‘开始的字符序列都表示如何格式化下一个参数。 典型的示例有 :‘%d‘是输出一个十进制整数,‘%f‘是输出一个浮点数,而‘%c‘是输出一个 字符,其编码由参数给出。
给 C 语言初学者 :指针和数组
在函数show_bytes(图 2-4)中,我们看到指针和数组之间紧密的联系,这将在 3.8 节中 详细描述。这个函数有一个类型为byte_pointer(被定义为一个指向unsigned char的 指针)的参数start,但是我们在第 8 行上看到数组引用start[i]。在 C 语言中,我们能够 用数组表示法来引用指针,同时我们也能用指针表示法来引用数组元素。在这个例子中,引用 start[i]表示我们想要读取以start指向的位置为起始的第i个位置处的字节。
给 C 语言初学者 :指针的创建和间接引用
我们看到对 C 和 C++ 中两种独有操作的使用。C 的“取地址” 运算符&创建一个指针。在这三行中,表达式&x创建了一个指向保存变量x的位置的指针。这 个指针的类型取决于x的类型,因此这三个指针的类型分别为int*、?oat*和void**。(数 据类型void*是一种特殊类型的指针,没有相关联的类型信息。) 强制类型转换运算符可以将一种数据类型转换为另一种。因此,强制类型转换(byte_ pointer)&x表明无论指针&x以前是什么类型,它现在就是一个指向数据类型为unsigned char的指针。这里给出的这些强制类型转换不会改变真实的指针,它们只是告诉编译器以新的 数据类型来看待被指向的数据。
Linux 32 :运行 Linux 的 Intel IA32 处理器
Windows :运行 Windows 的 Intel IA32
Sun : 运行 Solaris 的 Sun Microsystems SPARC 处理器
Linux 64 :运行 Linux 的 Intel x86-64 处理器
布尔代数简介
二进制值是计算机编码、存储和操作信息的核心,所以围绕数值 0 和 1 的研究已经演化出了 丰富的数学知识体系。这起源于 1850 年前后乔治 • 布尔(George Boole,1815—1864)的工作, 因此也称为布尔代数(Bool algebra)。布尔注意到通过将逻辑值 TRUE(真)和 FALSE(假)编 码为二进制值 1 和 0,能够设计出一种代数,以研究逻辑推理的基本原则。 最简单的布尔代数是在二元集合 {0,1} 基础上的定义。图 2-7 定义了这种布尔代数中的几 种运算。我们用来表示这些运算的符号是和 C 语言的位级运算使用的符号相匹配的,这些将在 后面讨论到。布尔运算 ~ 对应于逻辑运算 NOT,在命题逻辑中用符号﹁表示。也就是说,当 P 不是真的时候,我们就说﹁ P 是真的,反之亦然。相应地,当 P 等于 0 时,~P 等于 1,反之亦 然。布尔运算 &对应于逻辑运算AND,在命题逻辑中用符号∧表示。当P和Q都为真时,我们说 P∧Q为真。相应地,只有当p =1且q =1时,p & q才等于1。布尔运算|对应于逻辑运算OR,在 命题逻辑中用符号∨表示。当P或者Q为真时,我们说P∨Q成立。相应地,当p =1或者q =1时, p|q等于1。布尔运算^对应于逻辑运算异或,在命题逻辑中用符号σ表示。当P或者Q为真但 不同时为真时,我们说PσQ成立。相应地,当p =1且q =0,或者p =0且q =1时,p^q等于1。
C 语言中的位级运算
C 语言的一个很有用的特性就是它支持按位布尔运算。事实上,我们在布尔运算中使用的那 些符号就是 C 语言所使用的 : | 就是 OR(或),& 就是 AND(与),~ 就是 NOT(取反),而 ^ 就 是 EXCLUSIVE-OR(异或)。这些运算能运用到任何“整型”的数据类型上,也就是那些声明为 char或者int的数据类型,无论它们有没有像short、long、long long或者unsigned 这样的限定词。
整数表示
在本节中,我们描述用位来编码整数的两种不同的方式 :一种只能表示非负数,而另一种能 够表示负数、零和正数。后面我们将会看到它们的数学属性和机器级实现方面密切关联。我们还 会研究扩展或者收缩一个已编码整数以适应不同长度表示的效果。