博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入理解计算机系统阅读笔记CSAPP(1)计算机系统漫游,程序的执行过程,
阅读量:336 次
发布时间:2019-03-04

本文共 5677 字,大约阅读时间需要 18 分钟。

计算机系统漫游

计算机系统是由硬件和系统软件组成的,它们共同工作来运行应用程序。虽然系统的 具体实现方式随着时间不断变化,但是系统内在的概念却没有改变。所有计算机系统都有 相似的硬件和软件组件,它们又执行着相似的功能。一些程序员希望深入了解这些组件是 如何工作的以及这些组件是如何影响程序的正确性和性能的,以此来提高自身的技能。本 书便是为这些读者而写的。

现在就要开始一次有趣的漫游历程了。如果你全力投身学习本书中的概念,完全理解底 层计算机系统以及它对应用程序的影响,那么你会步上成为为数不多的“大牛”的道路。

你将会学习一些实践技巧,比如如何避免由计算机表示数字的方式引起的奇怪的数字 错误。你将学会怎样通过一些小窍门来优化自己的C代码,以充分利用现代处理器和存储 器系统的设计。你将了解编译器是如何实现过程调用的,以及如何利用这些知识来避免缓 冲区溢岀错误带来的安全漏洞,这些弱点给网络和因特网软件带来了巨大的麻烦。你将学 会如何识别和避免链接时那些令人讨厌的错误,它们困扰着普通的程序员。你将学会如何 编写自己的Unix shell.自己的动态存储分配包,甚至于自己的Web服务器。你会认识并发 带来的希望和陷阱,这个主题随着单个芯片上集成了多个处理器核变得越来越重要。
在Kernighan和Ritchie的关于C编程语言的经典教材[61]中,他们通过图1-1中所 示的hello程序来向读者介绍C。尽管hello程序非常简单,但是为了让它实现运行,系 统的每个主要组成部分都需要协调工作。从某种意义上来说,本书的目的就是要帮助你了 解当你在系统上执行hello程序时,系统发生了什么以及为什么会这样。

1#include 
22int main()3{
4printf("hello, world\n");5return 0;}

我们通过跟踪hello程序的生命周期来开始对系统的学习一一从它被程序员创建开始, 到在系统上运行,输出简单的消息,然后终止。我们将沿着这个程序的生命周期,简要地介 绍一些逐步出现的关键概念、专业术语和组成部分。后面的章节将围绕这些内容展开。

1- 1信息就是位+上下文

hello程序的生命周期是从一个源程序(或者说源文件)开始的,即程序员通过编辑器创 建并保存的文本文件,文件名是hello.c0源程序实际上就是一个由值0和1组成的位(又称 为比特)序列,8个位被组织成一组,称为字节。每个字节表示程序中的某些文本字符。

大部分的现代计算机系统都使用ASCII标准来表示文本字符,这种方式实际上就是用 一个唯一的单字节大小的整数值㊀来表示每个字符。比如,图1-2中给出了 hello.c程序 的ASCII码表示。# i n c 1 u d e SP < s t d i o .

35 105 110 99 108 117 100 101 32 60 115 116 100 105 Ill 46
h > \n \n i n t SP m a i n ( ) \n {
104 62 10 10 105 110 116 32 109 97 105 110 40 41 10 123
\n SP SP SP SP P r i n t f ( II h e 1
10 32 32 32 32 112 114 105 110 116 102 40 34 104 101 108
1 0 9 SP w 0 r 1 d \ n H ) \n SP
108 111 44 32 119 111 114 108 100 92 110 34 41 59 10 32
SP SP SP r e t u r n SP 0 9 \n } \n
32 32 32 114 101 116 117 114 110 32 48 59 10 125 10
图1-2 hello.c的ASCII文本表示

hello.c程序是以字节序列的方式储存在文件中的。每个字节都有一个整数值,对应 于某些字符。例如,第一个字节的整数值是35,它对应的就是字符“# ”。第二个字节的 整数值为105,它对应的字符是‘尸,依此类推。注意,每个文本行都是以一个看不见的 换行符来结束的,它所对应的整数值为10。像hello.c这样只由ASCII字符构成 的文件称为文本文件,所有其他文件都称为二进制文件。

