深入理解计算机系统学习笔记(一)

 

1.1 对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:

1).预处理,产生.ii文件

预处理主要包含下面的内容:

a.对所有的“#define”进行宏展开;

b.处理所有的条件编译指令,比如“#if”,“#ifdef”,“#elif”,“#else”,“#endif”

c.处理“#include”指令,这个过程是递归的,也就是说被包含的文件可能还包含其他文件

d.删除所有的注释“//”和“/**/”

e.添加行号和文件标识

f.保留所有的“#pragma”编译器指令

    经过预处理后的.ii文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.ii文件中。

2).编译,产生汇编文件(.s文件)

编译的过程就是将预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件(.s文件)

3).汇编,产生目标文件(.o或.obj文件)

汇编器是将汇编代码转变成机器可以执行的代码,每一个汇编语句几乎都对应一条机器指令。最终产生目标文件(.o或.obj文件)。

4).链接,产生可执行文件(.out或.exe文件)

链接的过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)

 

 

深入理解计算机系统学习笔记(一)

1.2 为什么需要做的编译系统是如何工作的

1.优化程序性能。为了在C程序中做出更好的编码选择,我们确实需要了解一些机器代码以及编译将不同C语句转化成机器代码的方式。比如,一个switch语句是否总是比一系列的if-else语句高效得多?一个函数调用的开销有多大?while循环总是比for循环更有效吗?指针引用比数组索引更有效吗?为什么将循环求和的结果放到一个本地变量,会比将其放到一个通过引用传递过来的参数中,运行起来快很多?为什么我们只是简单重新排列一下算术表达式的括号就能让程序运行更快?

2.理解链接时出现的错误。根据我们的经验,一些令人困扰的程序错误往往都与连接器操作有关,尤其是当你试图构建大型的软件系统时。比如,连接器报告说它无法解析一个引用,这是什么意思?静态变量和全局变量的区别是什么?如果你在不同的C文件中定义了相同名字的两个全局变量会发生什么?静态库和动态库的区别是什么?我们在命令行上排列库的顺序有什么影响?最严重的是有些链接错误直到运行时才出现?(第7章)

3.避免安全漏洞。多年来,缓冲区溢出错误是造成大多数网络和Internet服务器上安全漏洞的主要原因。存在这些错误是因为很少有程序员能够理解需要限制从不受信任的源接收数据的数量和格式。学习安全编程的第一步就是理解数据和控制信息存储在程序上的方式会引起的后果。(第3章)

深入理解计算机系统学习笔记(一)

 

1.3 系统的硬件组成

1. 总线

贯穿整个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部门间传递。通常总线被设计成传送定长的字节块,也就是字。字中的字节数(即字长)是一个基本的系统参数,各个系统中都不尽相同。大多数机器字长要么是4个字节(32位),要么是8个字节(64位)。

2 I/O设备

I/O(输入/输出)设备是系统与外部世界的联系通道。每一个I/O设备都通过一个控制器或适配器与I/O总线相连,控制器和适配器之间的区别主要在于它们的封装方式。控制器是I/O设备本身或者系统的主印制电路板(通常称为主板)上的芯片组。而适配器则是一块插在主板插槽上的卡,无论如何,它们的功能都是在I/O总线和I/O设备之间传递信息。

3. 主存

主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据,从物理上来说,主存是由一组动态随机存储器(DRAM)芯片组成的。从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),这些地址是从零开始的。一般来说,组成程序的每条机器指令都由不同数量的字节构成。与C程序变量相对应的数据项的大小是根据类型变化的。

4. 处理器

中央处理单元(CPU),简称处理器,是解释(或执行)存储在主存中指令的引擎。处理的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC都指向主存中的某条机器语言指令(即含有该条指令的地址)。寄存器是一个小的存储设备,由一些单个字长的寄存器组成,每个寄存器都有唯一的名字。ALU(逻辑单元)计算新的数据和地址值。CPU在指令的要求下可能会执行这些操作。

加载:从主存复制一个字节或者一个字到寄存器,以覆盖寄存器原来的内容。

存储:从寄存器复制一个字节或者一个字到主存的某个位置,以覆盖这个位置上原来的内容。

操作:把两个寄存器的内容复制到ALU,ALU对这两个字做算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容。

跳转:从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中原来的值。

我们将处理器的指令集架构和处理器的微体系结构区分开来:指令集架构描述的是每条机器代码指令的效果;而微体系结构描述的是处理器实际上是如何实现的(第3、4、5章有介绍)。

深入理解计算机系统学习笔记(一)

 

根据机械原理,较大的存储设备要比较小的存储设备运行得慢,而快速设备的造价远比高于同类的低速设备。

针对处理器与主存之间的差异,系统设计在者采用了更小更快的存储设备,称为高速缓存存储器(cache memory,简称cache或者高速缓存),作为暂时的集结区域,存放处理器近期可能会需要的信息。

1.4 存储设备形成层次结构

深入理解计算机系统学习笔记(一)

 

1.5 操作系统管理硬件

操作系统有两个基本功能:

(1)防止硬件被失控的应用程序滥用;

(2)向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。

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

深入理解计算机系统学习笔记(一)

文件是对I/O设备的抽象表示,虚拟内存是对主存和磁盘I/O设备的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示。

进程

进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另外一个进程的指令是交错执行的。大多数系统中,需要运行的进程数是多于可以运行它们的CPU数的。

从一个进程到另外一个进程的转换是由操作系统内核(kernel)管理的。内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用(system call)指令,将控制权传递给内核。然后内核执行被请求的操作并返回应用程序。注意,内核不是一个独立的进程,相反,它是系统管理全部进程所用代码和数据结构的集合。

 

深入理解计算机系统学习笔记(一)

线程

在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。

虚拟内存

虚拟内存是一个抽象概念,它为每个过程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。下图是Linux进程的虚拟地址空间。在Linux中,地址空间最上面的区域是保留给操作系统中的代码和数据,这对所有进程来说都是一样,地址空间的底部区域存放用户进程定义的代码和数据。注意,图中的地址是从下往上增大的。

 

深入理解计算机系统学习笔记(一)

程序代码和数据:对所有的进程来说,代码是从同一个固定地址开始,紧接着的是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的,在实例中就是可执行文件hello。(第7章)

堆:代码和数据区后紧接着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用malloc和free这样的C标准库函数时,堆可以在运行时动态地扩展和收缩。(第9章)

共享库:大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域。共享库的概念非常强大(第7章)。

栈:位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用,和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。特别的,每次我们调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩(第3章)。

内核虚拟内存:地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。虚拟内存运作的基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。(第9章)

文件

文件就是字节序列,仅此而已。它向应用程序提供了一个统一的视图,来看待系统中可能含有的所有各式各样的I/O设备。