2第1章计算杌系统漫游 应于某个字符。例如,第一个字节的整数值是35,它对应的就是字符‘#’;第二个字节整数值 为105,它对应的字符是‘’,依次类推。注意,每个文本行都是以个不可见的换行符‘\n 来结束的,它所对应的整数值为10。像he11o.c这样只由ASCI字符构成的文件称为文本文 件,所有其他文件都称为二进制文件。 3510511099108117 101326011511610010511146 h n n a1 ()\n 1046210101051101163210997105110404110123 n sp>pr n h 1032 32321121141051101161024034104101108 r d n 10811144321191111141081009211034415910125 图1-2he110.c的ASCI文本表示 he1lo.c的表示方法说明了一个基本的思想:系统中所有的信息——包括磁盘文件、存储 器屮的程序、存储器屮存放的用户数据以及网络上传送的数据,都是由一串位表示的。区分不同 数据对象的唯一方法是我们读到这些数据对象时的上下文。比如,在不同的上下文中,一个同样 的字节序列可能表示个整数、浮点数、字符串或者机器指令 作为程序员,我们需要了解数字的机器表示方式,因为它们与实际的整数和实数是不同的。 它们是对真值的有限近似值,有时候会有意想不到的行为表现。这方面的基木原理将在第2章中 详细描述。 C编程语言的起源 C语言是贝尔实验室的Dennisritchie于1969年~1973年间创建的。美国国家标准学会 (AmericanNationalStandardsInstitute,ANSI)在1989年颁布了ANSIC的标准,后来使C语 言标准化成为了国际标准化组织(InternationalStandardsOrganization,ISO)的责任。这些标准 定义了C语言和一系列函数库,即所谓的“C标准库”。Kernighan和Ritchie在他们的经典著 作中描述了ANSiO,这本著作被人们满怀感情地称为“K&R”[58]。用Ritchie的话来说[88], C语言是“古怪的、有缺陷的,但也是一个巨大的成功”。为什么会成功呢? C语言与Unⅸx操作系统关系密切。C语言从一开始就是作为一种用于Unix系统的程序 设计语言而开发出来的。大部分Unⅸ内核,以及所有支撑工具和函数库都是用C语言编 写的。20世纪阳0年代后期到80年代初期,Unⅸx在高等院校兴起,许多人开始接触C语 言并喜欢上了它。因为Unⅸ几乎全部是用C编写的,所以可以很方便地栘植到新的机器 上,这种特点为C和Unⅸx贏得了更为广泛的支持。 C语言小而简单。掌控C语言设计的是一个人而非一个协会,因此这是一个简洁明了、 没有什么冗赘的一致的设计。K&R这本书用了大量的例子和练习描述了完整的C语言及 其标准庠,而全书不过261页。C语言的简单使它相对而言易于学习,也易于移植到不同 的计算机上。 C语言是为实践目的设计的。C语言是为实现UnⅸX操作系统而设计的。后来,其他人发 现能够用这门语言无障碍地编写他们想要的程序。 C语言是系统级编程的首选,冋时它乜非常适用于应用级程序的编写。然而,它也并非适 正文.ird2 010-10-1914:1717 第!章计算杌系统漫游 用于所有的栏序员和所有的情况。C语言的指针是造成困惑和程序错误的一个常见原因。同时, C语言还缺乏对非常有用的抽象(例如类、对象和异常)的显式支持。像C+十和Java这祥针 对应用级程序的新程序设汁语言解决了这些问題 1.2程序被其他程序翻译成不同的格式 he11程序的生命周期是从一个高级C语言程序开始的,因为这种形式能够被人读懂。然 而,为了在系统上运行he11o.c程序,每条C语句都必须被其他程序转化为一系列的低级机器 语言指令。然后这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形 式存放起来。目标程序也称为可执行目标文件。 在Unⅸx系统上,从源文件到目标文什的转化是山编译器驱动程序完成的: unix>gcc-chellohello.C 在这生,GCC编译器驱动程序读取源程序文件he11○.c,并把它翻译成一个可执行日标文件 he11o。这个翻译的过程可分为四个阶段完成,如图1-3所示。执行这四个阶段的程序(预处理 器、编译器、汇编器和链接器)一起构成了编译系统(compilationsystem)。 prl he1lo·、预处理器he1lo:i编译器|he--0·8汇编器|he12.。链接器 源程序 被修改的(c1)编程序 可重定位 (ld) 可执行 (文本) 源程序 (文本) 日标程序 日标程序 (文本) (二进制) (二进制) 图1-3编译系统 预处理阶段。预处理器(cpφp)根据以字符#开头的命令,修改原始的C程序。比如he11o.℃ 中第1行的#inc1ude命令告诉预处理器读取系统头文件stdio.h的内容, 并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩 展名。 ·编译阶段。编译器(c1)将文本文件he11o.i翻译成文本文件he11o.s,它包含一个 汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地揹述了一条低 级机器语言抬令。汇编语言是廾常有用的,因为它为不同高级语言的不冋同编译器提供了通 川的输出言。例如,C编详器和Fortran编译器产生的输出文件川的都是一样的汇编语言。 汇编阶段。接下来,汇编器(as)将he1lσ.s翻译成机器语言指令,把这些指令打包成 种叫徹可重定位目标程序(relocatableobjectprogram)的格式,并将结果保存在目标文 件he11o.o中。hello.σ文件是一个二进制文件,它的字节编码是机器语言指令而不是 宇符。如果我们在文本编辑器中打开he110.o文件,看到的将是堆乱码 ·娃接阶段。请注意,he11o程序调用了printf函数,它是每个C编译器都会提供的标 准C库中的一个函数。printf函数存在于一个名为printf,o的单独的预编译好了的目 标文件中,而这个文件必须以某种方式合并到我们的he11o.o程序中。链接器(ld)就 负责处理这种合并。结果就得到he11o文件,它是一个可执行目标文件(或者简称为可 执行文件),可以被加载到內存屮,由系统执行。 GNU项目 GCC是GNU(GNU是GNU’sNotUnix的缩写)项目开发土来的众多有用工具之一。GNU 项月是1984年由Richardstallman发起的一个免税的慈善项目。该项月的月标非常宏大,就是开 正文.ird 010-10-19 4第1章计算杌系统漫游 发出一个完整的类Uniⅸx的系统,其源代码能够不受限制地被修改和传播。GNU项目已经开发岀 了一个包含Unⅸx操作系统的所有主要部件的环境,但内核除外,内核是由Linux项目独立发展 而来的。(NU环境包括EMACS编辑器、GCC编译器、GDB调试器、汇编器、链接器、处理 进剖文件的工具以及其他一些部件。GCC编译器已经发展到支持许多不同的语言,能够针对 许多不同的机器生成代码。支持的语言包抬C、C++、Fortran、Java、Pascal、面向对象C语言 (Objective-C)和Ada G、\U项目取得了非凡的成绩,但是却常常被忽略。现代开孜源码运动(通常和Liux联系 在一起)的思想起源是GNU项目中自由软件(freesoftware)的穊念。(此处的free为自由言论 (freespeech)中“自由”之意,而非免费啤酒(freebeer)中“免费”之意。)而且,Linux如此 受欢迎在很大程度上还要归功于(NU工具,因为它们给[inuⅸX內核提供了环境。 1.3了解编译系统如何工作是大有益处的 对于像he11o.·这样简单的程序,我们可以依靠编译系统生成正确有效的机器代码。但 是,有一些重要的原因促使程序员必须知道编译系统是如何工作的,其原因如下 优化程序性能。现代编译器都是成熟的工具,通常可以生成很好的代码。作为程疗员,我 们无需为了写岀高效代码而去了解编译器的内部工作。但是,为了在C程序中做出好的编 码选择,我们确实需要了解一些机器代码以及编译器将不同的C语句转化为机器代码的方 式。例如,一个switch语句是否总是比一系列的if-then-e1se语句高效得多?一个 函数调用的开销有多大?while循环比for循环更有效吗?指针引用比数组索引更有效 吗?为什么将循环求和的结果放到一个本地变量中,与将其放到一个通过引用传递过来的 参数中相比,运行速度要快很多呢?为什么我们只是简单地重新排列一下一个算术表达式 中的括号就能让个函数运行得更快? 在第3章中,我们将介绍两种相关的机器语言:IA32和x86-64。IA32是32位的, 目前普遍应用于运行Linux、Windows以及较新版本的Macintosh操作系统的机器上; x86-64是64位的,可以用在比较新的微处理器上。我们会介绍编译器是如何把不同的 C语言结构转换成它们的机器语言的。第5章,你将学习如何通过简单转换C语言代 码,以帮助编译器更妤地完成工作,从而调整C程序的性能。在第6章,你将学习到 存储器系统的层次结构特性,C语言编译器将数组存放在冇储器中的方式,以及C程 序又是如何能够利用这些知识从而更高效地运行。 ·理解锤接时岀现的错误。根据我们的经验,一些最令人困扰的程序错误往往都与链接器操 作有关,尤其是当你试图构建大型的软件系统时。例如,链接器报告它无法解析一个引 用,这是什么意思?静态变量和全局变量的区别是什么?如果你在不同的C文件中定义了 名字相同的两个全局变量会发生什么?静态库和动态库的区别是什么?我们在命令行上排 列库的顺序有什么影响?最严重的是,为什么有些链接错误直到运行时才会出现?在第7 章,你将得到这些问题的答案。 避免安全漏洞。多年来,缓冲区溢岀错误是造成大多数网络和Internet服务器上安全漏泂 的主要原因。存在这些错误是因为很少有人能理解限制他们从不受信任的站点接收数据 的数量和格式的重要性。学习安仝编程的第一步就是理解数据和控制信息存储在程序栈 上的方式会引起的后果。作为学习汇编语言的一部分,我们将在第3章中描述堆栈原理 和缓冲区溢出错误。我们还将学习程序员、编译器和操作系统可以用来降低攻击威胁的 方法。 正文.ird4 010-10-19 第 计算杌系统漫游 1.4处理器读并解释存储在存储器中的指令 此刻,he1o·C源程序已经被编译系统翻译成了可执行目标文件he11°,并存放在磁盘 上。要想在Unix系统上运行该可执行文件,我们将它的文件名输入到称为外壳(shell)的应 用程序中 unix>./hello hello,world unix> 外壳是个命令行解释器,它输岀个提示符,等待你输入·个命令行,然后执行这个命 令。如果该命令行的第一个单词不是一个内置的外壳命令,那么外壳就会假设这是一个可执行文 件的名字,它将加载并运行这个文件。所以在此例中,外壳将加载并运行he1-o程序,然后等 待程序终止。he11o程序在屏幕上输出它旳信息,然后终止。外壳随后输出一个提示符,等待 下一个输入的命令行 14.1系统的硬件组成 为了理解运行he-1o程序时发生」什么,我们需要」解一个典型系统的硬件组织,如图 1-4所示。这张图是Intelpentium系统产品系列的模犁,但是所有其他系统也有相同的外观和特 性。现在不要担心这张图的复杂性——我们将在本书分阶段介绍大量的细节 CPU 寄存器文件 PC ALU 系统总线存储器总绽 总线接口 主存 桥 储器 IO总线 扩展槽,留待 USB 网络适配器 控制器 图形适配器 磁盘控制器类的设备使用 鼠标键盘显小器 存储在磁盘上的he11° 磁盘可执行文件 图1-4一个典型系统的硬件组成 CPL:中央处理单元;ALU:算术/逻辑单元;PC:程序计数器;USB:通用串行总线 1.总线 贯穿整个系统的是一组电子管道,称做总线,它携带信息字节并负责在各个部件间传递。通 常总线被设计成传送定长的字节块,也就是字(word)。字中的字节数(即字长)是一个基本的 系统参数,在各个系统中的情况都不尽相同。现在的大多数机器字长有的是4个字节(32位),有 的是8个字节(64位)。为了讨论的方便,假设字长为4个字节,并且总线每次只传送1个字。 2.1O设备 输入/输出(IO)设备是系统与外部世界的联系通道。我们的示例系统包括4个IO设备 作为用户输人的键盘和鼠标,作为用户输岀的显示器,以及用于长期存储数据和程序的磁盘驱动 正文.ird5 010-10-19 6第1章计算杌系统漫游 器(简单地说就是磁盘)。最初,叮执行程序he11o就存放在磁盘上。 每个ⅣO设备都通过个控制器或适配器与/O总线相连。控制器和适配器之间的区别主要 在于它们的封装方式。控制器是置于IO设备本身的或者系统的主印制电路板(通常称为主板 上的芯片组,而适配器则是一块插在主板插槽上的卡。无论如何,它们的功能都是在IO总线和 I/O设备之间传递信息。 第6章会更多地说明磁盘之类的IO设备是如何工作的。在第10章,你将学习如何在应用 程序屮利用UnixI/c接口访间设备。我们将特别关注网络类设备,不过这些技术对于其他设备 来说也是通用的。 3.主存 主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理 上来说,主仔是由一组动态随机存取存储器(DRAM)芯片组成的。从逻辑上来说,仔储器是 个线性的字节数组,每个字节都有其唯·的地址(即数组索引),这些地址是从零开始的。殷 来说,组成程序的每条机器指令都由不同数量的字节构成。与C程序变量相对应的数据项的大 小是根据类型变化的。例如,在运行Linux的IA32机器上,short类型的数据需要2个字节, irt、foat和1ong类型需要4个字节,而double类型需要8个字节。 第6章将具体介绍存储技术,如DRAM芯片是如何工作的,以及它们又是如何组合起来构 成主存的。 4.处理器 中央处理单元(CPU),简称处理器,是解释(或执行)存储在主存中指令的引擎。处理器 的核心是一个字长的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC都指向主 存中的某条机器语言指令(即含有该条指令的地址) 从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新 程序计数器,使其指向下一条指令。处理器看上去是按照一个非常筲单的指令执行模型来操作 的,这个模型是由指令集结构决定的。在这个模型屮,指令按照严格的顺序执行,而执行一条指 令包含执行一系列的步骤。处理器从程序计数器(PC)指向的存储器处读取指令,解释指令中 的位,执行该指令指示的简单操作,然后更新PC,使其指向下一条指令,而这条指令并不一定 与存储器中刚刚执行的指令相邻。 这样的简单操作并不多,而且操作是围绕着主存、寄存器文件(registerfile)和算术/逻辑 单元(ALU)进行的。寄存器文件是个小的存储设备,由些1字长的寄存器组成,每个寄 存器都有唯一的名字。ALU计算新的数据和地址值。下面列举一些简单操作的例子,CPU在指 令的要求下可能会执行以下操作 加载:把一个字节或者一个字从主存复制到寄存器,以覆盖寄存器原来的内容。 存储:把一个字节或者一个字从寄存器复制到主存的某个位置,以覆盖这个位置上原来 的内容。 ·操作:把两个奇冇器的内容复制到ALU,ALU对这两个字做算术操作,并将结果存放到 个寄存器中,以覆盖该寄存器中原来的内容。 跳转:从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中 原来的值 处理器看上去只是它的指令集结构的简单实现,但是实际上现代处理器使用了非常复杂的机 制来加速程序的执行。因此,我们可以这样区分处理器的指令集结构和微体系结构:指令集结构 PC也普遍地被用来作为“个人计算机”的缩写。然而,两者之间的区别应该可以很清楚地从上下文中看出来。 正文.ird6 010-10-19 第!章计算杌系统漫游7 措述的是每条机器代码指令的效果;而微体系结构描述的是处理器实际上是如何实现的。第3章 我们研究机器代码时考虑的是机器的指令集结构所提供的抽象性。第4章将更详细地介绍处理器 实际上是如何实现的。 1.4.2运行he11o程序 前面简单描述了系统的硬件组成和操作,现在开始介绍当我们运行示例程序时到底发生了 些什么。在这里我们必须省略很多细节稍后再做补充,但是从现在起我们将很满意这种整体上 的描述 初始时,外壳程序执行它的指令,等待我们输入一个命令。当我们在键盘上输入字符串 /he11o”后,外壳程序将字符逐一读入寄存器,再把它存放到存储器中,如图1-5所示。 CPLI 寄存器文件 PC ALU 系统总线存储器总线 总线接口 主存 hello 储器 I/O总线 扩展槽,留行 USB 网络适配器 控制器 图形适配器 磁盘控制器类的设备使用 鼠标键盘显示器 用户输入 磁盘 图1-5从键盘上读取he110命令 当我们在键盘上敲回车键时,外壳程序就知道我们已经结束了命令的输入。然后外壳执行 系刎指令来加载可执行的he1lo文件,将he11o目标文件中的代码和数据从磁盘复制到主存 数据包括最终会被输出的字符串“he11o,wor1d\n”。 利用直接存储器存取(DMA,将在第6章讨论)的技术,数据可以不通过处理器而直接从 磁盘到达主存。这个步骤如图1-6所示 旦目标文件he11o中的代码和数据被加载到主存,处理器就开始执行he11。程序的 main程序中的机器语言指令。这些指令将“he11o,world\n”字符串中的字节从主存复制到 寄存器文件,再从寄存器文件屮复制到显示设备,最终显示在屏幕上。这个步骤如图1-7所示。 1.5高速缓存至关重要 这个简单的示例揭示了一个重要的问趑,即系统花费了大量的时间把信息从一个地方挪到另 个地方。he11o程序的机器指令最初是存放在磁盘上的,当程序加载时,它们被复制到主存 当处理器运行程序时,指令又从主存复制到处理器。相似地,数据串“he11o,wor1a\n”初 始时在磁盘上,然后复制到主存,最后从主冇上复制到显示设备。从程序员的角庋来看,这些复 制就是开销,减缓了程序“真正”的工作。因此,系统设计者的个主要目标就是使这些复制操 作尽可能快地完成。 正文.ird7 010-10-19 8第1章计算杌系统漫游 CPU 寄存器文件 AlU 系统总线存储器总线 总线接口 主存 ne11o,wor1d、n 桥 储器 he11o代码 IO总线 扩展槽,留待 N络适配器 USB 图形 磁盘|类的设备使用 控制器 适配器 鼠标键盘显示器 存储在磁盘上的he11 磁盘可执行文件 图1-6从磁盘加载可执行文件到主存 CPU 奇存器文件 AlU 系统总线存储器总线 总线接口 主存“he11o,wor1dn 储器|ne10代码 I/O总线 刂」→ 扩展槽,留待 图形 磁盘 网终适配器 控制器 适配器 控制器类的设备使用 鼠标键盘显示器 he11o,wor1d、n 盘存储在磁盘上的he110 可执行文件 图1-7将输出字符串从内存写到显示器 根据机械原理,较大旳存储设备要比较小的存储设备运行得慢,而忺速设备的造价远高于同 类的低速设备。例如,一个典型系统上的磁盘驱动器可能比主存大1000倍,但是对处理器而言 从磁盘驱动器上读取一个字的时间开销要比从主存中读取的开销大1000万倍。 类似地,一个典型的寄存器文件只存储儿百字节的信息,而主存里可冇放儿十亿字节。然 而,处理器从寄存器文件中读数据的速度比从主存中读取几乎要快100倍。更麻烦的是,随着这 些年半导体技术的进步,这种处理器与主存之间的差距还在持续增人。加快处理器的运行速度比 加快主存的运行速度要容易和便宜得多 针对这种处理器与主存之间的差异,系统设计者采用了更小、更快的存储设备,即高遠缓 存存储器(简称高速缓存),作为暂时的集结区城,用来存放处理器近期可能会需要的信息。图 1-8展示了一个典型系统屮的高速缓存存储器。位于处理器芯片上的L1高速缓存的容量可以达 正文.ird8 010-10-19 第!章计算杌系统漫游9 CPU心片 寄存器文件 高速缓存 ALU 存储器 系统总线储器总线 主存 总线接口 )桥 楮器 图1-8高速缓存存储器 到数万宇节,访问速度几乎和访问寄存器文件一样快。一个容量为数十万到数百万字节的更大的 I2高速缓存通过一条特殊的总线连接到处理器。进程访间I2高速缓存的时间要比访问I1高速 缓存的时间长5倍,但是这仍然比访问主存的时间快5~10倍。L1和L2高速缓存是用一种叫 做静态随机访问存储器(SRAM)的硬件技术实现的ε比较新的、处理能力更強大的系统甚至有 三级高速缓存:L1、L2和L3。系统可以获得一个很大的存储器,同时访问速度也很快,原因是 利用了高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势。通过让髙速缓 存里存放可能经常访问的数据的方法,大部分的存储器操作都能在快速的髙速缓存中完成。 本书得出的重要结论之一,就是意识到高速缓存存在的应用程序员可以利用高速缓存将他们 程序的性能提高一个数量级。你将在第6章学习这些重要的设备以及如何利用它们 6存储设备形成层次结构 在处理器和一个又大又慢的设备(例如主存)之间插入一个更小更快的存储设备(例如高速 缓存)的想法已经成为了一个普遍的观念。实际上,每个计算机系统中的存储设备都被组织成了 一个存储器层次结构,如图1-9所示。在这个层次结构中,从上至下,设备变得访问速度越来越 慢、谷量越来越大,并且每字节的造价也越来越便宜。寄仔器文件在层次结构中位于最顶鄙,也 就是第0级或记为L0。这里我们展示的是三层高速缓存L1到L3,占据存储器层次结构的第1 寄 更小 存器 CPU寄存器保存来自高速缓存 更快 存诸器的 /高速缓存 (每字节) SRAM L1高速缓存保存取白L2高速缓存 更贵的 的高速缓存 L2 I2高速缓存 存储设备 (SRAM L2高速缓存保存取自L3高速缓存 L3高速缓存 的高連缓存行 3: SRAM L3高速缓存保存取自主存 更大 主存 的高速缓存行 更 DRAM) 每字节) 主存保存取自本地磁盘 本地二级存储 的磁盘块 更便宜的 (本地磁盘 存储设备 本地磁盘保存取自远程网终 服务器上磁盘的文件 远程级存储 (分布式文件系统,Web服务器) 图1-9一个存储器层次结构的示例 010-10-19