购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

6.7 计算机中的负数

6.7.1 无符号数和有符号数

为了讲解后面的内容时能够顺利一些,现在我们离开源程序,来介绍一些题外的知识。

从本书的开篇到现在,我们一直没有提到负数,就好像世界上根本没有负数一样。计算机当然要处理负数,要不然它将没有多少实用价值。

在计算机中使用负数,这是一个容易令人产生迷惑的话题。不信?现在就开始了。

尽管我们从来没有考虑过数的正负问题,但是,事实上,我们在编写程序的时候,既可以使用正数,也可以使用负数。如图6-3 所示,我们在程序中用伪指令db 声明了一些正数和一些负数。

图6-3 在汇编源程序中使用负数的例子

图6-4 显示了编译后的结果。用伪指令db 声明的数据都只有一个字节的长度, 所以很容易在这两幅图的各个数之间建立对应关系。

图6-4 正数和负数编译后的结果

前面的正数都很好理解,十进制数128 对应的二进制数是10000000,对应的十六进制数是0x80;十进制数0 对应的二进制数是00000000,对应的十六进制数是0x00。为什么我们对此不感到新鲜?因为这显得非常自然,从本书一开始到现在,我们就是这样工作的。

真正的麻烦在于后面的负数,比如-1,它在编译的时候,编译器会怎么做呢?

它很笨,但也很聪明。因为-1 其实等于0-1,它就知道可以做一次减法。当然,这个减法,不是你已经熟悉的十进制减法,这没有用,你得做二进制的减法,也就是用二进制数0 减去二进制数1,结果是

注意左边的省略号,这是因为在相减的过程中,不停地向左边借位的结果。因此,可以说,这个数字是很长的,取决于你什么时候停止借位。

再比如十进制数-2,可以用0-2 来得到,在二进制的世界里,该减法是二进制数0 减去二进制数10,结果是

同样,相减的过程要向左借位,所以这个数字相当长。但是,最右边那一位是0。

在计算机中,数字保存在寄存器里,而在16 位处理器里,寄存器通常是8 位和16 位的。因此,以上相减的结果,只能保留最右边的8 位或者16 位。举个例子,十进制数-1 在寄存器AL中的二进制形式是

即0xFF;十进制数-2 在寄存器AL 中的二进制形式是

即0xFE。如果是16 位的寄存器,则相应地,要保留相减结果的最右边16 位。因此,十进制数-1 在AX 寄存器中的二进制形式是

即0xFFFF;十进制数-2 在寄存器AX 中的二进制形式是

即0xFFFE。

当然,数据还可以保存在内存中,或者编译后的二进制文件中。在二进制文件中,数据是用伪指令db 或者dw 等定义的。但是,数据的表示形式和它们在寄存器中的形式相同,以下代码片断很清楚地说明了这一点。

这是很令人吃惊的。因为我们知道,0xFF 等于十进制数255,但现在它又是十进制数-1,哪一个才是正确的呢?我们应该以哪一个为准呢?

好吧,假设这勉强能接受的话,那么,对照一下图6-3 和图6-4,你会发现,0x80 既是十进制数128,又是十进制数-128,到底哪一个是正确的呢?

这真是令人头疼的问题,不单单是对我们,对几十年前那些计算机工程师们来说也是如此。

一个良好的解决方案是,将计算机中的数分成两大类:无符号数和有符号数。无符号数的意思是我们不关心这些数的符号,因此也就无所谓正负,反正它们就是数而已,就像小学生一样,眼中只有自然数。在8 位的字节运算中,无符号数的范围是00000000~11111111,即十进制的0~255;在16 位的字运算中,无符号数的范围是0000000000000000~1111111111111111,即十进制的0~65535;在将来要讲到的32 位运算中,无符号数的范围是000000000000000000000000~ 11111111111111111111111111111111,即十进制的0~4294967295。很显然,我们以前使用的一直是无符号数。

相反地,有符号数是分正、负的,而且规定,数的正负要通过它的最高位来辨别。如果最高位是0,它就是正数;如果是1,就是负数。如此一来,在8 位的字节运算环境中,正数的范围是00000000~01111111,即十进制的0~127;负数的范围是10000000~11111111,即十进制的-128~-1。

