CSAPP 学习笔记
本文参考:https://hansimov.gitbook.io/csapp
本文默认你已经掌握了基本的linux系统操作,所以有关怎么执行编译好的可执行程序的过程,就不再赘述了。
仅供个人学习查看。
1-1 计算机系统漫游-编译过程-Helloworld程序的生成过程
#include <stdio.h>
int main()
{
printf("HelloWorld");
return 0;
}
在上面的代码中,我们使用c语言完成了一个打印HelloWorld的程序,接着我们执行
gcc -o hello hello.c
以下代码在WSL环境下执行
第一阶段:预处理
会读取头文件,例如在上面的HelloWorld
程序中,第一行是#include <stdio.h>
,预处理器就会读取该头文件中的内容,将其中的内容直接插入到源程序中,结果就得到了另外一个C程序。
这个经过预处理器处理过后得到的文件通常以.i
结尾,这个hello.i
依然是一个文本文件
第二阶段: 编译阶段
在这个阶段,编译器把hello.i
文件翻译成hello.s
文件,这个过程,我们称之为编译。
编译这个阶段包括 词法分析,语法分析,语义分析,中间代码生成以及优化等一系列的中间操作
第三阶段: 汇编阶段
汇编器会根据指令集把汇编程序 hello.s
翻译成机器指令,把这一系列的机器指令按照固定的规则进行打包吗,得到可重定位目标文件,hello.o
第四阶段:链接
当读取到我们的printf
函数的时候,计算机就知道你要在屏幕上打印内容,接着会生成一个已经编译好的printf.o
目标文件,链接器负责把hello.o
和printf.o
进行合并,合并需要遵循一定的规则
学习编译过程的原因:
1-2 计算机系统漫游-计算机硬件系统组成
https://hansimov.gitbook.io/csapp/ch01-a-tour-of-computer-systems/1.4
1.总线
贯穿整个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是字(word)。字中的字节数(即字长)是一个基本的系统参数,各个系统中都不尽相同。现在的大多数机器字长要么是 4 个字节(32 位),要么是 8 个字节(64 位)。本书中,我们不对字长做任何固定的假设。相反,我们将在需要明确定义的上下文中具体说明一个“字”是多大。
2. I/O 设备
I/O(输入/输出)设备是系统与外部世界的联系通道。我们的示例系统包括四个 I/O 设备∶作为用户输入的键盘和鼠标,作为用户输出的显示器,以及用于长期存储数据和程序的磁盘驱动器(简单地说就是磁盘)。最开始,可执行程序 hello 就存放在磁盘上。
每个 I/O 设备都通过一个控制器或适配器与 I/O 总线相连。控制器和适配器之间的区别主要在于它们的封装方式。控制器是 I/O 设备本身或者系统的主印制电路板(通常称作主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。无论如何,它们的功能都是在 I/O 总线和 I/O 设备之间传递信息。
CPU:中央处理单元;
ALU:算术/逻辑单元;
PC:程序计数器;
USB:通用串行总线
3.主存
主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理上来说,主存是由一组动态随机存取存储器(DRAM)芯片组成的。从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),这些地址是从零开始的。一般来说,组成程序的每条机器指令都由不同数量的字节构成。与 C 程序变量相对应的数据项的大小是根据类型变化的。比如,在运行 Linux 的 x86-64 机器上,short 类型的数据需要 2 个字节,int 和 float 类型需要 4 个字节,而 long 和 double 类型需要 8 个字节。 第 6 章将具体介绍存储器技术,比如 DRAM 芯片是如何工作的,它们又是如何组合起来构成主存的。
4.处理器
中央处理单元(CPU),简称处理器,是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)
注意:这里的PC非彼PC
平时人们说的PC是个人计算机(Personal Computer)的缩写,是指一种专门为个人使用而设计的计算机。与服务器、工作站等大型计算机相比,个人计算机通常具有较小的尺寸、较低的成本和较低的性能,但足以满足个人或小型办公室的计算需求。
从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器看上去是按照一个非常简单的指令执行模型来操作的,这个模型是由指令集架构决定的。在这个模型中,指令按照严格的顺序执行,而执行一条指令包含执行一系列的步骤。处理器从程序计数器指向的内存处读取指令,解释指令中的位,执行该指令指示的简单操作,然后更新 PC,使其指向下一条指令,而这条指令并不一定和在内存中刚刚执行的指令相邻。
这样的简单操作并不多,它们围绕着主存、寄存器文件(register file)和算术/逻辑单元(ALU)进行。寄存器文件是一个小的存储设备,由一些单个字长的寄存器组成,每个寄存器都有唯一的名字。ALU 计算新的数据和地址值。下面是一些简单操作的例子,CPU 在指令的要求下可能会执行这些操作。
**加载:**从主存复制一个字节或者一个字到寄存器,以覆盖寄存器原来的内容。
**存储:**从寄存器复制一个字节或者一个字到主存的某个位置,以覆盖这个位置上原 来的内容。
**操作:**把两个寄存器的内容复制到 ALU,ALU 对这两个字做算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容。
**跳转:**从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖 PC 中原来的值。
输出helloworld
寄存器,CPU缓存,内存,,磁盘
从寄存器中读取最快,然后是缓存,接着是内存,最后是磁盘
速度:L1 > L2 > L3 缓存
1-3 计算机系统漫游-操作系统管理硬件
我们可以把操作系统看成是应用程序和硬件之间插入的一层软件
操作系统有两个基本功能∶
(1)防止硬件被失控的应用程序滥用;
(2)向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。文件是对 I/O 设备的抽象表示,虚拟内存是对主存和磁盘 I/O 设备的抽象表示,进程则是对处理器、主存和 I/O 设备的抽象表示。我们将依次讨论每种抽象表示。
1.进程
进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。在大多数系统中,需要运行的进程数是多于可以运行它们的 CPU 个数的。传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。无论是在单核还是多核系统中,一个 CPU 看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为上下文切换。为了简化讨论,我们只考虑包含一个 CPU 的单处理器系统的情况。
操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是上下文,包括许多信息,比如 PC 和寄存器文件的当前值,以及主存的内容。在任何一个时刻,单处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从它上次停止的地方开始。图 1-12 展示了示例 hello 程序运行场景的基本理念。
示例场景中有两个并发的进程∶shell 进程和 hello 进程。最开始,只有 shell 进程在运行,即等待命令行上的输入。当我们让它运行 hello 程序时,shell 通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存 shell 进程的上下文,创建一个新的 hello 进程及其上下文,然后将控制权传给新的 hello 进程。hello 进程终止后,操作系统恢复 shell 进程的上下文,并将控制权传回给它,shell 进程会继续等待下一个命令行输入。
如图 1-12 所示,从一个进程到另一个进程的转换是由操作系统内核(kernel)管理的。内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用(system call)指令,将控制权传递给内核。然后内核执行被请求的操作并返回应用程序。注意,内核不是一个独立的进程。相反,它是系统管理全部进程所用代码和数据结构的集合。
2.线程
尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。由于网络服务器中对并行处理的需求,线程成为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程更高效。当有多处理器可用的时候,多线程也是一种使得程序可以运行得更快的方法
3.虚拟内存
虚拟内存是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。图 1-13 所示的是 Linux 进程的虚拟地址空间(其他 Unix 系统的设计也与此类似)。在 Linux 中,地址空间最上面的区域是保留给操作系统中的代码和数据的,这对所有进程来说都是一样。地址空间的底部区域存放用户进程定义的代码和数据。请注意,图中的地址是从下往上增大的。
每个进程看到的虚拟地址空间由大量准确定义的区构成,每个区都有专门的功能。
问题:堆和栈有什么区别?
堆和栈都是计算机中常见的数据结构,它们的作用和使用方式不同。
栈(Stack)是一种线性数据结构,它的特点是先进后出(Last In First Out,LIFO)。就像书堆成一摞,只有最上面的一本书能被拿走,栈也只允许在栈顶进行插入和删除操作。栈常用于计算表达式、递归算法、函数调用等场景,它可以记录函数调用的信息和执行状态,以便在函数调用结束后恢复现场。
堆(Heap)也是一种数据结构,它是用于动态分配内存的一种机制。堆中的数据存储并不是按照线性的顺序存储的,而是根据数据的大小关系来存储的。堆中的数据可以通过指针进行随机访问。堆的一个重要应用就是动态内存分配,程序可以在运行时从堆中申请一块内存区域,使用完毕后再将其释放,以便其他程序使用。
在计算机的内存中,栈通常位于高地址部分,堆通常位于低地址部分,这是因为栈的大小是固定的,而堆的大小是动态变化的,因此在设计内存分配时,通常将堆和栈分配到不同的地址空间中,以便进行更灵活的内存管理。
**程序代码和数据 **对所有的进程来说,代码是从同一固定地址开始,紧接着的是和 C 全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的,在示例中就是可执行文件 hello。
堆 代码和数据区后紧随着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用像 malloc 和 free 这样的 C 标准库函数时,堆可以在运行时动态地扩展和收缩。
**共享库。**大约在地址空间的中间部分是一块用来存放像 C 标准库和数学库这样的共享库的代码和数据的区域。共享库的概念非常强大,也相当难懂。
栈。位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。特别地,每次我们调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩。
**内核虚拟内存。**地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。
虚拟内存的运作需要硬件和操作系统软件之间精密复杂的交互,包括对处理器生成的每个地址的硬件翻译。基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。
4.文件
文件就是字节序列,仅此而已。每个I/O设备,包括磁盘、键盘、显示器,甚至网络,都可以看成是文件。系统中的所有输入输出都是通过使用一小组称为 Unix I/O 的系统函数调用读写文件来实现的。
文件这个简单而精致的概念是非常强大的,因为它向应用程序提供了一个统一的视图,来看待系统中可能含有的所有各式各样的 I/O 设备。例如,处理磁盘文件内容的应用程序员可以非常幸福,因为他们无须了解具体的磁盘技术。进一步说,同一个程序可以在使用不同磁盘技术的不同系统上运行。
1-4 计算机系统漫游-系统之间利用网络通信
https://hansimov.gitbook.io/csapp/ch01-a-tour-of-computer-systems/1.8
系统漫游至此,我们一直是把系统视为一个孤立的硬件和软件的集合体。实际上,现代系统经常通过网络和其他系统连接到一起。从一个单独的系统来看,网络可视为一个 I/O 设备,如图 1-14 所示。当系统从主存复制一串字节到网络适配器时,数据流经过网络到达另一台机器,而不是比如说到达本地磁盘驱动器。相似地,系统可以读取从其他机器发送来的数据,并把数据复制到自己的主存。
回到 hello 示例,我们可以使用熟悉的 telnet 应用在一个远程主机上运行 hello 程序。假设用本地主机上的 ssh客户端连接远程主机上的 ssh服务器。在我们登录到远程主机并运行 shell 后,远端的 shell 就在等待接收输入命令。此后在远端运行 hello 程序包括如图
当我们在 ssh客户端键入 “hello” 字符串并敲下回车键后,客户端软件就会将这个字符串发送到 ssh的服务器。ssh服务器从网络上接收到这个字符串后,会把它传递给远端 shell 程序。接下来,远端 shell 运行 hello 程序,并将输出行返回给 ssh服务器,最后,ssh服务器通过网络把输出串转发给 ssh 客户端,客户端就将输出串输出到我们的本地终端上。
Amdahl定律
https://hansimov.gitbook.io/csapp/ch01-a-tour-of-computer-systems/1.9