3.加减运算

数据的表示与存放问题解决了。这些数据要参与运算,冯诺依曼计算机的特征是所有信息都是二进制编码,所以运算也是基于01完成的,这部分研究如何构建运算电路。

3.1 布尔代数和基本逻辑电路

3.1.1

关于01的一套数学运算体系起源于1850年前后英国数学家乔治布尔,所以就叫做布尔代数了。

最基本的逻辑运算:与或非,任一逻辑表达式都可以写成这种关系,比如异或也是常用的逻辑运算。

基本的门电路与或非门,其他的门电路可以用这三种基本门电路实现。

对于n位逻辑运算,重复来n个相同逻辑门就行,实现按位与或非。

组合逻辑电路无存储功能,输出取决于当前输入。时序逻辑电路有存储功能,除了当前输入,也受到存储单元状态的影响。

可以使用基本逻辑门组成有特定功能的组合逻辑部件,如译码器、编码器、多路选择器、加法器。

可以使用基本的数字电路设计方法实现出组合逻辑部件

  • 真值表描述功能部件的输入和输出关系

  • 根据真值表确定逻辑表达式

  • 根据逻辑表达式实现逻辑电路

比如多路选择器,最基本的是二选一,很容易实现。选择端的位数是和输入端的个数相关的。

3.1.2 加法器

一位加法器,全加器,两个加数AB,低位进位Cin,和为F,向高位进位为Cout

实现出来之后封装好,就是一个模块化的全加器。

1bit加法器可以实现nbit加法器,进位串起来就可以。

加法实现以后,其他所有算术运算部件都可以基于加法器和逻辑运算来实现,因此所有的算术运算都可以基于01以及逻辑运算实现。这是个计算机设计时很重要的基础

n位的无符号数加法器无法用于带符号数想加,无法判断溢出,比较大小也无法实现。比较通常通过做减法来实现,减法用补码采用加法器实现。

因此加法器除了基本功能,还需要输出一些标志信息。

  • OF溢出,Cn异或Cn-1

  • SF符号标志,最高位就行

  • ZF全为0,或非

  • CF进位借位标志,Cout异或Cin

至此我们可以把这个东西叫做ALU了,功能比较基础

3.1.3 整数加减和ALU

int x = 9,y = 6,z1,z2;
z1 = x + y;
z2 = x - y;

来分析以下变量的机器数。

要能实现这些东西,要求机器的硬件可以实现补码的表示,以及和的补码和差的补码的实现。

[x]=2n+X[x]_{补} = 2^n + X

根据这个定义,可以推导一些补码运算的公式。

分析到此,在电路上,需要多设计一个取反电路,二路选择器实现原码还是补码,

在上面的基础上,就构造了一个可以实现整数加减法的运算部件。在此基础上,再加上寄存器,移位器,就可以实现ALU乘除运算等功能了。

ALU叫做算术逻辑部件,里面的核心部件就是整数加减运算。

ALU输出的结果,由ALUop控制端控制,控制端的位数由ALU可以做的操作的个数决定。比如8种操作就需要3bit的op控制线,ALUop的编码对应了不同的操作。

ALU是CPU里的器件,CPU里的控制器送来的就是ALU的控制编码,输出端获得结果以及标志位

3.2 C表达式到逻辑电路

C基础:基本数据类型;基本运算类型+-*/%<>==,位操作,逻辑运算,移位,扩展和截断

高级语言的运算表达式距离机器还有些距离,来看看是怎么实现的。

比如y = (x>>2) + k如何在计算机里实现。

任何高级语言的表达式,都要转换为指令序列。

上面这个式子

sarw $2, %ax ;x>>2 值在ax寄存器里 addw %bx, %ax ;k的值在bx

计算机直接执行指令来完成运算,控制器对指令移码,产生控制信号送运算电路。

操作数在运算电路里运算

sarw $2, %ax 将操作数2和R[ax]送移位器运算 addw 把两个寄存器送整数加法器中运算

可以看出,高级语言表达式到最终逻辑门电路中间是通过指令作为媒介。高级语言转换成指令,指令能直接在硬件上执行,指令最终靠逻辑门实现。

因此高级语言里的运算一定要由相应的指令来实现。