hello.c的表示方法说明了一个基本思想:系统中所有的信息——包括磁盘文件、内 存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串比特表示的。区 分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。比如,在不同的上下文 中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令。
作为程序员,我们需要了解数字的机器表示方式,因为它们与实际的整数和实数是不 同的。它们是对真值的有限近似值,有时候会有意想不到的行为表现。这方面的基本原理 将在第2章中详细描述。

C编程语言的起源

C语言是贝尔实验室的Dennis Ritchie于1969年〜1973年间创建的。美国国家标准学 会(American National Standards Institute, ANSI)在 1989 年颁布了 ANSI C 的标准,后来 C 语言的标准化成了国际标 准化组织(International Standards Organization, ISO)的责任。这 些标准定义了 C语言和一系列函数库,即所谓的C标准库。Kernighan和Ritchie在他们的 经典著作中描述了 ANSIC,这本著作被人们满怀感情地称为“K&R" [61]0用Ritchie的话 来说[92], C语言是“古怪的、有缺陷的,但同时也是一个巨大的成功为什么会成功呢?

• C语言与Unix操作系统关系密切。C从一开始就是作为一种用于Unix系统的程序 语言开发出来的。大部分Unix内核(操作系统的核心部分),以及所有支撑工具和 函数库都是用C语言编写的。20世纪70年代后期到80年代初期,Unix风行于高 等院校,许多人开始接触C语言并喜欢上它。因为Unix几乎全部是用C编写的, 它可以很方便地移植到新的机器上,这种特点为C和Unix赢得了更为广泛的支持。
•C语言小而简单。 C语言的设计是由一个人而非一个协会掌控的,因此这是一个 简洁明了、没有什么冗赘的设计。K&R这本书用大量的例子和练习描述了完整 的C语言及其标准库,而全书不过261页。C语言的简单使它相对而言易于学 习,也易于移植到不同的计算机上。
•C语言是为实践目的设计的。 C语言是设计用来实现Unix操作系统的。后来, 其他人发现能够用这门语言无障碍地编写他们想要的程序。
C语言是系统级编程的首选,同时它也非常适用于应用级程序的编写。然而,它也 并非适用于所有的程序员和所有的情况。C语言的指针是造成程序员困惑和程序错误的 一个常见原因。同时,C语言还缺乏对非常有用的抽象的显式支持,例如类、对象和异 常。像C++和Java这样针对应用级程序的新程序语言解决了这些问题。

1.2程序被其他程序翻译成不同的格式

hello程序的生命周期是从一个高级C语言程序开始的,因为这种形式能够被人读 懂。然而,为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系 列的低级机器语言指令。然后这些指令按照一种称为可执行目标程序的格式打好包,并以 二进制磁盘文件的形式存放起来。目标程序也称为可执行目标文件。

在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的:

linux> gcc -o hello hello.c

在这里,GCC编译器驱动程序读取源程序文件hello, c,并把它翻译成一个可执行 目标文件helloo这个翻译过程可分为四个阶段完成,如图1-3所示。执行这四个阶段的 程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统(compilation system)

•预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如 hello.c中第1行的#include < stdio. h>命令告诉预处理器读取系统头文件 stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常 是以.i作为文件扩展名。

