CSAPP读书笔记 第一部分 程序结构和执行 1.1

计算机,由处理器和存储器子系统组成。

首先我们需要方法表示基本数据类型,比如整数和实数的近似值。然后我们考虑指令如何操作这样的数据,编译器如何将C程序翻译成这样的指令。接下来,我们研究几种实现处理器的方法,帮助我们更好地了解硬件资源如何被用来执行指令

学习这一部分,你将学会一些技巧,帮助你写出安全、可靠、资源利用率高的程序。

信号的表示和处理

现代计算机存储和处理的信息以二值信号表示,称为位(bit)

二值信号能够很容易地被表示、存储和传输,例如,可以表示为穿孔卡片上有洞或无洞、导线上的高电压或低电压,或者顺时针或逆时针的磁场。对二值信号进行存储和执行计算的电子电路非常简单和可靠,制造商能够在一个单独的硅片上集成数百万甚至数十亿个这样的电路。

当把位组合在一起,再加上某种解释 (inter­pretation) , 即赋予不同的可能位模式以含意,我们就能够表示任何有限集合的元素。比如,使用一个二进制数字系统,我们能够用位组来编码非负数(部分)。通过使用标准的字符码,我们能够对文档中的字母和符号进行编码。

有限数量的位,表示有限集合的元素。当元素太大以至于超出集合时,就会溢出。溢出会导致意想不到的结果,比如正整数相乘竟然得到负数结果。此外,对于整数,计算机运算满足人们所熟知的真正整数运算的许多性质(也许结果会溢出,但是满足运算律的运算,结果都是相同的)

浮点运算有和整数运算完全不同的数学属性。首先,虽然同样可能溢出,但是正数的乘积总是正的。第二,由于表示的精度有限,浮点运算是不可结合的,就是运算顺序一改变,答案可能就会改变。之所以有不同的数学属性,是因为它们表示方式不同:整数的表示范围小,但是精确;浮点数表示范围大,但是这种表示只是近似的。

了解数字的实际表示、可以表示的值的范围、不同算术运算的属性,对于使编写的程序在数值范围内正确工作,提升跨越不同机器、OS、编译器的可移植性是很有帮助的。

从编码的基本定义开始,然后得出一些属性,例如可表示的数字的范围、它们的位级表示以及算术运算的属性。我们相信从这样一个抽象的观点来分析这些内容,对你来说是很重要的,因为程序员需要对计算机运算与更为人熟悉的整数和实数运算之间的关系有清晰的理解。这部分内容安排如下:首先给出以数学形式表示的属性,作为原理 。 然后,解释这个原理 。 我们建议你反复阅读原理描述和它的示例与讨论,直到你对该属性的说明内容及其重要性有了牢固的直觉 。 对于更加复杂的属性,还会提供推导,其结构看上去将会像一个数学证明 。 虽然最终你应该尝试理解这些推导,但在第一次阅读时你可以跳过它们 。

信息存储