3.3 C语言里的运算

  • 算术运算

    • 无符号数

    • 带符号整数

    • 浮点数的+-*/%

  • 位运算

    • 用来对位串实现掩码操作或相应的其他处理

    • 操作

      • |

      • &

      • ~

      • ^

  • 移位运算

    • 用来提取信息,扩大缩小特殊倍

    • << >>

    • 语法层次上未设计逻辑移位还是算术移位,在指令层次上有区别,由数据类型确定

比如从数据y中提取低位字节,使用&实现掩码操作:y&0x00ff,就得到了低字节。

移位会有数据丢失或溢出的问题。

  • 逻辑运算

    • 用于关系表达式的运算

    • ||

    • &&

    • !

区别按位操作。

还有一类没有运算符的运算,通常在类型转换嗯时候用到,位扩展和位截断运算,没有专门的操作运算符,都是隐含操作。

  • 位扩展,char转int

  • 位截断,int赋值给char

3.4 加减运算的实现

3.4.1 整数加减运算

按照前面ALU的思路,最核心的是加法器。加减法运算都用加法来处理,加法器加上额外的电路能实现减法运算。

加法器本身并不知道是否是带符号数,只管加,并且也不判断对错,只是给低n位作为结果,生成标志信息。至于结果是否正确,是靠软件实现的。

运算时候的条件码(标志位)专门记录到寄存器里,状态字寄存器,或者标志寄存器,如IA32里的EFLAGS寄存器。

这些标志可以在if语句里用作条件转移的判断。

在做加法运算时,主要判断是否溢出,无符号加溢出条件CF=1,带符号数溢出条件OF=1

如果是做减法,

3.4.2 整数减法举例

一个简单的C语言程序,加法部件假定位数为8。

#include <stdio.h>

typedef unsigned char uint8_t;
typedef signed char int8_t;

int main()
{
    uint8_t x = 134;  /* 机器码 0x86 0b1000 0110 */
    uint8_t y = 246;  /* 机器码 0xf6 0b1111 0110 */

    int8_t m = x;     /* 机器码 0x86 0b1000 0110 输出-122 */
    int8_t n = y;     /* 机器码 0xf6 0b1111 0110 输出-10  */

    uint8_t z1 = x - y; /* 0b1000 0110 + 0b00001001 + 1 = 0b1001 0000 */
    uint8_t z2 = x + y; /* 0x86+0xf6=0x17c 输出124 */

    int8_t k1 = m - n;
    int8_t k2 = m + n; /* 0x86+0xf6=0x17c 输出124 */

    printf("x: %d (%x %b)\n",x,x,x);
    printf("x: %d (%x %b)\n",y,y,y);

    printf("m: %d (%x %b)\n",m,m,m);
    printf("n: %d (%x %b)\n",n,n,n);

    printf("x-y: %d (%x %b)\n",z1,z1,z1);
    printf("x+y: %d (%x %b)\n",z2,z2,z2);

    printf("m-n: %d (%x %b)\n",k1,k1,k1);
    printf("m+n: %d (%x %b)\n",k2,k2,k2);
    return 0;
}

控制台输出结果:

x: 134 (86 10000110)
x: 246 (f6 11110110)
m: -122 (ffffff86 11111111111111111111111110000110)
n: -10 (fffffff6 11111111111111111111111111110110)
x-y: 144 (90 10010000)
x+y: 124 (7c 1111100)
m-n: -112 (ffffff90 11111111111111111111111110010000)
m+n: 124 (7c 1111100)

进程已结束,退出代码0

c语言里=赋值运算符是直接把机器码等过去。

从这个例子可以看出来,ALU只是机械的对机器码作运算,并不管结果的对错,结果对错需要程序判断。这些程序上运算结果的异常背后都是数字电路上的实现逻辑。

程序里的x-y运算,送到了ALU这边,有个问题是怎么求一个数负数的补码,直接全部(符号位也取反)按位取反就可以轻松转换成加法,sub指令相当于数据选择器选择原来的数据输出还是说取反输出,顺手输入到Cin即按位取反再加1,这个硬件上的设计还是挺有意思的。

有了这些基础,再来看看如何用程序判断一个无符号数相加有无溢出

最后更新于