Jialong's Blog
沉潜 自由 追寻幸福
csapp: 1.计算机系统漫游
书籍《深入理解计算机系统》阅读学习笔记

在本书中我们将会学习到一些使用技巧,比如如何避免由于计算机表示数字的方式引起的错误;通过一些技巧优化C代码,以充分利用现代处理器和存储器系统的设计;了解编译器如何实现过程调用,利用这些知识避免缓冲区溢出错误带来的安全漏洞;学习如何识别和避免链接时的错误;如何编写Unix Shell、动态存储分配包、Web服务器;并发带来的希望和陷阱,对于现在的单芯片集成多个处理器核心非常重要。

我们通过围绕Hello程序执行时的整个生命周期来对系统进行学习:

#include <stdio.h>
int main()
{
	printf("Hello, world\n");
	return 0;
}

信息就是位+上下文

8位为一个字节。

现代计算机系统使用ASCII标准来表示文本字符。即用一个单字节大小的整数值表示一个符号。

数字的机器表示是对真值的有限近似。

程序被翻译为不同的格式

hello.c中的每条C语句都会转换为一系列低级机器语言指令。这些指令按照可执行目标程序格式打包,以二进制磁盘文件的形式存放。

编译系统:

hello.c(源程序,文本) -> 预处理器(cpp) -> hello.i(修改了的源程序,文本) -> 编译器(ccl) -> hello.s(汇编程序,文本) -> 汇编器 -> hello.o(可重定位目标程序,二进制) -> 链接器 -> hello(可执行目标程序,二进制)

  • 预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始C程序。例如#include <stdio.h>告诉预处理器读取系统头文件的内容,将其插入程序文本中,得到另一个C程序,以.i为扩展名。
  • 编译阶段:编译器(ccl将hello.i翻译为hello.s,其包含了汇编语言程序
  • 汇编阶段:汇编器(as)将hello.s翻译为机器语言指令,将这些指令打包为可重定位目标程序,将结果保存在hello.o中,该文件为二进制文件。
  • 链接阶段:hello程序调用了printf函数,它是C编译器提供的标准C库中的一个函数,printf函数存在于一个名为printf.o的单独预编译好的目标文件中,这个文件要以某种形式合并到我们的hello.o程序中,链接器(ld)负责这种合并,合并后就得到了可执行目标文件hello,其可以被加载到内存中,由系统执行。

编译器的工作方式

有一些重要原因使得我们必须知道编译器的工作方式:

  • 优化程序性能。我们需要了解一些机器代码及编译器将不同C语句转化为机器代码的方式,例如一个switch语句是否总比一系列if-else语句高效。
  • 理解链接时出现的错误。
  • 避免安全漏洞。缓冲区溢出错误是大多网络和服务器安全漏洞的主要原因。

处理器读取并解释储存在内存中的指令

要在Unix系统运行可执行目标文件hello,需要将文件名输入shell应用程序中:

linux> ./hello
hello,world
linux>

系统的硬件组成

总线

贯穿整个系统,负责信息字节在各个部件之间传递。

通常被设计为传送定长的字节块,即,字中的字节数(即字长)是基本的系统参数,如今大多数机器中字长为8字节(即64位),4字节(32位)在逐渐被64位取代。

I/O设备

每个I/O设备通过控制器或适配器与I/O总线相连,控制器为I/O设备本身或系统主板上的芯片组,适配器为插在主板插槽上的卡。

主存RAM

临时存储设备,在处理器执行程序时用来存放程序和程序处理的数据。由一组动态随机存取存储器(DRAM)芯片组成。逻辑上,存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),这些地址从零开始。

处理器

CPU是解释或执行存储在主存中指令的引擎,处理器的核心是一个大小为一个字的存储设备(寄存器),称为程序计数器(PC),PC指向主存中的某条机器语言指令(即包含该指令的地址)。

处理器在一直不停地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器看上去是按照一个简单的指令执行模型来操作的,这个模型是指令集架构决定的。该模型中指令按照严格的顺序执行。执行一条指令包含一系列步骤,处理器从程序计数器指向的内存中读取指令,解释指令中的位,执行该指令指示的简单操作,然后更新PC,使其指向下一条指令,循环操作。

