301-算术运算和逻辑运算
# 301-算术运算和逻辑运算
计算机的核心功能,就是运算。而运算的基本类型包括算术运算,和逻辑运算。想要了解计算机是如何实现运算的,我们就得从算术运算指令,和逻辑运算指令开始说起。
这是我们非常熟悉的模型机。我们在最一开始介绍模型积时,就用了一条加法指令作为例子。 加法指令就是一种算术运算指令,当时这条例子中的,这条加法指令 是将 R0 寄存器当中的数,和存储器地址为 6 的存储单元的数相加, 并将结果放回到 R0 寄存器当中。 这条指令的格式比较接近 X86 指令,它可以在运算指令当中进行存储器的操作。 而现在,我们要选用 MIPS 指令作为示例进行讲解。 我们也知道,MIPS 的运算指令是只能对寄存器进行操作的,例如这样的格式 add 表示是一条加法指令,$符号带上数字,是指对应的通用寄存器 。所以这条指令要做的 是将 9 号寄存器的内容和 10 号寄存器的内容相加,并存到 8 号寄存器当中。如果我们写这么一段 C 语言程序,并且假设在程序中定义的这几个变量, 分别被分配到了八号、九号、十号的寄存器当中, 那在 C 语言当中所写的这条加法语句,就会经过 MIPS 的编译器,生成这样的 MIPS 的指令。那这条指令的操作数,全都是寄存器,所以它是一条 R 型指令。 首先我们查指令编码表,知道 i 的指令的 opcode 应该是 0 function 是 16 进制的 20,也就是 10 进制的 32, 因为它不是一条移位指令,所以移位的域应该为 0,然后根据这条指令当中的操作数,我们可以知道,它的目的操作数,是 8 号寄存器, 所以 rd 对应是 8, 第一个源操作数是 9 号寄存器,第二个源操作数是 10 号寄存器,所以这两个对应的域分别应该是 9 和 10。根据这样的分析,我们就可以知道这条指令的具体编码。这条二进制的编码,就是存放在内存中的一条计算机的指令了。
那 CPU 通过取址,就会把这条指令,从内存中取回来。 并放到 ir 寄存器中,也就是存放指令编码的寄存器。 指令译码电路,看到了这条指令的编码之后, 根据 opcode 域为全 0,知道了这是一条 r 型的指令,再根据 function 域的值, 分析出这是一条 add 指令,因此,控制电路会向 ALU,也就是运算器, 发出对应的控制信号,指明接下来要进行加法运算,与此同时,指令译码部件 分析出,第一个源操作数 rs 是 9 号寄存器,第二个源操作数 rt 是 10 号寄存器, 我们注意看右边的内部总线,控制电路会选通对应的 源操作数寄存器,将其内容传送到 ALU 的输入:
同时指令译码部件还会分析出目的操作数 rd, 为八号寄存器,因此控制电路还会将 ALU 的输出,联通到八号寄存器的输入, 在这样的设定下,ALU 会根据控制电路给出的信号, 将两个输入的源操作数,进行加法运算,并送出运算的结果, 会被保存在八号寄存器中,这就完成了一次加法运算。
因此,对于 MIPS 指令来说,其核心的 算数运算指令,采用 R 型格式的有这几种: 这条加法指令,是将 rs 和 rt 寄存器的内容相加,并存在 rd 寄存器中, 这个 addu 和 add,从运算操作上本身看来是一样的,我们通过后面的注释也能看出来, 但区别在于 add 这条指令加了一个注释的标记, 他指的是上面这条 add 指令,在加法运算产生溢出时,会像 控制器报告异常,由控制器进行相关的处理,而 addu 这条指令,在 发生溢出时不会报出异常。关于溢出的问题, 在讲解加法器的实现时,还会进一步解释。
除了加法,还有对应的减法,也同样分为 sub 和 subu 两种运算指令,其操作与对应的加法指令是类似的。 这一类 R 型指令,它的源操作数都是在寄存器当中的。
但如果 有一个源操作数是立即数的话,就需要采用另一种格式的指令,也就是 I 型指令。 这一条 I 型格式的加法指令的例子,我们是将 22 号寄存器的内容 加上一个立即数-50,并将结果存放在 21 号寄存器当中。 这是对应的 I 型指令的格式, 我们同样也来看一看这条指令的编码,透过查指令编码表我们可以得到,这批指令的 opcode 是 8, 然后根据这条指令当中的参数,可以知道,源操作数的寄存器编号 rs 等于 22,目的操作数的寄存器编号 rt 等于 21, 而立即数这个域,应该是-50, 将这些数都转换成二进制,填入对应的区域,我们就可以知道这条指令的编码,
让 CPU 将这条指令的编码,取回放入 ir 寄存器当中后, 指令译码部件会根据 opcode 域 发现这是一条立即数的加法指令,因此控制电路, 会将 ALU 发出控制信号,指示接下来要进行加法运算, 同时,指令译码部件分析也会发现,其中一个源操作数来自于 22 号寄存器, 我们注意看右边的内部走线区域,22 号寄存器的内容 会在控制电路的控制下,通过内部走线走到 ALU 的一个入口, 而这个运算的另一个操作数是一个立即数, 这个立即数是存放在指令编码中的, 因此,控制电路会从指令编码中提取出这个立即数, 并将其传送到 ALU 的另一个入口, 而这条运算指令指示的目的操作数 rt,是第 21 号寄存器, 因此 ALU 的输出会被传送到 21 号寄存器, 这就是这条立即数加法指令的运算过程
但是我们要注意的一点是,这个立即数,是 16 位的。 而另一个源操作数以及目的寄存器,都是 32 位的,那如何将一个 16 位数和 32 位数进行相加呢,我们就需要进行一些转换。 转换的方法在 MIPS 的指令说明中很清楚地指出了, 上面这部份是刚刚介绍过的 R 型指令,这是我们刚才介绍的 I 型指令, 我们可以发现,在指令说明中,指出将 rs 寄存器的内容, 加上这个立即数的符号扩展,然后再将结果存放在 r 寄存器当中。 关于这条指令有两个说明,第一条跟刚才介绍过的一样,就是这条指令会 在加法发生溢出时,向控制器报出异常,交由控制器来处理。
第二个说明,指的是对立即数的扩展的方法, 立即数是 16 位的,那它如何被扩展到 32 位呢?是让它的高 16 位 是由这低 16 位当中的最高位,复制了 16 次,填充到高 16 位当中。 也就是说这 32 位当中的高 16 位,都和低 16 位当中的最高位,完全一样。 这样的扩展方法我们称为符号扩展,熟悉的补码规则的话,我们都应该知道进行符号扩展不会改变这个数做为一个有符号数的数值, 也就是说如果你本来是 +5,经过符号扩展还是 +5,如果你是-7,经过符号 扩展,那还是-7,如果不了解这一点的话,就应该了解 i 进制补码的相关知识。
那么回到这个 I 型指令的说明,还有对应的 addiu 这条指令,它的运算过程和 addi 是一样的。 区别只在于它在溢出时,不会产生异常。 这就是 MIPS 指令系统当中,核心的算术运算指令。
除了算术运算指令,还有一大类就是逻辑运算指令。 与算术运算指令类似,逻辑运算指令也分成 R 型和 I 型两种格式, 在 R 型当中,有 and 指令,就两个寄存器 当中的数,进行与操作,并将结果放在第三个寄存器中。 还有 or 指令, 是对两个寄存器当中的数,进行或操作。nor 指令, 是进行或非操作,也就是先进行或,然后再取反。
I 型指令当中,有 andi 指令, 也是进行与操作,但其中一个操作数是立即数, 这与算术运算指令类似,但其不同点在于,这 16 位立即数的扩展 方式,它不是采用符号扩展,而是采用 0 扩展。 同样还有 ori 指令,他其中一个操作数也是立即数。 那么这个立即数的 0 扩展操作是这样的, 扩展后的高 16 位全都是 0,而不是低 16 位当中最高位的复制,这种扩展方式,就称为 0 扩展, 因为算术运算指令会考虑将操作数看做是一个有符号的数,在运算时要考虑正数和负数的这个性质, 而逻辑运算指令,是将操作数,视为一组二进制的 01 串, 这就不存在正数或着负数这样的概念,因此它采用了 0 扩展,而不是符号扩展。
同样我们也来看一个例子, 这里选的是 R 型的与指令,如果在 C 语言当中 写了这样的语句,那就会对应产生这样的 MIPS 指令, 我们也来看看它的指令编码是什么样的, 通过查指令编码表,我们可以得到 opcode 域是 0,function 域是 16 进制- 的 24, 移位域同样还是 0,然后根据这条指令的 三个操作数得到,rd 域,也就是目的操作数是 8。 第一个源操作数是 9,第二个源操作数是 10,我们在 将这些数都转换成二进制,就可以得到这条指令的编码。
如果 CPU 将这条编码从内存中取回,并放回 IR 寄存器中, 那控制电路这一次就会发出要进行与操作的控制信号。 在这样的控制下 ALU 就会将九号十号寄存器当中内容,进行与运算,并将结果送回到了 8 号寄存器当中。
最后,我们来看一看这些算术逻辑运算的总体需求, 对于算术运算, 我们可能需要将两个 32 位的数相加,其运算结果,是一个 32 位数。 或着进行两个 32 位数的减法,运算结果,也是一个 32 位数。 除了进行运算,我们还要检查运算的结果是否发生溢出。
而对于逻辑运算,我们需要进行两个 32 位数的与操作, 结果也是一个 32 位数。第二种情况是执行或操作, 第三种情况是执行或非操作, 这些就对应了 MIPS 的核心指令中,提供的加法 减法、与、或、或非这些指令的功能。
现在我们已经了解了这些运算指令的基本特点, 下一步,我们就得分析这些指令在硬件电路上, 是如何实现的。