> For the complete documentation index, see [llms.txt](https://s2.shizhz.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://s2.shizhz.me/s2e6.md).

# 6. 指令与汇编

本节我们来看如何设计最基本的算术逻辑运算指令，这些指令的运算通过前面章节提到的[16位ALU](/s2e3/3.md)来完成。我们先回顾一下该 ALU 的示意图：

![16 Bit ALU](/files/-MdWJOMG8AM4GSW0cTcJ)

ALU 通过6个控制位来确定执行何种操作，最多可以支持 2^6=64 种运算，因此理论上我们可以将想要完成的运算依次排列即可，例如规定 000001 执行 x+y, 000010 执行 x-y, 000011 执行 x\&y 等等。这种思路虽然简单直观，但并不适合设计复杂系统。

贯穿全文，我们始终在努力呈现一种方法论：复杂的组件是通过简单的组件构造而成的！这种组件化、模块化的思路是构造大型、复杂系统的核心思想。所以此处我们为 ALU 设计指令时，最好的策略也是通过组合各种更原始的操作，来达成最终的计算效果。所以此处我们让每个控制位负责一个独立的微操作，然后通过组合的方式达成复杂的计算效果。下表是6个控制位的职责：

| Control Bit | 0     | 1                        |
| ----------- | ----- | ------------------------ |
| zx          | -     | x = 0, 将 x 设置为0          |
| nx          | -     | x = !x, 将 x 按位取反         |
| zy          | -     | y = 0, 将 y 设置为0          |
| ny          | -     | y = !y, 将 y 按位取反         |
| f           | x + y | x & y                    |
| no          | -     | out = !out, 将结果 out 按位取反 |

可以将 ALU 的运算过程看着是顺序地执行各个控制位对应的操作，然后得到最终的结果，这样我们能够得到的指令表如下：

{% hint style="info" %}
其实也不是严格意义上的顺序执行，控制位 zx 与 nx 是对输入 x 进行预处理，而 zy 与 ny 是对输入 y 进行预处理，这两个处理是可以并行执行的。
{% endhint %}

| Table 1:         |    |    |    |   |    |        |
| ---------------- | -- | -- | -- | - | -- | ------ |
| ALU Instructions |    |    |    |   |    |        |
| zx               | nx | zy | ny | f | no | out    |
| 1                | 0  | 1  | 0  | 1 | 0  | 0      |
| 1                | 1  | 1  | 1  | 1 | 1  | 1      |
| 1                | 1  | 1  | 0  | 1 | 0  | -1     |
| 0                | 0  | 1  | 1  | 0 | 0  | x      |
| 1                | 1  | 0  | 0  | 0 | 0  | y      |
| 0                | 0  | 1  | 1  | 0 | 1  | !x     |
| 1                | 1  | 0  | 0  | 0 | 1  | !y     |
| 0                | 0  | 1  | 1  | 1 | 1  | -x     |
| 1                | 1  | 0  | 0  | 1 | 1  | -y     |
| 0                | 1  | 1  | 1  | 1 | 1  | x + 1  |
| 1                | 1  | 0  | 1  | 1 | 1  | y + 1  |
| 0                | 0  | 1  | 1  | 1 | 0  | x - 1  |
| 1                | 1  | 0  | 0  | 1 | 0  | y - 1  |
| 0                | 0  | 0  | 0  | 1 | 0  | x + y  |
| 0                | 1  | 0  | 0  | 1 | 1  | x - y  |
| 0                | 0  | 0  | 1  | 1 | 1  | y - x  |
| 0                | 0  | 0  | 0  | 0 | 0  | x & y  |
| 0                | 1  | 0  | 1  | 0 | 1  | x \| y |

> &#x20;注意，在二进制补码中对任意整数 x 按位取反后（即执行 !x 操作），实际得到的数值是 -x - 1, 因为 x + !x = 111….11, 在二进制补码中这是 -1 的编码方式。

我们来看一下 x + 1（对应控制码：011111）的执行过程：

1. zx 为 0, 不对 x 做任何操作
2. nx 为 1, 于是 x = !x, 从数值上来讲此时 x = -x - 1
3. zy 为 1, y = 0
4. ny 为 1, y = !y, 此时 y = -1
5. f 为 1, 所以 out = x + y = -x -2, 最右侧的 x 表示最原始的x输入数值
6. no 为 1, out = !out, 此时 out 的数值为 -(-x - 2) - 1 = x + 1

这样的逻辑拆分极大的简化了电路设计，由于每个控制位都在做一个二选一的操作，这可以通过之前介绍过的 Selector 来控制，完整的电路设计如下图所示：

![ALU Impl](/files/-MdWV_ztxtiNoVK85Kys)

图中 ng 可以通过一个 16-1 Selector 来选择 out 的最高位，而 zr 可以通过一个 16 位的或非门实现。

确定了ALU的控制信号之后，我们可以来看一条完整的CPU计算指令还缺哪些方面：

1. 如何指定输入，即 x 与 y \
   CPU 从寄存器取数据是最快的，所以我们可以通过指定两个寄存器的方式来指定输入。寄存器可以有固定的编号，例如如果 CPU 有 8 个通用寄存器的话，可以用三位数字对其进行编码，并且约定 x 在前，y 在后。例如 000001 表示使用编号为 000 的寄存器作为 x, 使用编号为 001 的寄存器作为 y, 如果不需要 y 的话可以只提供 x 的寄存器编号。
2. 如何存放输出，即 out, ng 与 zr \
   计算结果我们可以约定存入 x 的寄存器中，这样可以降低指令长度。ng 与 zr 可以使用固定的方式存入专门的寄存器中（例如一个叫着 FLAGS 的寄存器），在指令中可以不用指定。
3. CPU 如何知道这是一条计算指令 \
   上一节我们提到三类指令：计算类、数据传输类、流程控制类，因此我们可以通过两位数据来进行区分，这里令 00 表示计算类指令、01 表示数据传输类指令、10 表示流程控制类指令。这两位用以区分指令种类，我们可以将其放在指令的最前面

因此我们可以约定计算类指令的格式为：**<指令种类：2位><控制信号：6位><输入 x 与 y 的寄存器编号：3 位或者 6 位>**, 这样的话当 CPU 获取到指令 `00011111000` 时，就会将寄存器 000 中的数字加一。而对指令进行解码、寄存器寻址等操作由 CPU 的控制单元（CU）完成。

机器指令是二进制的，非常不便于人类记忆，我们可以发明一些助记符来帮助编程，即将二进制的指令符号化。例如我们可以将寄存器进行编号：A, B, C等；使用英文单词替换 ALU 的控制符，使其更有语义，例如上述指令可以简称 INC, 这样上述指令就可以写成：INC A, 而加法指令可以写成：ADD A, B. 这种符号化的机器指令叫着汇编语言（Assembly Language），从汇编语言到机器指令的转换由汇编器（Assembler）来完成。

汇编语言不仅仅是机器指令的简单映射，而是基于机器语言的又一次抽象。因为汇编语言需要通过汇编器进行处理，所以我们可以在汇编器中增加更多的功能来支持其他的编程范式，例如使用变量、模块化编程等等。这是让我们脱离硬件思维，开始使用软件架构的思路来看待程序设计的第一步。
