一、80386机器提供了几种扩展数据大小的操作码。
首先要记住计算机永远都不知道一个数是无符号数还是有符号数。
有符号数和无符号书的区别是通过程序员使用不同的操作来体现的。
对于无符号数,只要把它的高位简单的置0即可。例如扩展AL中的一个
byte值为一个字节大小的值从八位扩展到16位,用如下操作即可
MOV AH 0;
然而我们并不能用MOV操作把AX中一个字的无符号数扩展为EAX中的两个字的无符号数。
那么为什么会这样呢?原因在于80386没有提供这种把EAX高16位置0的MOV操作。至于为什么没有提供?
出于什么样的考虑还需去查找资料。在这里就不得而知了。
80386种解决的办法是提供了另一种操作即:MOVZX.这个操作必须提供两个参数:第一个参数作为目
标寄存器,这个寄存器必须是16或者32位。第二个参数是源寄存器,该寄存器必须是8或者16位。或
者第二个参数也可以是一个字节或一个字的内存对象。总之目标寄存器必须比源数据大。这个是可以
理解的,如果必须小那就难以理解了。
下面是几个例子:
movzx eax, ax ; 扩展ax 到 eax
movzx eax, al ; 扩展al 到eax
movzx ax, al ; 扩展al 到 ax
movzx ebx, ax ; 扩展ax 到 ebx
对于有符号数,就没有这么简单的MOV操作了。但是8086提供了几种操作用来扩展有符号数。不管用什么方法,
不解决是不行的。
CWT 用于把一个字节有符号数扩展为一个字有符号数
这个操作把AL扩展为AX
CWD 用于把一个字的有符号数扩展为两个字的有符号数
这个操作可以把AX 扩展到 DX:AX.注意这里DX:AX看作一个32位的寄存器。虽然他们是由两个寄存器组成。高16位存于DX中而低16位存于AX中。
CWDE 用于把一个字的有符号数扩展为两个字的有符号数
这个操作可以把AX 扩展到 EXA
CDQ 用于把两个字的有符号数扩展为四个字的有符号数
这个操作可以把EXA扩展到 EDX:EXA中
最后MOVSX操作和movzx 操作规则是一样的只不过它用于有符号数。
二、有符号数无符号数的C语言表示
在C语言中我们也会遇到有符号数无符号数的扩展问题。c语言中的变量可以被声明为有符号数或者无符号数。
(int 是有符号数)
看一下一下几行代码:
1 unsigned char uchar = 0xFF;
2 signed char schar = 0xFF;
3 int a = (int ) uchar ; / a = 255 (0x000000FF) /
4 int b = (int ) schar ; / b = -1 (0xFFFFFFFF) /
第三行中是一个无符号数的扩展,用到了MOVZX操作。第四行中是一个有符号数的扩展用到了MOVSX操作
char ch;
while( (ch = fgetc(fp)) != EOF ) {
/ do something with ch /
}
关于这方面的内容,有一个c语言的BUG.考虑上述的代码。是fgetc()函数的原形
int fgetc( FILE * );
问题是:这个函数是读取一个char的,而为什么要返回一个int呢?原因在于通常情况下这个函数是返回
一个char(用高位置0的方法扩展为一个int),但是这里有一个值却不是一个character EOF 是一个宏定义通常的值为-1.
因此fgetc返回一个char扩展为一个int表示为十六进制的形式因该为:000000xx形式。或者是-1扩展为一个十六
进制的形式为FFFFFFFF
代码中问题存在于他返回的是一个int值却存储在一个char里。这样在c语言里为自动的去掉高位把它变为一个char
值。问题只在有范围值为000000FF 和 FFFFFFFF 时。它们都会变为FF的byte值。这样在这个循环中就没有办法区分000000FF
和 FFFFFFFF的不同
确切的说上述代码究竟怎么运行取决于char是有符号还是无符号.为什么呢?因为在第二行中,ch是和一个int值进行
比较的。ch将扩展为一个int.
如果ch是一个无符号数,那么FF将扩展为000000FF.这个循环将永远循环下去。
如果ch是个有符号数,那么FF将扩展为FFFFFFFF,循环可以停止,但现在却没有办法区分从文件中读出来的000000FF还是结束符
FFFFFFFF
解决的方法是把字符定义为一个int,那么就不存在转化的问题了。
三、Two’s complement arithmetic
正像先前看到的,add操作码执行的是加的操作,sub操作码执行的是减的操作。在这两个操作中
会用到两个flag寄存器,一个是溢出标示(overflow flag),一个是进位标示(carry flag)。
在有符号算法中,当结果太大超出目标范围的时候,overflow flag会被设置为true。而carry flag
在加操作的进位和减操作的借位运算时将会被用到。所以carry flag可以用来监测无符号数是否溢出。
对于Two’s complement算法,add和sub操作对于无符号数和有符号数规则是相同的,因此它们可以用于
有符号数和无符号数:
002C 44
+ FFFF + (-1)
------- ------
002B 43
上面的例子是两个有符号数的加操作.产生了一个进位.但这个进位是丢丢到的。
但是这里却有两个乘和两个除操作。两个乘操作是:MUL or IMUL。MUL是用来进行两个无符号数
的乘操作。而IMUL是用来进行两个有符号数的乘操作。为什么会需要两个操作呢?那是因为对于2’s complement
算法,有符号数和无符号数的乘法规则是不同的。为什么会这样呢?考虑一下以下的乘法:FF * FF,用无符号数
的乘法结果应该是255 * 255 = 65025(FE01 十六进制)。如果FF是个有符号数那么结果为:-1 * -1 = 1(0001 十六进制);
对于乘法的操作码,有很多种形式:
mul source
这个是最古老的一种形式,source可以是一个寄存器,也可一个是一个内存的指针。但确不能是
立即数。确切的讲mul执行什么样的操作依赖于source操作数的大小。如果操作数是一个字节大小,那么操作数
就会乘上AL中的数值并把结果保存在AX中。如果source是十六位的那么操作数就会乘上AX中的值并把结果
保存到DX:AX中。如果source是32位的那么操作数会乘上EAX中的数值并把结果保存到EDX:ECX中。
imul操作码和mul有着相同的形式,但是还有其它两种形式:
imul dest, source1
imul dest, source1, source2
------------------------------------------------------------- |
dest source1 source2 Action |
reg/mem8 AX = AL*source1 |
reg/mem16 DX:AX = AX*source1 |
reg/mem32 EDX:EAX = EAX*source1 |
reg16 reg/mem16 dest *= source1 |
reg32 reg/mem32 dest *= source1 |
reg16 immed8 dest *= immed8 |
reg32 immed8 dest *= immed8 |
reg16 immed16 dest *= immed16 |
reg32 immed32 dest *= immed32 |
reg16 reg/mem16 immed8 dest = source1*source2|
reg32 reg/mem32 immed8 dest = source1*source2|
reg16 reg/mem16 immed16 dest = source1*source2|
reg32 reg/mem32 immed32 dest = source1*source2|
---------------------------------------------------------------
上图列出了可能的组合;
相应的也有两种除法操作:DIV和IDIV.它们分别作无符号数和有符号数的除法操作。我们来看一下
DIV source
1.source是8位的,那么AX除以source中的值。除数被放在AL寄存器中.剩余数被放在AH中:例如
7 / 3 那么AL的值为2,AH的值为1。
2.source是16位的:那么DX:AX中的值除以source,结果是除数被放在AX中,余数被放在AX中
3.source是32位的: 那么EDX:EAX中的值除以source.除数放在EAX中,余数放在EDX中
IDIV操作码和DIV是相同的,它不同于IMUL有那么多的形式。如果除数太大或者被除数为0那么程序就会
报错终止。一个常见的错误是在进行除法前忘记初始化DX或者EDX的值。
NEG操作码是用Two’s complement算法计算符号数的否定操作。它的操作数可以为8,16,32位的寄存器
或者内存数