正的有符号数,和与它同值的无符号数相同,这没什么好说的,毕竟它们形式上相同,按相同的方式处理最为方便。但是,负数就不同了,在这里,10000000~11111111 这些负数,都是用0减去它们相对应的正数得到的。想知道它们各自对应的正数是谁吗?很简单,因为“负数的负数”是正数,所以只需要用0 减去这个负数就行。所以,你可以试试看,因为

所以,10000000~11111111 这个范围内的有符号数,对应着十进制数-128~-1。

顺便说一下,在8086 处理器中,有一条指令专门做这件事,它就是neg。neg 指令带有一个操作数,可以是8 位或者16 位的寄存器,或者内存单元。如

它的功能很简单,用0 减去指令中指定的操作数。例子:如果AL 中的内容是00001000(十进制数8),执行neg al 后,AL 中的内容变为11111000(十进制数-8);如果AL 中的内容为11000100(十进制数-60),执行neg al 后,AL 中的内容为00111100(十进制数60)。

相应地,在16 位的字运算环境中,正数的范围是0000000000000000~0111111111111111,即十进制的0~32767,负数的范围是1000000000000000~1111111111111111,即十进制的-32768~-1。

不要给计算机和编译器添麻烦。既然你已经知道一个字节可以容纳的数据范围是十进制的-128~127,就不要这样写:

寄存器AL 只有8 位,因此,编译后,-200 将被截断,机器码为B0 38。你可以这样写:

这时,编译后的机器码为B8 38 FF。

同样的规则也适用于伪指令db 和dw。举例(以下均为十进制数):

32 位有符号数是16 位和8 位有符号数的超集,16 位有符号数又是8 位有符号数的超集,它们互相之间有重叠的部分。正数还好说,十进制数15,在8 位运算环境中是00001111,在16位运算环境中是0000000000001111,没有什么区别。但是,同一个负数,其表现形式略有差别。比如十进制数-3,它在8 位运算中是11111101,即0xFD;在16 位运算中,则是1111111111111101,即0xFFFD。这种差别的来源很简单,我们已经讲过了,在计算机中,-3 是用0 减去3 得到的,在8 位运算中只能保留结果的低8 位,即11111101(0xFD);在16 位运算中只能保留结果的低16 位,即1111111111111101(0xFFFD)。

很显然,一个8 位的有符号数,要想用16 位的形式来表示,只需将其最高位,也就是用来辨别符号的那一位(几乎所有的书上都称之为符号位,实际上这并不严谨),扩展到高8 位即可。为了方便,处理器专门设计了两条指令来做这件事:cbw(Convert Byte to Word)和cwd(Convert Word to Double-word)。

cbw 没有操作数,操作码为98。它的功能是,将寄存器AL 中的有符号数扩展到整个AX。举个例子,如果AL 中的内容为01001111,那么执行该指令后,AX 中的内容为0000000001001111;如果AL 中的内容为10001101,执行该指令后,AX 中的内容为1111111110001101。

cwd 也没有操作数,操作码为99。它的功能是,将寄存器AX 中的有符号数扩展到DX:AX。举个例子,如果AX 中的内容为0100111101111001,那么执行该指令后,DX 中的内容为0000000000000000,AX 中的内容不变;如果AX 中的内容为1000110110001011,那么执行该指令后,DX 中的内容为1111111111111111,AX 中的内容同样不变。

尽管有符号数的最高位通常称为符号位,但并不意味着它仅仅用来表示正负号。事实上,通过上面的讲述和实例可以看出,它既是数的一部分,和其他比特一起共同表示数的大小,同时又用来判断数的正负。

6.7.2 处理器视角中的数据类型

无符号数和有符号数的划分并没有从根本上打消我们的疑虑,即假如寄存器AX 中的内容是0xB23C,那么,它到底是无符号数45628 呢,还是应当将其看成是-19908?

答案是,这是你自己的事,取决于你怎么看待它。对于处理器的多数指令来说,执行的结果和操作数的类型没有关系。换句话说,无论你是从无符号数的角度来看,还是从有符号数的角度来看,指令的执行结果都是正确无误的。比如

