引言
本章中主要围绕一个简单的 C 语言程序展开
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
1.1 信息就是位+上下文
-
源程序(源文件)是一个由 0 和 1 组成的位序列,每 8 位称为 1 字节。
- 通常我们编辑源程序的最小单位就是字节,如 010editor、IDA 中的 patch
-
只由 ASCII 字符构成的文件称为文本文件,其他的所有文件都称为二进制文件。
1.2 程序被其他程序翻译成不同的格式
-
高级语言必须被其他程序转化为一系列的低级机器语言指令,然后这些指令按照一种称为可执行目标程序的格式打好包,以文件的形式存放。这个过程是编译器驱动程序完成的
-
高级语言到低级语言的翻译过程包括四个阶段,流程图如下
- 预处理阶段:预处理器(cpp)读取
#
开头的命令,将相应的文件插入程序文本中,得到另一个 C 程序hello.i
- 编译阶段:编译器(ccl)将
hello.i
翻译成hello.s
,它们都是文本文件。hello.s
中包含汇编语言程序 - 汇编阶段:汇编器(as)将
hello.s
翻译成机器语言指令,并将指令打包成可重定位目标程序,保存在hello.o
中 - 链接阶段:一些标准 C 库中的函数(如
printf
)存在于单独预编译好的.o
文件中,链接器(ld)将这些文件合并到目标文件中。
- 预处理阶段:预处理器(cpp)读取
1.3 了解编译系统如何工作是大有益处的
- 优化程序性能
while
和for
哪个效率更高switch
和if-else
语句哪个效率更高- …
- 理解链接时出现的错误
- 静态链接、动态链接
- 静态变量和全局变量的区别
- …
- 避免安全漏洞
1.4 处理器读并解释储存在内存中的命令
- 可执行文件在 shell 中运行,shell 是一个命令行解释器,它会判断输入的命令行的第一个单词是否为内置 shell 命令,若不是则假设其为可执行文件名字并加载运行该文件。
1.4.1 系统的硬件组成
-
总线:贯穿整个系统的一组电子管道,携带信息字节并负责在各个部件之间传递。总线传递定长的字节块,也就是字(word)。现在大多数机器字长为 4 或 8。
-
I/O 设备:
- 输入设备:鼠标、键盘
- 输出设备:显示器
- 存储设备:磁盘
每个 I/O 设备都通过一个控制器或适配器与 I/O 总线相连。
- 控制器:I/O 设备本身或系统的主印制电路板(主板)上的芯片组
- 适配器:一块插在主板插槽上的卡
- 主存:一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。
- 从物理上来说,主存是由一组动态随机存取存储器(DRAM)芯片组成的。
- 从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引)。组成程序的每条机器指令都由不同数量的字节构成。
- 处理器:中央处理单元(CPU),是解释(或执行)存储在主存中指令的引擎。
- 处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。PC 指向主存中的某条机器语言指令。
- 处理器一直在处理 PC 指向的指令,然后更新 PC 使其指向下一条指令。
- 处理器遵循指令集架构顺序执行指令。
- CPU 可能执行的操作
- 加载:从主存复制一个字节或一个字覆盖到寄存器
- 存储:从寄存器复制一个字节或一个字覆盖到主存的某个位置
- 操作:把两个寄存器的内容复制到 ALU(算术/逻辑单元),ALU 对这两个字做算术运算,并将结果存放到一个寄存器中
- 跳转:从指令本身中抽取一个字,并将这个字复制到 PC 中
- 指令集架构描述的是每条机器代码指令的效果,偏抽象;微体系结构描述的是处理器实际上是如何实现的,因为现代处理器使用了非常复杂的机制来加速程序的运行
1.4.2 运行 hello 程序
- 输入
./hello
,回车后 shell 程序解析用户命令,加载 hello 文件。 - 利用直接存储器读取(DMA)技术,hello 文件的代码和数据不通过处理器从磁盘被复制到主存。
- 处理器开始执行 hello 程序的 main 程序中的机器语言指令。这些指令将
hello, world\n
字符串的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备。
1.5 高速缓存至关重要
- 信息的移动会加大时间开销。
- 处理器从磁盘读一个字节的时间是从主存读取的 1000 万倍
- 处理器从寄存器文件读数据比从主存中读取快 100 倍
- 高速缓存存储器(cache memory,简称为 cache 或高速缓存)存放处理器近期可能会需要的信息
- L1 高速缓存访问速度和寄存器文件一样
- L2 高速缓存访问速度比寄存器文件慢 5 倍
- L1、L2 高速缓存使用静态随机访问存储器(SRAM)的硬件技术实现
1.6 存储设备形成层次结构
- 在处理器和一个较大较慢的设备(如主存)之间插入一个更小更快的存储设备(如高速缓存)的想法已经成为了一个普遍的观念,每个计算机系统中的存储设备都被组织成了一个存储器层次结构。(通俗一点理解,就是说上一层级的作为下一层级的高速缓存,保存下一层级的内容且提供更快的速度,通常下一层级每字节的价格更便宜,例如内存条和硬盘条)
1.7 操作系统管理硬件
- 所有应用程序对硬件的操作尝试都必须通过操作系统
- 操作系统的两个基本功能:
- 防止硬件被失控的应用程序滥用
- 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备
1.7.1 进程
-
进程是操作系统对一个正在运行的程序的一种抽象,是系统进行资源分配的基本单位。
-
进程能够并发运行,这是通过处理器在进程间切换实现的,操作系统实现这种交错执行的机制称为上下文切换。
- 上下文:操作系统保持跟踪进程运行所需的所有状态信息。包括 PC 和寄存器文件的当前值,以及主存的内容等。
- 执行上下文切换时,操作系统保存当前进程的上下文,然后将控制权从当前进程转移到新进程,新进程从它上次停止的地方开始
- 执行 hello 程序时发生的事件先后顺序如下:
- shell 进程等待输入
- shell 进程通过系统调用执行用户请求
- 操作系统保存 shell 进程上下文,创建 hello 进程及其上下文并执行
- hello 进程终止,shell 进程上下文恢复,继续等待命令输入
- 进程切换是由操作系统内核管理的,内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,就执行系统调用指令,将控制权传递给内核,然后内核执行相应操作。内核不是一个独立的进程,而是系统管理全部进程所用代码和数据结构的集合。
1.7.2 线程
- 一个进程可由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,共享同样的代码和全局数据。
1.7.3 虚拟内存
- 虚拟内存是一个抽象概念,为每个进程提供了一个独占使用主存的假象。每个进程看到的内存都是一致的,称为虚拟地址空间
- 虚拟内存的每个区
- 程序代码和数据
- 堆
- 共享库
- 栈
- 内核虚拟内存
1.7.4 文件
- 文件就是字节序列。每个 I/O 设备,包括磁盘、键盘、显示器,甚至网络,都可以看成文件。
1.8 系统之间利用网络通信
-
独立的系统可以通过网络和其他系统连接到一起。
-
网络可以视为一个 I/O 设备。
1.9 重要主题
1.9.1 Amdahl 定律
- 定义:当对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。
- 若系统执行某应用程序需要时间为 $T_{old}$,假设系统某部分所需执行时间与该时间的比例为$\alpha$,而该部分性能提升比例为$k$,即该部分初始所需时间为$\alpha T_{old}$,现在所需时间为$(\alpha T_{old})/k$,因此,总的执行时间应为: $$ T_{new} = (1-\alpha)T_{old} + (\alpha T_{old})/k = T_{old}[(1-\alpha)+\alpha/k] $$ 由此可以计算加速比$S=\frac 1 {(1-\alpha)+\alpha/k}$
- 由 Amdahl 公式可知,系统某部分改进后获得的系统加速比远小于该部分的加速比。因此,要想显著加速整个系统,必须提升全系统中相当大的部分的速度。
1.9.2 并发和并行
-
并发指一个同时具有多个活动的系统
-
并行指用并发来使一个系统运行得更快
- 线程级并发:传统的并发是模拟出来的,即处理器在正在执行的进程间快速切换实现。多核处理器和超线程的出现使得线程可以并行高效执行。
- 指令级并行:在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行(流水线、超标量、超长指令字)。
- 单指令、多数据并行:在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,即 SIMD 并行。
1.9.3 计算机系统中抽象的重要性
- 文件是对 I/O 设备的抽象
- 虚拟内存是对程序存储器的抽象
- 进程是对一个正在运行的程序的抽象
- 虚拟机提供对整个计算机的抽象,包括操作系统、处理器和程序