6. 指令与汇编

本节我们来看如何设计最基本的算术逻辑运算指令,这些指令的运算通过前面章节提到的16位ALU来完成。我们先回顾一下该 ALU 的示意图:

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 的运算过程看着是顺序地执行各个控制位对应的操作,然后得到最终的结果,这样我们能够得到的指令表如下:

其实也不是严格意义上的顺序执行,控制位 zx 与 nx 是对输入 x 进行预处理,而 zy 与 ny 是对输入 y 进行预处理,这两个处理是可以并行执行的。

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

注意,在二进制补码中对任意整数 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 来控制,完整的电路设计如下图所示:

图中 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)来完成。

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

最后更新于