计算机的工作原理

本书的最终目标是将我们的语言编译成能够在 CPU 上原生运行的机器码。要实现这一目标,我们需要对计算机的工作原理有一定的了解。所以,让我们从宏观层面来看看计算机能做些什么,而不深入探讨细节。

本章概述了在开始编译器项目之前你需要了解的知识。具体细节将在后面介绍。

5.1 寄存器、内存和指令

CPU 会按顺序执行存储在内存中的一系列 “指令”。其中有用于算术运算的指令,比如对数字进行加法或乘法运算。也有用于控制流的指令,这些指令会打破顺序执行(分支),例如跳转到另一条指令、调用函数以及从函数调用中返回。

CPU 包含固定数量的内部存储单元,称为 “寄存器”。一个寄存器存储一定数量的二进制位,通常表示一个整数、一个指针或其他东西(所有内容都是一些二进制位)。计算是在寄存器上进行的,而不是直接在内存中进行。

比如说,你想把两个数字相加,这两个数字必须从内存加载到寄存器中,然后 CPU 才能执行加法运算。加法运算的结果也存储在一个寄存器中,之后可以再将其传回到内存中。

总而言之,CPU 指令可以分为以下几类:

  1. 1. 算术运算:在寄存器上进行计算。
  2. 2. 内存操作:在内存和寄存器之间进行数据加载或存储。
  3. 3. 分支操作:转移执行流程。

请注意,CPU 指令并不总是只属于单一类别。在 x86(以及 x64)架构中,算术指令也可以进行内存的加载或存储操作。例如,除了对寄存器进行加法运算外,x86 还允许将一个寄存器与一个内存位置的数据相加,反之亦然(但不允许对两个内存位置的数据直接相加)。

5.2 函数调用和栈

通常有两种类型的分支指令:

  1. 1. 基于条件跳转到另一条指令,这用于如 if-then-else 和循环等控制流中。
  2. 2. 函数调用和返回。

函数调用指令并非必不可少。编译器可以仅使用第一种类型的分支指令来实现(模拟)函数调用和返回。然而,对于函数调用存在一些约定。

当从函数调用中返回时,程序如何知道返回的位置呢?一个常见的约定是,在跳转到目标函数之前,调用者将其自身的位置(返回地址)压入栈中;这样,被调用者通过查看栈就知道应该返回到哪里。在 x86 架构中,有专门遵循这一约定的指令:call 指令将当前程序位置压入栈中,并跳转到目标函数,而 ret 指令则执行相反的操作。

这通常被称为 “调用约定”。调用约定还包括其他部分,例如栈是如何定义的以及参数是如何传递的,这些我们将在后面学习。

除了返回地址外,栈还存储局部变量。你可以看到局部变量是如何体现 “局部性” 的 —— 当从函数调用返回时,这些局部变量会从栈中移除。

5.3 系统调用

一个程序必须与操作系统进行交互以实现输入和输出操作。这种交互机制看起来就像一个普通的函数调用:你调用操作系统的一个例程,调用线程会暂停,直到调用返回。

使用像 C 或 Python 这样的高级编程语言进行编程时,很少会直接调用系统调用。程序员依赖于语言的运行时环境或标准库在底层处理系统调用。

然而,由于我们是从零开始构建一个编译器,没有现有的运行时环境,所以我们稍后将直接处理系统调用。一个不依赖于运行时环境或库的程序被称为 “独立程序”。

阅读剩余
THE END