这条指令显然根本不考虑操作数的类型。再比如

在这里,0xf0 的二进制形式是11110000,它既可以解释为无符号数240(十进制),也可以解释为有符号数-16,毕竟它的符号位是1。无论如何,inc 是加一指令,这条指令执行后,AH 中的内容是二进制数11110001,既是无符号数241,也是有符号数-15。

再考虑加法运算。比如

0x8c03 的二进制形式是1000110000000011,既可以看做无符号数35843(十进制),也可以看成是有符号数-29693(十进制)。在运算过程中,数的视角要统一,如果把0x8c03 看成是无符号数,那么0x05 也是无符号数;如果0x8c03 是有符号数,那么0x05 也是有符号数。

关键是运算后的结果。很幸运的是,add 指令同样适用于无符号数和有符号数。所以,这两条指令执行后,AX 中的内容是0x8c08,分别可以看成是无符号数35848 和有符号数-29688。

再来考虑一下减法。考虑一下,如果要计算10-3,这其实可以看成是10+(-3)。因此,使用以下三条指令就可以完成减法运算:

正是因为这个原因,很多处理器内部不构造减法电路,而是使用加法电路来做减法。

尽管如此,为了方便起见,处理器还是提供了减法指令sub,该指令和加法指令add 相似,目的操作数可以是8 位或者16 位通用寄存器,也可以是8 位或者16 位的内存单元;源操作数可以是通用寄存器,也可以是内存单元或者立即数(不允许两个操作数同时为内存单元)。比如

因为处理器没有减法运算电路,所以,举例来说,sub ah,al 指令实际上等效于下面两条指令:

可以这么说,几乎所有的处理器指令既能操作无符号数,又能操作有符号数。但是,有几条指令除外,比如除法指令和乘法指令。

我们已经学过除法指令div。严格地说,它应该叫做无符号除法指令(Unsigned Divide),因为这条指令只能工作于无符号数。换句话说,只有从无符号数的角度来解释它的执行结果才能说得通。举个例子:

从无符号数的角度来看,0x0400 等于十进制数1024,0xf0 等于十进制数240。相除后,寄存器AL 中的商为0x04,即十进制数4,完全正确。

但是,从有符号数的角度来看,0x0400 等于十进制数1024,0xf0 等于十进制数-16。理论上,相除后,寄存器AL 中结果应当是0xc0。因其最高位是“1”,故为负数,即十进制数为-64。

为了解决这个问题,处理器专门提供了一个有符号数除法指令idiv(Signed Divide)。idiv 的指令格式和div 相同,除了它是专门用于计算有符号数的。如果你决定要进行有符号数的计算,必须采用如下代码:

在用idiv 指令做除法时,需要小心。比如用0xf0c0 除以0x10,也就是十进制数的除法-3904÷ 16。你的做法可能会是这样的:

以上的代码是16 位二进制数除法,结果在寄存器AL 中。除法的结果应当是十进制数-244,遗憾的是,这样的结果超出了寄存器AL 所能表示的范围,必然因为溢出而不正确。为此,你可能会用32 位的除法来代替以前的做法:

很遗憾,这依然是错的。十进制数-3904 的16 位二进制形式和32 位二进制形式是不同的。前者是0xf0c0,后者是0xfffff0c0。还记得cwd 吗?你应该用这条指令把寄存器AX 中数的符号扩展到DX。所以,完全正确的写法是这样的:

以上指令全部执行后,寄存器AX 中的内容为0xff0c,即十进制数-244。

主动权在你自己手上,在写程序的时候,你要做什么,什么目的,你自己最清楚。如果是无符号数计算,必须使用div 指令;如果你是在做有符号数计算,就应当使用idiv 指令。

检测点6.3

假如以下声明的是有符号数,那么,其中的负数是(             )。

data0 db 0xf0,0x05,0x66,0xff,0x81

data1 dw 0xfff,0xffff,0x8b,0x8a08 5Yp4uIrcvn/M/Q9VMvbG3FIWDnEOkW9x/UAE69E7/MDzFN1jLxzCSXtqP/nWvDTA

点击中间区域
呼出菜单
上一章
目录
下一章
×