这样的简单操作并不多,主要围绕**主存、寄存器文件和ALU(算术逻辑单元)**进行。寄存器文件是一个小型存储设备,由一些单个字长的寄存器组成,ALU计算新的数据和地址值。CPU可能进行以下操作:

  • 加载:从主存读取一个字到寄存器
  • 存储:从寄存器赋值一个字到主存
  • 操作:将两个寄存器的内容复制到ALU,ALU对两个字进行算术运算,将结果存放到一个寄存器中
  • 跳转:从指令本身抽取一个字,将这个字复制到PC中

指令集架构:每条机器代码指令的效果

微体系结构:处理器实际上是如何实现的

运行hello程序

在键盘上输入.\hello后,shell会将这些字符逐一读入寄存器,再将其存放到内存中。按下回车后,shell知道命令输入已经结束,将执行一系列指令加载hello文件,这些指令会将hello目标文件中的代码和数据从磁盘复制到主存,利用DMA(直接存储器存取)技术,可将数据不经过CPU直接从磁盘放入主存。

目标文件hello被放入主存后,处理器就开始执行机器语言指令。

高速缓存

为了减少信息加载的时间,采用更小更快的存储设备作为数据或指令暂时的集结区域。处理器芯片上的L1高速缓存缓存的访问速度集合和访问寄存器文件一样快,容量更大的L2高速缓存通过特殊总线连接到寄处理器。高速缓存采用静态随机访问存储器(SRAM)技术实现。

存储器层次:

L0:寄存器

L1:L1高速缓存(SRAM)

L2:L2高速缓存(SRAM)

L3:L3高速缓存(SRAM)

L4:主存(DRAM)

L5:本地磁盘

L6:远程存储

高速缓存举例

如上图为我的电脑的三级缓存示意。

操作系统管理硬件

前面的shell程序和hello程序都依靠操作系统提供的服务访问存储器。操作系统是应用程序和硬件之间的软件。

操作系统的主要功能有:

  • 防止硬件被失控的应用程序滥用
  • 向应用程序提供简单的机制控制复杂又不同的低级硬件设备。

操作系统通过几个基本抽象概念:进程、虚拟内存和文件实现这些功能。

文件是对I/O设备的抽象。

虚拟内存是对主存和磁盘I/O设备的抽象。

进程是对处理器、主存和磁盘I/O设备的抽象。

进程

进程是操作系统对正在运行程序的一种抽象,一个系统可以同时运行多个进行,每个进程好像都在独占使用硬件。并发运行指的是多个不同程序进程指令交错运行。处理器通过在进程间切换来实现多个程序的并发运行,这种机制称为上下文切换

操作系统保存跟踪进程运行所需要的所有状态信息,这种状态即为上下文。这些信息有很多,比如PC和寄存器文件的当前值。

任何一个时刻,单处理器只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到新进程时,就会进行上下文切换。

从一个进程到另一个进程的切换是操作系统内核(kernal)管理的。内核是操作系统代码常驻主存的部分。内核不是一个独立的进程,是系统管理全部进程所用的代码和数据结构的集合。

线程

一个进程可以有多个成为线程的执行单元组成,每个线程都运行在进程的上下文中,共享同样的代码和全部数据。多线程相比多进程更容易共享数据。

虚拟内存

虚拟内存为每个进程提供一个假象,即每个进程都在独占地使用主存。每个进程看到内存都是一致的,成为虚拟地址空间。

每个进程看到的虚拟地址由大量准确定义的构成,每个区都有专门的功能。以下从最低的地址逐步向上介绍:

  • 程序代码和数据:所有进程的代码是从同一固定地址开始,紧接着是和C全局变量对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的。
  • 堆:当调用malloc和free这样的函数时,堆可以动态地扩展和收缩。
  • 共享库:是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域。
  • 栈:编译器用它来实现函数调用,栈在程序执行期间可以动态扩展和收缩。
  • 内核虚拟内存:地址空间顶部区域是为内核保留的,不允许应用程序读写这个区域的内容或直接调用内核代码定义的函数。

文件

文件就是字节序列,系统所有输入输出都是通过使用一小组成为Unix I/O的系统函数调用读写文件实现的。

系统之间利用网络通信

网络是一个I/O设备,通过网络适配器连接到系统总线。

一些重要主题

并发和并行

并发:一个同时具有多个活动的系统

并行:用并发使得一个系统运行得更快

并行可以在计算机系统的多个抽象层次上运行。

线程级并发

指令级并发

单指令、多数据并行

计算机系统中抽象的重要性

文件是对I/O设备的抽象,虚拟内存是对程序存储器的抽象,进程是对正在运行程序的抽象,虚拟机提供对整个计算机的抽象。


最后修改于 2021-06-11