目录
0x00 – 第一章:计算机系统漫游
计算机系统是由硬件和系统软件组成的,他们共同工作来运行应用程序。
0x01 – 信息就是位 + 上下文
程序的生命周期从一个源程序(源文件)开始。(即程序员通过编辑器创造并保存的文件)
源程序实际上就是由 0 和 1 组成的位(又称为比特)序列,8个位被组织成一个组,称为字节。
一个程序以字节序列的方式存储在文件中,每个字节都有一个整数值对应某个字符(ASCII码表对应)。
每个文本行都是以一个看不见的换行符“\n”来结束的,它在ASCII码表中对应的整数值位10
只由ASCII字符构成的文件称为文本文件;
其他所有文件都称为二进制文件。
一个重要的基本思想:
系统中所有的信息——包括磁盘文件、内存中的程序、内存中用户数据以及网络上传送的数据,都是由一串比特表示的。
区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。
C语言有很多优点,但是同时它缺乏对非常有用的抽象的显式支持。
0x02 – 程序被其他程序翻译成不同的格式
目标程序也叫可执行目标文件。
# 翻译过程大概分为四个阶段完成
hello.c # 源程序(文本)
【预处理器(cpp)】 # 第一阶段:预处理阶段:根据文件里面带#号的部分修改文件,比如 #include <stdio.h> 它会把原来写好的这部分代码直接添加进 hello.c 文件,并改名为 hello.i
hello.i # 修改了的源文件(文本)
【编译器(ccl)】 # 第二阶段:编译阶段:将 hello.i 这个 C 语言程序变成 汇编语言的程序并改名为 hello.s
hello.s # 汇编程序(文本)
【汇编起(as)】 # 第三阶段:汇编阶段:将汇编语言程序变成机器可以执行的二进制文件 hello.o
hello.o # 可重定位目标程序(二进制)
printf.o # 额外插入一个文件:C库提供的printf函数已经预先编译好的二进制文件
【链接器(ld)】 # 第四阶段:链接阶段:将 hello.o 和 其他(不一定只有printf.o这一个)只已经预编译好的二进制文件进行链接
hello # 可执行的目标程序(二进制)
0x03 – 了解编译系统如何工作大有益处
- 优化程序性能:现在的编译工具很成熟,可以很好的编译代码,我们无需了解其内部工作,但是为了写出更好的代码,我们需要了解一些机器代码以及编译器将不同的C语言转化为机器代码的方式。后续章节会学到很多细节。
- 理解链接时出现的错误:根据经验,一些令人困扰的程序错误往往都与链接器操作有关,尤其是当你试图构建大型软件系统时。
- 避免安全漏洞:多年来,缓冲区溢出是造成网络和服务器上的安全漏洞的主要原因。存在这些错误是因为很少有程序猿能理解从不受信任的源接受数据的数量和格式。学习安全编程的第一步就是理解数据和控制信息存储在程序栈上的方式会引起的后果。
0x04 – 处理器读并解释存储在内存中的指令
0x04 – 1 – 系统硬件的组成
1 – 总线
贯穿整个系统的一组电子管道,称作总线。总线携带信息字节并负责在各个部分间传递。
通常总线被设计成传送定长的字节快,也就是字(word)。字中的字节数(即字长)是一个基本的系统参数。各个系统不尽相同
目前大多数4字节(32位),8字节(64位)
2 – I/O设备
I/O(输入/输出)设备是系统与外部世界的联系通道。
每个I/O设备通过一个控制器或适配器与总线相连。控制器与适配器区别是封装方式
控制器(Controller):集成在CPU主板上并可以将CPU发来的逻辑指令通过特定协议转换为设备可以识别的控制信号;
适配器(Adapter):独立的外部设备,可以实现和控制器一样的功能,如网卡等;
3 – 主存(内存)
主存是一个临时的存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。
从物理上来说:主存是由一组动态随机存取存储器(DRAM)芯片组成的。
从逻辑上来说:存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),这些地址是从零开始的。
一般来说,组成程序的每条机器指令都由不同数量的字节构成。
4 – 处理器
中央处理单元(CPU),简称处理器。是解释或执行存储在主存中指令的引擎。
处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。
从系统通电开始直到断电,处理器一直在不断的执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。
寄存器文件(register file)是一个小的存储设备,由一些单个字长的寄存器组成,每个寄存器都有唯一的名字。
算术/逻辑单元(ALU)。
处理器看上去是它的指令集架构的简单实现,实际上现在的处理器使用了非常复杂的机制来加速程序的执行,因此我们将处理器的指令集架构和处理器的微体系结构区分开来。
指令集架构:描述的是每条机器代码指令的效果;
微体系结构:描述的是处理器实际上是如何实现的;
5 – 程序运行
略
0x05 – 高速缓存至关重要
程序开始时再磁盘上,被复制到主存上,然后复制到显示设备上,这些复制从程序猿的角度看就是开销,减慢程序工作的“真正”原因。
根据机械原理:较大的存储设备要比较小的存储设备运行的慢,而快速设备的造价远高于同类的低速设备。
处理器从寄存器文件中读取数据比从主存中读取几乎要快100倍。这些年随着半导体技术的进步,这种处理器与主存之间的差距还在持续增大。
根据这种差异,系统设计者采用更小更快的存储设备,称为高速缓存存储器(cache memory,简称cache或高速缓存),作为暂存的集结区域,存放处理器近期可能会需要的信息。
-
L1高速缓存:位于处理器芯片上,存储容量达到数万字节,访问速度几乎和寄存器文件一样快。
-
L2告诉缓存:容量为数十万到数百万字节,通过一条特殊的总线连接到处理器。
进程访问L2的时间要比访问L1长5倍,这仍比直接访问主存快5-10倍。
L1和L2高速缓存是用一种叫做**静态随机访问存储器(SRAM)**的硬件技术实现的。
系统能够获得一个很大的存储器,同时访问速度也很快,原因是利用了高速缓存的局部性原理,即程序具有访问局部区域里数据和代码的趋势。通过高速缓存存放可能经常访问的数据,大部分的内存操作都能在快速的高速缓存中完成。
意识到高速缓存存储器存在的应用程序猿能够利用高速缓存将程序的性能提高一个数量级。
0x06 – 存储设备形成层次结构
每个计算机系统中的存储设备都被组织成了一个存储器层次结构。
在这个层次结构中,从上至下,设备的访问速度越来越慢、容量越来越大,并且每字节的造价也越来越便宜。
存储器层次结构的主要思想是上一层的存储器作为第一层存储器的高速缓存。
正如可以运用不同的高速缓存的知识来提高程序性能一样,程序猿童谣可以利用对整个存储器层次结构的理解来提高程序性能。
0x07 – 操作系统管理硬件
shell 和 hello 程序都没有直接访问键盘、显示器、磁盘或者主存。取而代之的是,他们依靠操作系统提供的服务。
操作系统有两个基本功能:
- 防止硬件被时空的应用程序滥用;
- 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的第几硬件设备。
操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。
0x07 – 1 – 进程
进程是计算机科学中最重要和最成功的概念之一。
**进程是操作系统对一个正在运行的程序的一种抽象。**在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。
并发运行:一个进程的指令和另一个进程的指令是交错执行的。
在大多数系统中,需要运行的进程数是多余可以运行他们的CPU个数的。
无论是单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程之间切换来实现的。
操作系统的这种交错执行的机制称为上下文切换。
操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是【上下文
】,包括许多信息,比如PC和寄存器文件的当前值,以及主存的内容。
当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从它上次停止的地方开始。
从一个进程到另一个进程的转换是由操作系统内核(kernel)管理的。
内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用(system call)指令,将控制权传递给内核。然后内核执行被请求的操作并返回应用程序。注意,内核不是一个独立的进程。相反,它是系统管理全部进程所有代码和数据结构的集合。
0x07 – 2 – 线程
在现代系统中,一个进程实际上可以有多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享相同的代码和全局数据。
由于网络服务器中对并行处理对需求,线程成为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程更高效。
当有多处理器可用的时候,多线程也是一种使得程序可以运行的更快的方法。
0x07 – 3 – 虚拟内存
虚拟内存是一个抽象概念,他为每个进程提供一个假象,即每个进程都在独占的使用主存。每个进程看到的内存都是一致的,成为虚拟地址空间。
每个进程看到的虚拟地址空间由大量准确定义的区构成,每个区都有专门的功能。
- 程序代码和数据:对所有进程来说,代码是从同一个固定地址开始的,紧接着是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的。(进程已开始的时候就被指定了大小。)
- 堆:代码和数据后紧接着是运行时堆。(当调用像malloc和free这样的C标准库函数时,堆可以在运行时动态的扩展和收缩。)
- 共享库:这个概念非常强大也非常难懂。
- 栈:位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。(和堆一样,用户栈在程序执行期间可以动态的扩展和收缩)
- 内核虚拟内存:地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。相反,他们必须调用内核来执行这些操作。
0x07 – 4 – 文件
文件就是字节序列,仅此而已。
每个 I/O 设备,包括磁盘、键盘、显示器,甚至网络,都可以堪称是文件。
系统中的所有输入输出都是通过使用一小组成为 Unix I/O的系统函数调用读写文件来实现的。
0x08 – 系统之间利用网络通信
我们一直把系统视为一个鼓励的硬件和软件的集合。实际上,现代系统经常通过网络和其他系统连接到一起。从一个单独的系统来看,网络可视为一个 I/O 设备。
当系统从主存复制一串字节到昂立适配器时,数据流经过网络达到另一台机器,而不是比如说到达本地磁盘驱动器。相似的,系统可以读取从其他机器发送来的数据,并把数据复制到自己的主存。
0x09 – 重要主题
系统不仅仅只是硬件。系统时硬件和系统软件互相交织的集合体,他们必须共同协作以达到运行应用程序的最终目的。
0x09 – 1 – Amdahl 定律
简单来讲,Amdahl(阿姆达尔)定律的主要观点——要想显著加速整个系统,必须提升全系统中相当大的部分的速度。
2021年10月10日