大多数计算机使用 8 位的块,或者宇节 (byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位。

机器级程序将内存视为一个非常大的字节数组,称为虚拟内存(virtual memory) 。 内存的每个字节都由一个唯一的数字来标识,称为它的地址(ad­dress),所有可能地址的集合就称为虚拟地址空间 (virtual address space) 。 顾名思义,这个虚拟地址空间只是一个展现给机器级程序的概念性映像。实际的实现是将动态随机访问存储器 (DRAM ) 、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的字节数组。

编译器和运行时系统将存储器空间划分为更可管理的单元,来存放不同的程序对象 (program object ) , 即程序数据、指令和控制信息 。可以用各种机制来分配和管理程序不同部分的存储。这种管理完全是在虚拟地址空间里完成的 。 例如, C 语言中一个指针的值(无论它指向一个整数、一个结构或是某个其他程序对象)都是某个存储块的第一个字节的虚拟地址。 C 编译器还把每个指针和类型信息联系起来 , 这样就可以根据指针值的类型,生成不同的机器级代码来访问存储在指针所指向位置处的值。尽管 C 编译器维护着这个类型信息,但是它生成的实际机器级程序并不包含关于数据类型的信息。每个程序对象可以简单地视为一个字节块 , 而程序本身就是一个字节序列 。

一个字节由 8 位组成。在二进制表示法中,它的值域是 00000000 ~ 11111111。如果看成十进制整数,它的值域就是 0 ~255 。 两种符号表示法对于描述位模式来说都不是非常方便。二进制表示法太冗长,而十进制表示法与位模式的互相转化很麻烦。

替代的方法是,以 16 为基数,或者叫做十六进制 (hexadecimal)数,来表示位模式。十六进制(简写为 "hex")使用数字 '0'~'9' 以及字符 'A'~'F' 来表示 16 个可能的值。在 C 语言中,0X 或 0x 开头的数字常量被认为是十六进制的值。字符 'A'~'F'既可以是大写,也可以是小写。

这样,编写机器级程序的一个常见任务就是在位模式的十进制、二进制和十六进制表示之间人工转换

二进制和十六进制之间的转换比较简单直接,因为可以一次执行一个十六进制数字的转换(1个十六进制数字表示范围是0~15,4位二进制数字也是,所以1个十六进制数字对应一个4位二进制数字)。将二进制转化为十六进制也很容易。可以通过先把它分为每 4 位一组,再对每组转换。如果位总数不是4的倍数,最左边的一组可以少于4位,缺少的位用0补足。

 0x173A4C -> 0001 0111 0011 1010 0100 1100、11 1100 1010 1101 1011 0011->3CADB3

一个特殊情况是二进制数是2的非负整数 n 次幂时,也就是x写成1后面跟n个0,此时把x写成十六进制:n=i+4j,开头是1(i=0)或2(i=1)或4(i=2)或8(i=3),后面是j个0。

 十进制和十六进制之间的转换需要用乘法或除法来处理一般情况。

10->16,可以反复用16除x,写成x=16*q+r。然后以十六进制表示的r作为最低位数字,对q继续处理得到剩下的数字。314156->0x4CB2C

16->10,可以用每一位相应的16的幂乘以每个十六进制数字。0x7AF->1967

每台计算机都有一个字长 (word size), 指明指针数据的标称大小 (nominal size) 。 因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为w位的机器而言,虚拟地址的范围为 0~2^w-1,程序最多访问2^w个字节。

最近这些年,出现了大规模的从 32 位字长机器到 64 位字长机器的迁移。这种情况首先出现在为大型科学和数据库应用设计的高端机器上,之后是台式机和笔记本电脑,最近则出现在智能手机的处理器上。 32 位字长限制虚拟地址空间为 4 千兆字节(写作 4GB),64位则使得虚拟地址空间为16EB。

大多数 64 位机器也可以运行为 32 位机器编译的程序,这是一种向后兼容。因此,我们将程序称为 "32 位程序”或 "64 位程序”时,区别在于该程序是如何编译的,而不是其运行的机器类型。

计算机和编译器支持多种不同方式编码的数字格式,如不同长度的整数和浮点数,也就是说有专门针对这些长度的数的指令。许多机器都有处理单个字节的指令,也有处理编码为 2 字节、 4 字节或者 8 字节整数的指令,还有些指令支持编码为 4 字节和 8 字节的浮点数。

有些数据类型的确切字节数依赖于程序是如何被编译的。我们给出 32 位和 64 位程序的典型值

整数或者为有符号的,即可以表示负数、零和正数;或者为无符号的,即只能表示非负数。 C 的数据类型 char 表示一个单独的字节。尽管 "char" 是由于它被用来存储文本串中的单个字符这一事实而得名,但它也能被用来存储整数值。数据类型 short 、int 和 long 可以表示各种大小的整数。即使是为 64 位系统编译,数据类型 int 通常也只有4 个字节。数据类型 long 一般在 32 位程序中为 4 字节,在 64 位程序中则为 8 字节。

为了避免由于依赖"典型”大小和不同编译器设置带来的奇怪行为, ISO C99 引入了一类数据类型,其数据大小是固定的,不随编译器和机器设置而变化。 其中就有数据类型int32_t 和 int64_t,它们分别为 4 个字节和 8 个字节 。使用确定大小的整数类型是程序员准确控制数据表示的最佳途径。

大多数机器还支持两种不同的浮点数格式:单精度(在 C 中声明为 float)和双精度(在 C 中声明为 double) 。 这些格式分别使用 4 字节和 8 字节。

可移植性的一个方面就是使程序对不同数据类型的确切大小不敏感。 C 语言标准对不同数据类型的数字范围设置了下界,但是却没有上界。因为从 1980 年左右到 2010 年左右, 32 位机器和 32 位程序是主流的组合,许多程序的编写都假设为 32 位程序的字节分配 。 随着 64 位机器的日益普及,在将这些程序移植到新机器上时,许多隐藏的对字长的依赖性就会显现出来,成为错误。

对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。

排列表示一个对象的字节有两个通用的规则 。 考虑一个w位的整数,其位分别为[x_{w-1},x_{w-2},...,x_1,x_0], 其中 Xw-1是最高有效位,而 X0是最低有效位 。 假设 w 是 8 的倍数,这些位就能被分组成为字节,其中最高有效字节包含位 Xw-1~Xw-8,而最低有效字节包含位X7~X0,其他字节包含中间的位 。 某些机器选择在内存中按照从最低有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效字节的顺序存储。前一种规则——最低有效字节在最前面的方式,称为小端法 (little endian) 。后一种规则—-—最高有效字节在最前面的方式,称为大端法 (big endian)。

0x0123->大端:01 23;小端:23 01。

大多数 Intel 兼容机都只用小端模式 。 另一方面, IBM 和 Oracle(从其 2010 年收购Sun Microsystems 开始)的大多数机器则是按大端模式操作 。 注意我们说的是“大多数”。这些规则并没有严格按照企业界限来划分 。 比如, IBM 和 Oracle 制造的个人计算机使用的是 Intel 兼容的处理器,因此使用小端法。许多比较新的微处理器是双端法 (bi-endian),也就是说可以把它们配置成作为大端或者小端的机器运行。然而,实际情况是:一旦选择了特定操作系统,那么字节顺序也就固定下来。比如,用于许多移动电话的 ARM 微处理器,其硬件可以按小端或大端两种模式操作,但是这些芯片上最常见的两种操作系统——Android和IOS,却只能运行小端模式。

有时候,字节顺序会成为问题。一个常见的问题是当小端法机器产生的数据被发送到大端法机器或者反过来时,接收程序会发现,字里的字节成了反序的。为了避免这类问题,网络应用程序的代码编写必须遵守已建立的关于字节顺序的规则,以确保发送方机器将它的内部表示转换成网络标准,而接收方机器则将网络标准转换为它的内部表示。另外,当阅读表示整数数据的字节序列时字节顺序也很重要。这通常发生在检查机器级程序时。字节顺序变得重要的第三种情况是当编写规避正常的类型系统的程序时。在 C 语言中,可以通过使用强制类型转换 (cast)或联合 (union)来允许以一种数据类型引用一个对象,而这种数据类型与创建这个对象时定义的数据类型不同。

C 语言中的 typedef 声明提供了一种给数据类型命名的方式 。 这能够极大地改善代码的可读性,因为深度嵌套的类型声明很难读懂。

强制类型转换运算符可以将一种数据类型转换为另 一种。因此,强制类型转换(byte_pointer)&x 表明无论指针 &x 以前是什么类型,它现在就是一个指向数据类型为 unsigned char 的指针。这里给出的这些强制类型转换不会改变真实的指针,它们只是告诉编译器以新的数据类型来看待被指向的数据

C语言中字符串被编码为一个以 null(其值为 0)字符结尾的字符数组 。 每个字符都由某个标准编码来表示,最常见的是 ASCII 字符码。如果我们以参数 "12345" 和 6(5个字符+1个终止符)来运行例程 show_bytes, 我们得到结果 31 32 33 34 35 00。在使用 ASCII 码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。'a'的ASCII码为0x61。

ASCII 宇符集适合于编码英语文档,但是在表达一些特殊宇符方面并没有太多办法。它完全不适合编码希腊语、俄语和中文等语言的文档。这些年,提出了很多方法来对不同语言的文字进行编码。 Unicode 联合会 (Unicode Consortium)修订了最全面且广泛接受的文字编码标准。当前的 Unicode 标准 (7.0 版)的字库包括将近 100 000 个字符,支持广泛的语言种类,包括古埃及和巴比伦的语言。
基本编码,称为 Unicode 的“统一字符集“,使用 32 位来表示宇符。这好像要求文本串中每个字符要占用 4 个宇节。不过,可以有一些替代编码,常见的宇符只需要 1 个或 2 个字节,而不太常用的字符需要多一些的字节数。特别地, UTF-8 表示将每个字符编码为一个字节序列,这样标准 ASCII 字符还是使用和它们在 ASCII 中一样的单宇节编码,这也就意味着所有的 ASCII 字节序列用 ASCII 码表示和用 UTF-8 表示是一样的。

不同的机器类型使用不同的且不兼容的指令和编码方式。 即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。二进制代码很少能在不同机器和操作系统组合之间移植。计算机系统的一个基本概念就是,从机器的角度来看,程序仅仅只是字节序列。机器没有关于原始源程序的任何信息,除了可能有些用来帮助调试的辅助表以外。

逻辑运算认为所有非零的参数都表示 TRUE, 而参数 0 表示 FALSE。它们返回 1 或者 0,分别表示结果为 TRUE 或者为 FALSE。

逻辑运算符&&和 II 与它们对应的位级运算&和 1 之间第二个重要的区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。

几乎所有的编译器/机器组合都对有符号数使用算术右移,另一方面,对于无符号数,右移必须是逻辑的。

加法 (和减法)的优先级比移位运算要高。当你拿不准的时候,请加上括号!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值