•编译阶段。编译器(cel)将文本文件hello.i翻译成文本文件hello.s,它包含一 个汇编语言程序。该程序包含函数main的定义,
1 main:
2 subq $8, %rsp
3 movl $.LCO, %edi
4 call puts
5 movl $0, %eax
6 addq $8, %rsp
7 ret
定义中2〜7行的每条语句都以一种文本格式描述了一条低级机器语言指令。 汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语
言。例如,C编译器和Fortran编译器产生的输岀文件用的都是一样的汇编语言。
•汇编阶段。接下来,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成 一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标 文件hello.。中。hello, o文件是一个二进制文件,它包含的17个字节是函数main 的指令编码。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码。
•链接阶段。请注意,hello程序调用了 printf函数,它是每个C编译器都提供的 标准C库中的一个函数。printf函数存在于一个名为printf .o的单独的预编译 好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链 接器(Id)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件 (或者简称为可执行文件),可以被加载到内存中,由系统执行。
GNU项目
GCC是GNU(GNU是GNU’s Not Unix的缩写)项目开发出来的众多有用工具之 —o GNU项目是1984年由Richard Stallman发起的一个免税的慈善项目。该项目的目 标非常宏大,就是开发出一个完整的类Unix的系统,其源代码能够不受限制地被修改 和传播。GNU项目已经开发出了一个包含Unix操作系统的所有主要部件的环境,但内 核除外,内核是由Linux项目独立发展而来的。GNU环境包括EMACS编辑器、GCC 编译器、GDB调试器、汇编器、链接器、处理二进制文件的工具以及其他一些部件。 GCC编译器已经发展到支持许多不同的语言,能够为许多不同的机器生成代码。支持 的语言包括 C、C++、Fortran、Java、Pascal、面向对象 C 语言(Objective-C)和 Ada。
GNU项目取得了非凡的成绩,但是却常常被忽略。现代开放源码运动(通常和 Linux联系在一起)的思想起源是GNU项目中自由软件(free software)的概念。(此处的free 为自由言论(free speech)中的“自由"之意,而非免费啤酒(free beer)中的“免费"之意。)而 且,Linux如此受欢迎在很大程度上还要归功于GNU工具,它们给Linux内核提供了环境。

1. 3 了解编译系统如何工作是大有益处的

对于像hello.c这样简单的程序,我们可以依靠编译系统生成正确有效的机器代码。 但是,有一些重要的原因促使程序员必须知道编译系统是如何工作的。

•优化程序性能。现代编译器都是成熟的工具,通常可以生成很好的代码。作为程序 员,我们无须为了写出高效代码而去了解编译器的内部工作。但是,为了在C程序中 做出好的编码选择,我们确实需要了解一些机器代码以及编译器将不同的C语句转化 为机器代码的方式。比如,一个switch语句是否总是比一系列的if-else语句高效 得多? 一个函数调用的开销有多大? whi况循环比for循环更有效吗?指针引用比数 组索引更有效吗?为什么将循环求和的结果放到一个本地变量中,会比将其放到一个 通过引用传递过来的参数中,运行起来快很多呢?为什么我们只是简单地重新排列一 下算术表达式中的括号就能让函数运行得更快?
在第3章中,我们将介绍x86-64,最近几代Linux、Macintosh和Windows计算机的 机器语言。我们会讲述编译器是怎样把不同的C语言结构翻译成这种机器语言的。在第 5章中,你将学习如何通过简单转换C语言代码,帮助编译器更好地完成工作,从而调 整C程序的性能“在第6章中,你将学习存储器系统的层次结构特性,C语言编译器如 何将数组存放在内存中,以及C程序又是如何能够利用这些知识从而更高效地运行。
•理解链接时出现的错误。根据我们的经验,一些最令人困扰的程序错误往往都与链 接器操作有关,尤其是当你试图构建大型的软件系统时。比如,链接器报告说它无 法解析一个引用,这是什么意思?静态变量和全局变量的区别是什么?如果你在不 同的C文件中定义了名字相同的两个全局变量会发生什么?静态库和动态库的区别 是什么?我们在命令行上排列库的顺序有什么影响?最严重的是,为什么有些链接 错误直到运行时才会岀现?在第7章中,你将得到这些问题的答案。
•避免安全漏洞。多年来,缓冲区溢出错误是造成大多数网络和Internet服务器上安 全漏洞的主要原因。存在这些错误是因为很少有程序员能够理解需要限制从不受信 任的源接收数据的数量和格式。学习安全编程的第一步就是理解数据和控制信息存 储在程序栈上的方式会引起的后果。作为学习汇编语言的一部分,我们将在第3章 中描述堆栈原理和缓冲区溢岀错误。我们还将学习程序员、编译器和操作系统可以 用来降低攻击威胁的方法。

转载地址:http://adxh.baihongyu.com/

你可能感兴趣的文章