本文由zengl.com站长对
http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
另外再附加一个英特尔英文手册的共享链接地址:
http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)
本篇翻译对应汇编教程英文原著的第252页到第258页,对应原著第8章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为258/ 577)。
Division 除法运算:
类似于乘法运算,除法运算中无符号和有符号数的除法指令也是不同的。另外,除法运算的结果分为两部分:商和余数,下面就无符号整数和有符号整数的除法指令分别进行介绍。
Unsigned division 无符号数的除法运算:
无符号数使用DIV指令进行除运算,DIV指令的格式如下:
上面格式中的divisor操作数表示除数,dividend被除数是隐示存储在AX或DX : AX或EDX : EAX中的。
当被除数位于16位AX寄存器时,divisor除数就必须是8位大小,当被除数位于32位的DX : AX寄存器组时,除数就必须是16位的大小,当被除数位于64位的EDX : EAX寄存器组时,除数就必须是32位大小。(所以简单来说,就是除数的位尺寸必须是被除数的位尺寸的一半)
除法运算的结果:商和余数存储在被除数所在的寄存器里,所以结果会覆盖掉寄存器中原来的被除数。
下表显示了除法运算里被除数,商和余数所在的寄存器情况:
Dividend
被除数 |
Dividend Size
被除数的位尺寸 |
Quotient
商 |
Remainder
余数 |
AX |
16 位 |
AL |
AH |
DX:AX |
32 位 |
AX |
DX |
EDX:EAX |
64 位 |
EAX |
EDX |
|
上表显示了寄存器里的被除数会被结果覆盖,所以在使用除法指令之前,如果被除数还有用的话,最好在内存中留个备份。
下面的divtest.s程式就演示了DIV指令的用法:
# divtest.s - An example of the DIV instruction
.section .data
dividend:
.quad 8335
divisor:
.int 25
quotient:
.int 0
remainder:
.int 0
output:
.asciz "The quotient is %d, and the remainder is %d\n"
.section .text
.globl _start
_start:
nop
movl dividend, %eax
movl dividend+4, %edx
divl divisor
movl %eax, quotient
movl %edx, remainder
pushl remainder
pushl quotient
pushl $output
call printf
add $12, %esp
pushl $0
call exit
|
上面代码中,在dividend标签处使用.quad定义了一个64位的被除数8335,在divisor标签处使用.int定义了32位的除数25,在div指令之前,先使用mov指令将dividend里的被除数的低32位存储到EAX,高32位存储到EDX,divl divisor执行完除法运算后,再使用mov指令将EAX里的商存储到quotient内存位置,将EDX里的余数存储到remainder内存位置,最后通过printf函数将结果显示出来。
下面是程序执行时的输出结果:
$ as -gstabs -o divtest.o divtest.s
$ ld -dynamic-linker /lib/ld-linux.so.2 -lc -o divtest divtest.o
$ ./divtest
The quotient is 333, and the remainder is 10
$ |
这里要注意的是,DIV指令的被除数和除数都是无符号整数,代码中不要使用负数进行DIV运算,例如,如果将上面代码里的dividend设为-8335,那么在linux下就会产生Floating point exception浮点异常,这是因为这个负数在DIV指令看来是一个很大的无符号数,那么除运算的结果商就会超过EAX寄存器可以容纳的范围,就会发生浮点异常。
Signed division 有符号数的除法运算:
有符号数是使用IDIV指令来进行除法运算的,IDIV指令的格式如下:
divisor除数可以是8位,16位或32位的寄存器或内存里的值,同时和DIV指令一样,被除数也是隐示存储在AX或DX : AX或EDX : EAX中的。
IDIV的结果也和DIV一样是覆盖掉被除数所在的寄存器的,只不过结果中,商和余数可以是负数。
小提示:对于IDIV有符号除法运算,得到的结果中,余数的正负号总是和被除数的正负号保持一致的。
另外,被除数的位尺寸也必须是除数位尺寸的两倍,如果不是两倍将会产生错误的结果,所以有时候需要使用MOVSX之类的指令将被除数扩展到除数两倍的大小,再进行运算,你可以将上面的divtest.s例子里的dividend设为-8335,然后将代码段中的divl指令改为idivl指令,就可以查看到正确的结果,div和idiv指令都需要在助记符后面添加位尺寸符如divl后面的l字符,以表示操作数的大小。
Detecting division errors 检测除法错误:
当除法运算中,除数为零时,或者除运算的结果里,商或余数溢出了目标寄存器的大小时,就会发生错误,在linux系统下会抛出浮点异常,例如,可以将前面的divtest.s程式里的divisor除数设为0,然后程序运行时,就会出现下面的异常:
$ ./divtest
Floating point exception (core dumped)
$ |
在执行DIV和IDIV指令之前,你需要自己编写代码来检查除数和被除数,如果不做检查,则结果就有可能会发生错误。
Shift Instructions 移位指令:
乘法和除法在处理器中是两个非常耗时的操作,我们可以使用移位指令来加快某些乘除运算,例如12 = 6 * 2 ,其中12的二进制格式为 1100 相当于6的二进制 0110 中所有位左移一位得来,反过来,6 = 12 / 2 这个除法运算的结果6,可以看成是12的二进制1100所有位右移一位得来,因此在这种以2为倍数的场合就可以用移位指令来代替乘除运算,移位运算就比乘除运算快多了。
下面就介绍和移位运算相关的指令。
Multiply by shifting 左移指令:
二进制左移运算有两个指令:SAL (算术左移) 和 SHL (逻辑左移) ,这两个指令执行的操作完全一样,可以互换使用,之所以左移弄出两条功能一样的指令出来,是为了和右移的那两条指令相对应,在右移时,算术右移和逻辑右移的作用就不一样了,为了和右移指令一一对应,所以就有了算术左移和逻辑左移,只要记住在左移时,两条指令是相同的即可。
左移指令有三个格式:
sal destination
sal %cl, destination
sal shifter, destination |
第一个格式是将destination目标操作数里的二进制左移一位,相当于将它的值乘以2。
第二个格式是将destination根据CL寄存器里的值来进行左移,例如当CL里的值是3时,就左移3位,相当于乘以2的3次方即乘以8,以此类推。
最后一个格式是将destination根据shifter立即数的值来进行左移。
这三个格式中的destination目标操作数可以是8位,16位,32位的寄存器或某内存位置里的值。
移位指令也需要在助记符后面添加一个位尺寸字符来表示操作数的大小,例如b来表示8位字节大小,w来表示16位字大小,以及l来表示32位的双字大小。
左移指令既可以用于有符号整数又可以用于无符号整数。左移时最低位用0填充,最高位则移入carry进位标志里,以此类推,如下图所示:
图1
下面的saltest.s程式就演示了SAL指令的用法:
# saltest.s - An example of the SAL instruction
.section .data
value1:
.int 25
.section .text
.globl _start
_start:
nop
movl $10, %ebx
sall %ebx
movb $2, %cl
sall %cl, %ebx
sall $2, %ebx
sall value1
sall $2, value1
movl $1, %eax
movl $0, %ebx
int $0x80
|
上面的代码演示了SAL指令的所有格式,代码主要对EBX和value1里的值进行移位操作,在所有移位指令执行完后,在调试器中得到的结果如下:
(gdb) info reg
eax 0x0 0
ecx 0x2 2
edx 0x0 0
ebx 0x140 320
.........................
(gdb) x/d &value1
0x804909c <value1>: 200
(gdb) |
saltest程式中先将EBX设置为10,接着sall %ebx将其左移一位,这样EBX就变为20,再将CL计数器设为2,sall %cl, %ebx指令就会将EBX里的20根据CL里的值向左移2位,相当于乘以4,得到EBX里的值为20 * 4 = 80 ,最后使用立即数格式:sall $2, %ebx,再将EBX里的值80左移2位,最终得到的EBX值就是80 * 2 * 2 = 320 ,value1里的值一开始是25,sall value1将其左移一位,接着sall $2, value1再将其左移2位,结果就是25 * 2 * 2 * 2 = 200 ,调试输出的结果和预期的一致。
Dividing by shifting 右移指令:
右移指令和左移指令方向刚好相反,可以产生一个除2的效果,不过二进制在右移时需要注意整数的最高符号位,对于无符号整数,右移后最高位填充为0,不会有影响,然而对于有符号数,主要是负数,最高位一开始是1,右移后,如果最高位用0填充,则整数的符号性质就被改变了。
为了解决这个问题,右移也对应有两个指令:SHR (逻辑右移) 和 SAR (算术右移),只不过这两个指令的作用是不一样的,不能像左移指令那样可以互换着用,SHR逻辑右移时会将最高位始终用0填充,所以SHR逻辑右移只能用于无符号整数,SAR算术右移则会根据整数的符号性质来决定是用0还是用1来填充最高位,当对正数进行SAR算术右移时,最高位就用0填充,当对负数进行SAR右移时,最高位就用1填充,这样算术右移后,整数的符号性质就不会被改变。
右移指令的最低位在移出后,会自动移入carry进位标志中,再次右移时,之前移入carry标志的值就会被之后移入的二进制给覆盖掉,如下图所示:
图2
Rotating bits 循环移位指令:
循环移位指令类似于前面的移位指令,只不过移出去的位不会丢失,而是进入另一端,例如0111循环左移一位后,最高位0就会进入另一端即最低位,从而变为1110,这个1110再循环左移一位,则最高位1也会循环到最低位,从而变为1101,以此类推,循环右移则方向刚好相反。
下表显示了可用的循环移位指令:
Instruction
指令 |
Description
描述 |
ROL |
Rotate value left
循环左移指令 |
ROR |
Rotate value right
循环右移指令 |
RCL |
Rotate left and include carry flag
带进位标志的循环左移 |
RCR |
Rotate right and include carry flag
带进位标志的循环右移 |
|
最后两个RCL和RCR指令将carry进位标志也加入到了循环移位里面,例如假设某个数是1101,且此时carry标志位为0,则RCL带进位循环左移指令执行时,carry里的0就会进入该数的最低位,而该数的最高位1则移入carry标志,这样得到的结果就是1010,且得到的carry标志位为1 。
这些循环移位指令也是三个格式:
rol destination
rol %cl, destination
rol shifter, destination |
第一个格式将destination循环移动一位,第二个格式将destination的值根据CL里的值来进行循环移位,最后一个格式就是根据shifter立即数的值来对destination进行循环移位。
本篇就到这里,下一篇介绍BCD码的运算和一些逻辑运算指令,转载请注明来源:www.zengl.com
OK,休息,休息一下 o(∩_∩)o~~