《汇编语言:基于X86处理器》第7章 整数运算(2)
本章将介绍汇编语言最大的优势之一:基本的二进制移位和循环移位技术。实际上,位操作是计算机图形学、数据加密和硬件控制的固有部分。实现位操作的指令是功能强大的工具,但是高级语言只能实现其中的一部分,并且由于高级语言要求与平台无关,所以这些指令在一定程度上被弱化了。本章将展示一些对移位操作的应用,包括乘除法的优化。
并非所有的高级编程语言都支持任意长度整数的运算。但是汇编语言指令使得它能够加减几乎任何长度的整数。本章还将介绍执行压缩十进制整数和整数字符串运算的专用指令。
7.3 乘法和除法指令
32位模式下,整数乘法可以实现32位、16位或8位的操作。64位模式下,还可以使用64位操作数。MUL和IMUL指令分别执行无符号数和有符号数乘法。DIV指令执行无符号数除法,IDIV指令执行有符号数除法。
7.3.1 MUL 指令
32位模式下,MUL(无符号数乘法)指令有三种类型:第一种执行8位操作数与AL寄存器的乘法;第二种执行16位操作数与AX寄存器的乘法:第三种执行32位操作数与EAX寄存器的乘法。乘数和被乘数的大小必须保持一致,乘积的大小则是它们的一倍。这三种类型都可以使用寄存器和内存操作数,但不能使用立即数:
MUL reg/mem8
MUL reg/mem16
MUL reg/mem32
MUL指令中的单操作数是乘数。表7-2按照乘数的大小,列出了默认的被乘数和乘积。由于目的操作数是被乘数和乘数大小的两倍,因此不会发生溢出。如果乘积的高半部分不为零,则MUL会把进位标志位和溢出标志位置1。因为进位标志位常常用于无符号数的算术运算,在此我们也主要说明这种情况。例如,当AX乘以一个16 位操作数时,乘积存放在 DX和 AX寄存器对中。其中,乘积的高16位存放在DX,低16位存放在AX。如果DX不等于零则进位标志位置1,这就意味着隐含的目的操作数的低半部分容纳不了整个乘积。
有个很好的理由要求在执行MUL后检查进位标志位,即,确认忽略乘积的高半部分是否安全。
1.MUL 示例
下述语句实现AL乘以BL,乘积存放在AX中。由于AH(乘积的高半部分)等于零因此进位标志位被清除(CF=0):
mov al, 5h
mov bl, 10h
mul bl ;AX = 0050h, CF = 0
下图展示了寄存器内容的变化:
下述语句实现16位值2000h乘以0100h。由于乘积的高半部分(存放于DX)不等于委,因此进位标志位被置1:
.data
val1 WORD 2000h
val2 WORD 0100h
.code
mov ax, val1 ;AX = 2000h
mul val2 ;DX:AX = 00200000h, CF = 1
下述语句实现12345h乘以1000h,产生的64位乘积存放在EDX和EAX寄存器对中。EDX中存放的乘积高半部分为零,因此进位标志位被清除:
mov eax, 12345h
mov ebx, 1000h
mul ebx ;EDX:EAX = 0000000012345000h, CF = 0
下图展示了寄存器内容的变化:
完整代码测试笔记
;7.3.1_1.asm 7.3.1 MUL 指令INCLUDE Irvine32.inc.data
val1 WORD 2000h
val2 WORD 0100h.code
main PROC;8位乘法,乘积存放在AX中。由于AH(乘积的高半部分)等于零因此进位标志位被清除(CF=0):mov al, 5hmov bl, 10hmul bl ;AX = 0050h, CF = 0;16位乘法。由于乘积的高半部分(存放于DX)不等于委,因此进位标志位被置1:mov ax, val1 ;AX = 2000hmul val2 ;DX:AX = 00200000h, CF = 1;32位乘法,产生的64位乘积存放在EDX和EAX寄存器对中。EDX中存放的乘积高半部分为零,因此进位标志位被清除:mov eax, 12345hmov ebx, 1000hmul ebx ;EDX:EAX = 0000000012345000h, CF = 0INVOKE ExitProcess,0
main ENDP
END main
运行调试:
16位乘法:
32位乘法:
2.在 64 位模式下使用 MUL
64位模式下,MUL指令可以使用64位操作数。一个64位寄存器或内存操作数与RAX相乘,产生的128位乘积存放到RDX:RAX寄存器中。下例中,RAX乘以2,就是将RAX中的每一位都左移一位。RAX的最高位溢出到RDX寄存器,使得RDX的值为0000 0000 0000 0001h:
mov rax, 0FFFF0000FFFF0000h
mov rbx, 2
mul rbx ;RDX:RAX = 0000000000000001FFFE0001FFFE0000h
下面的例子中,RAX乘以一个64位内存操作数。该寄存器的值乘以16,因此,其中的每个十六进制数字都左移一位(一次移动4个二进制位就相当于乘以16)。
.data
multiplier QWORD 10h
.code
mov rax, 0AABBBBCCCCDDDDh
mul multiplier ;RDX:RAX=00000000000000000AABBBBCCCCDDDD0h
完整代码测试笔记
;7.3.1_2.asm 7.3.1 MUL 指令
;2.在 64 位模式下使用 MULExitProcess PROTO.data
multiplier QWORD 10h.code
main PROC;64位乘法,产生的128位乘积存放到RDX:RAX寄存器中。mov rax, 0FFFF0000FFFF0000hmov rbx, 2mul rbx ;RDX:RAX = 0000000000000001FFFE0001FFFE0000h;下面的例子中,RAX乘以一个64位内存操作数。该寄存器的值乘以16,;因此,其中的每个十六进制数字都左移一位(一次移动4个二进制位就相当于乘以16)。mov rax, 0AABBBBCCCCDDDDhmul multiplier ;RDX:RAX=00000000000000000AABBBBCCCCDDDD0hcall ExitProcess
main ENDP
END
运行调试:
7.3.2 IMUL指令
IMUL(有符号数乘法)指令执行有符号整数乘法。与MUL指令不同,IMUL会保留乘积的符号,实现的方法是,将乘积低半部分的最高位符号扩展到高半部分。x86指令集支持三种格式的IMUL 指令:单操作数、双操作数和三操作数。单操作数格式中,乘数和被乘数大小相同,而乘积的大小是它们的两倍。
单操作数格式 单操作数格式将乘积存放在AX、DX:AX或EDX:EAX中:
IMUL reg/mem8 ;AX=A *reg/mem8
IMUL reg/mem16 ;DX:AX=X*reg/mem16
IMUL reg/mem32 ;EDX:EAX=X*reg/mem32
和 MUL指令一样,其乘积的存储大小使得溢出不会发生。同时,如果乘积的高半部分不是其低半部分的符号扩展,则进位标志位和溢出标志位置1。利用这个特点可以决定是否忽略乘积的高半部分。
双操作数格式(32 位模式) 32位模式中的双操作数IMUL指令把乘积存放在第一个作数中,这个操作数必须是寄存器。第二个操作数(乘数)可以是寄存器、内存操作数和立即数。16位格式如下所示:
IMUL regl6,reg/mem16
IMUL reg16,imm8
IMUL regl6,imm16
32位操作数类型如下所示,乘数可以是32位寄存器、32位内存操作数或立即数(8位或32位):
IMULreg32,reg/mem32
IMULreg32,imm8
IMULreg32,imm32
双操作数格式会按照目的操作数的大小来截取乘积。如果被丢弃的是有效位,则溢出标志位和进位标志位置1。因此,在执行了有两个操作数的IMUL操作后,必须检查这些标志位中的一个。
三操作数格式 32位模式下的三操作数格式将乘积保存在第一个操作数中。第二个操作数可以是16位寄存器或内存操作数,它与第三个操作数相乘,该操作数是一个8位或16位立即数:
IMUL reg16, reg/meml6, imm8
IMUL reg16, reg/meml6, imm16
而32位寄存器或内存操作数可以与8位或32位立即数相乘:
IMUL reg32, reg/mem32, imm8
IMUL reg32, reg/mem32, imm32
IMUL执行时,若乘积有效位被丢弃,则溢出标志位和进位标志位置1。因此,在执行了有三个操作数的IMU操作后,必须检查这些标志位中的一个。
1.在 64 位模式下执行 IMUL
在64位模式下,IMUL指令可以使用64位操作数。在单操作数格式中,64位寄存器或内存操作数与RAX相乘,产生一个128位目符号扩展的乘积存放到RDX:RAX寄存器中在下面的例子中,RBX与RAX相乘,产生128位的乘积-16。
mov rax, -4
mov rbx, 4
imul rbx ;RDX=0FFFFFFFFFFFFFFFFh, RAX = -16
也就是说,十进制数-16在RAX中表示为十六进制FFFFFFFFFFFO,而RDX只包含了RAX的高位扩展,即它的符号位。
三操作数格式也可以用于64位模式。如下例所示,被乘数(-16)乘以4,生成RAX中的乘积-64:
.data
multiplicand QWORD -16
.code
imul rax, multiplicand, 4 ;RAX=0FFFFFFFFFFFFFC0 (-64)
无符号乘法 由于有符号数和无符号数乘积的低半部分是相同的,因此双操作数和三操作数的IMUL指令也可以用于无符号乘法。但是这种做法也有一点不便的地方:进位标志位和溢出标志位将无法表示乘积的高半部分是否为零。
完整代码测试笔记
;7.3.2_1.asm 7.3.2 IMUL指令
;1.在 64 位模式下执行 IMULExitProcess PROTO.data
multiplicand QWORD -16.code
main PROC;单操作数乘法mov rax, -4mov rbx, 4imul rbx ;RDX=0FFFFFFFFFFFFFFFFh, RAX = -16;3操作数乘法, 第1个操作数用来保存积的,第2个和第3个操作数才参与乘积计算imul rax, multiplicand, 4 ;RAX=0FFFFFFFFFFFFFC0 (-64)call ExitProcess
main ENDP
END
运行调试:
2.IMUL示例
下述指令执行48乘以4,乘积+192保存在AX中。虽然乘积是正确的,但是 AH不是AL的符号扩展,因此溢出标志位置1:
mov al, 48
mov bl, 4
imul bl ;AX=00C0h,OF=1
下述指令执行-4乘以4,乘积-16保存在AX中。AH是AL的符号扩展,因此溢出标志位清零:
mov al, -4
mov bl, 4
imul bl ;AX=FFF0h,OF=0
下述指令执行48乘以4,乘积+192保存在DX:AX中。DX是 AX的符号扩展,因此溢出标志位清零:
mov ax, 48
mov bx, 4
imul bx ;DX:AX=000000c0h,0F=0
下述指令执行32位有符号乘法(4823424*-423),乘积-2040308352保存在 EDX:EAX中。溢出标志位清零,因为EDX是EAX的符号扩展:
mov eax, +4823424
mov ebx, -423
imul ebx ;EDX:EAX=FFFFF86635D80h,OF=0
下述指令展示了双操作数格式
.data
word1 SWORD 4
dword1 SDWORD 4
.code
mov ax, -16 ;AX=-16
mov bx, 2 ;BX =2
imul bx, ax ;BX =-32
imul bx, 2 ;BX =-64
imul bx, word1 ;BX=-256
mov eax, -16 ;EAX =-16
mov ebx, 2 ;EBX =2
imul ebx, eax ;EBX=-32
imul ebx, 2 ;EBX=-64
imul ebx, dword1 ;EBX=-256
双操作数和三操作数IMUL指令的目的操作数大小与乘数大小相同。因此,有可能发生有符号溢出。执行这些类型的IMUL指令后,总要检查溢出标志位。下面的双操作数指令展示了有符号溢出,因为-64000不适合16位目的操作数:
mov ax, -32000
imul ax, 2 ;OF=1
下面的指令展示的是三操作数格式,包括了有符号溢出的例子:
.data
word1 SWORD 4
dword1 SDWORD 4
.code
imul bx, word1, -16 ;BX=wordl *-16
imul ebx, dword1, -16 ;EBX=dwordl*-16
imul ebx, dword1, -2000000000 ;有符号溢出!
完整代码测试笔记
;7.3.2_2.asm 7.3.2 IMUL乘法INCLUDE Irvine32.inc.data
word1 SWORD 4
dword1 SDWORD 4.code
main PROC;下述指令执行48乘以4,乘积+192保存在AX中。 有溢出mov al, 48mov bl, 4imul bl ;AX=00C0h,OF=1;下述指令执行-4乘以4,乘积-16保存在AX中。无溢出mov al, -4mov bl, 4imul bl ;AX=FFF0h,OF=0;下述指令执行48乘以4,乘积+192保存在DX:AX中。无溢出mov ax, 48mov bx, 4imul bx ;DX:AX=000000c0h,0F=0;下述指令执行32位有符号乘法(4823424*-423),乘积-2040 308 352保存在 EDX:EAX中。溢出标志位清零,因为EDX是EAX的符号扩展:mov eax, +4823424mov ebx, -423imul ebx ;EDX:EAX=FFFFF86635D80h,OF=0;下述指令展示了双操作数格式mov ax, -16 ;AX=-16mov bx, 2 ;BX =2imul bx, ax ;BX =-32imul bx, 2 ;BX =-64imul bx, word1 ;BX=-256mov eax, -16 ;EAX =-16mov ebx, 2 ;EBX =2imul ebx, eax ;EBX=-32imul ebx, 2 ;EBX=-64imul ebx, dword1 ;EBX=-256;下面的双操作数指令展示了有符号溢出,因为-64000不适合16位目的操作数:mov ax, -32000imul ax, 2 ;OF=1;下面的指令展示的是三操作数格式,包括了有符号溢出的例子,imul bx, word1, -16 ;BX=wordl *-16imul ebx, dword1, -16 ;EBX=dwordl*-16imul ebx, dword1, -2000000000 ;有符号溢出!INVOKE ExitProcess,0
main ENDP
END main
7.3.3 测量程序执行时间
通常,程序员发现用测量执行时间的方法来比较一段代码与另一段代码执行的性能是很有用的。Microsoft Windows API为此提供了必要的工具,Irvine32库中的GetMseconds 过程可使其变得更加方便使用。该过程获取系统自午夜过后经过的毫秒数。在下面的代码示例中,首先调用GetMseconds,这样就可以记录系统开始时间。然后调用想要测量其执行时间的过程(FirstProcedureToTest)。最后,再次调用GetMseconds,计算开始时间和当前毫秒数的差值:
.data
startTime DWORD
procTimel DWORD2
procTime2 DWORD?
.code
call GetMseconds ;获得开始时间
mov startTime,eax
.
call FirstProcedureToTest
.
call GetMseconds ;获得结束时间
sub eax,startTime ;计算执行花费的时间
mov procTimel,eax ;保存执行花费的时间
当然,两次调用GetMseconds会消耗一点执行时间。但是在衡量两个代码实现的性能时间之比时,这点开销是微不足道的。现在,调用另一个被测试的过程,并保存其执行时间(procTime2 ):
.code
call GetMseconds ;获得开始时间
mov startTime,eax
.
call SecondProcedureToTest
.
call GetMseconds ;获得结束时间
sub eax,startTime ;计算执行花费的时间
mov procTime2,eax ;保存执行花费的时间
则procTimel和procTime2的比值就可以表示这两个过程的相对性能,
MUL、IMUL与移位的比较
对老的x86处理器来说,用移位操作实现乘法和用MUL、IMUL指令实现乘法之间有着明显的性能差异。可以用 GetMseconds 过程比较这两种类型乘法的执行时间。下面的两个过程重复执行乘法,用常量LOOPCOUNT决定重复的次数:
;7.3.3.asm 7.3.3 测量程序执行时间INCLUDE Irvine32.inc.data
intval DWORD 5
startTime DWORD ?
LOOP_COUNT = 0FFFFFFFFh.code
;------------------------------------------------------------
;用SHL执行EAX乘以36,执行次数为LOOP_COUNT
;-------------------------------------------------------------
mult_by_shifting PROCmov ecx, LOOP_COUNT
L1: push eax ;保存原始EAXmov ebx, eaxshl eax, 5shl ebx, 2add eax, ebxpop eaxloop L1 ;恢复EAXret
mult_by_shifting ENDP;---------------------------------------------------------------
;用MUL执行EAX乘以36,执行次数为LOOP_COUNT
;---------------------------------------------------------------
mult_by_MUL PROCmov ecx, LOOP_COUNT
L1: push eax ;保存原始 EAXmov ebx, 36mul ebxpop eax ;恢复EAXloop L1ret
mult_by_MUL ENDP
;下述代码调用multibyshifting,并显示计时结果。完整的代码实现参见本书第7章的CompareMult.asm程序:
main PROC;第1次调用call GetMseconds ;获取开始时间mov startTime, eaxmov eax, intval ;开始乘法call mult_by_shiftingcall GetMseconds ;获取结束时间sub eax, startTimecall WriteDec ;显示乘法执行花费的时间call Crlf ;换行;第2次调用call GetMseconds ;获取开始时间mov startTime, eaxmov eax, intval ;开始乘法call mult_by_MULcall GetMseconds ;获取结束时间sub eax, startTimecall WriteDec ;显示乘法执行花费的时间INVOKE ExitProcess,0
main ENDPEND main
运行调试:
intel CORE i5处理器测试效果, mul指令比移位指令快。下面是书上的结论:
用同样的方法调用mult_by_MUL,在传统的4GHz奔腾4处理器上运行的结果为:SHL方法执行时间是6.078秒,MUL方法执行时间是20.718秒。也就是说,使用MUL指令速度会慢 2.41倍。但是,在近期的处理器上运行同样的程序,调用两个函数的时间是完全一样的。这个例子说明,Intel在近期的处理器上已经设法大大地优化了MUL和IMUL指令。
7.3.4 DIV 指令
32位模式下,DIV(无符号除法)指令执行8位、16位和32位无符号数除法。其中单寄存器或内存操作数是除数。格式如下:
DIV reg/mem8
DIV reg/mem16
DIV reg/mem32
下表给出了被除数、除数、商和余数之间的关系,
64 位模式下,DIV指令用RDX:RAX作被除数,用64位寄存器和内存操作数作除数商存放到RAX,余数存放在RDX中。
DIV 示例
下述指令执行8位无符号除法(83h/2),生成的商为41h,余数为1:
mov ax, 0083h ;被除数
mov bl, 2 ;除数
div bl ;AL= 41h, AH=01h
下图展示了寄存器内容的变化:
下述指令执行16位无符号除法(8003h/100h),生成的商为80h,余数为3。DX包含的是被除数的高位部分,因此在执行 DIV指令之前,必须将其清零:
mov dx, 0 ;清除被除数高16位
mov ax, 8003h ;被除数的低16位
mov cx, 100h ;除数
div cx ;AX=0080h,DX= 0003h
下图展示了寄存器内容的变化:
下述指令执行32位无符号除法,其除数为内存操作数:
.data
dividend QWORD 0000000800300020h
divisor DWORD 00000100h
.code
mov edx, DWORD PTR dividend + 4 ;高双字
mov eax, DWORD PTR dividend ;低双字
div divisor ;EAX=08003000h,EDX=00000020h
下图展示了寄存器内容的变化:
下面的64位除法生成的商(0108000000003330h)在RAX中,余数(0000000000000020h)在RDX中:
.data
dividend_hi QWORD 0000000000000108h
dividend_lo QWORD 0000000033300020h
divisor QWORD 0000000000010000h
.code
mov rdx, dividend_hi
mov rax, dividend_lo
div divisor ;RAX=0108000000003330;RDX=0000000000000020
请注意,由于被64k除,被除数中的每个十六进制数字是如何右移4位的。(若被16除,则每个数字只需右移一位。)
完整代码测试笔记:
;7.3.4.asm 7.3.4 DIV 指令 (64位环境)ExitProcess PROTO.data
dividend QWORD 0000000800300020h
divisor DWORD 00000100h
dividend_hi QWORD 0000000000000108h
dividend_lo QWORD 0000000033300020h
divisor_64b QWORD 0000000000010000h.code
main PROC;下述指令执行8位无符号除法(83h/2),生成的商为41h,余数为1:mov ax, 0083h ;被除数mov bl, 2 ;除数div bl ;AL= 41h, AH=01h;下述指令执行16位无符号除法(8003h/100h),生成的商为80h,余数为3。mov dx, 0 ;清除被除数高16位mov ax, 8003h ;被除数的低16位mov cx, 100h ;除数div cx ;AX=0080h,DX= 0003h;下述指令执行32位无符号除法,其除数为内存操作数:mov edx, DWORD PTR dividend + 4 ;高双字mov eax, DWORD PTR dividend ;低双字div divisor ;EAX=08003000h,EDX=00000020h;下面的64位除法生成的商(0108000000003330h)在RAX中,余数(0000000000000020h)在RDX中:mov rdx, dividend_himov rax, dividend_lodiv divisor_64bcall ExitProcess
main ENDPEND
7.3.5 有符号数除法
有符号除法几乎与无符号除法相同,只有一个重要的区别:在执行除法之前,必须对被除数进行符号扩展。符号扩展是指将一个数的最高位复制到包含该数的变量或寄存器的所有高位中。为了说明为何有此必要,让我们先不这么做。下面的代码使用MOV把-101赋给AX,即 DX:AX的低半部分:
.data
wordVal SWORD -101 ;009Bh
.code
mov dx, 0
mov ax, wordVal ;DX:AX = 0000009Bh (+155)
mov bx, 2 ;BX是除数
idiv bx ;DX:AX除以BX(有符号操作)
可惜的是,DX:AX中的009Bh并不等于-101,它等于+155。因此,除法产生的商为+77,这不是所期望的结果。而解决该问题的正确方法是使用CWD(字转双字)指令,在进行除法之前在DX:AX中对AX进行符号扩展:
.data
wordVal SWORD -101 ;009Bh
.code
mov dx, 0
mov ax, wordVal ;DX:AX = 0000009Bh (+155)
cwd ;DX:AX = FFFFFF9Bh (-101)
mov bx, 2 ;BX是除数
idiv bx ;DX:AX除以BX(有符号操作)
AX为-52h的补, DX为-1的补码
本书第4章与MOVSX指令一起介绍过符号扩展。x86指令集有几种符号扩展指令。首先了解这些指令,然后再将其应用到有符号除法指令IDIV中。
完整代码测试笔记
;7.3.5.asm 7.3.5 有符号数除法INCLUDE Irvine32.inc.data
wordVal SWORD -101 ;009Bh.code
main PROC;下面的代码使用MOV把-101赋给AX,即 DX:AX的低半部分:mov dx, 0mov ax, wordVal ;DX:AX = 0000009Bh (+155)mov bx, 2 ;BX是除数idiv bx ;DX:AX除以BX(有符号操作);在进行除法之前在DX:AX中对AX进行符号扩展:mov dx, 0mov ax, wordVal ;DX:AX = 0000009Bh (+155)cwd ;DX:AX = FFFFFF9Bh (-101)mov bx, 2 ;BX是除数idiv bx ;DX:AX除以BX(有符号操作)INVOKE ExitProcess,0
main ENDPEND main
1.符号扩展指令(CBW、CWD、CDQ)
Intel提供了三种符号扩展指令:CBW、CWD和CDO。CBW(字节转字)指令将AL的符号位扩展到 AH,保留了数据的符号。如下例所示,9Bh(AL中)和FF9Bh(AX中)都等于十进制的 -101:
.data
byteVal SBYTE -101 ;9Bh
.code
mov al, byteVal ;AL = 9Bh
cbw ;AX = FF9Bh
CWD(字转双字)指令将AX的符号位扩展到DX:
.data
byteVal SWORD -101 ;FF9Bh
.code
mov ax, byteVal ;AX = FF9Bh
cwd ;DX:AX = FFFFFF9Bh
CDQ(双字转四字)指令将EAX的符号位扩展到EDX:
.data
byteVal SDWORD -101 ;FFFFFF9Bh
.code
mov eax, byteVal
cdq ;EDX:EAX = FFFFFFFFFFFFFF9Bh
完整代码测试笔记
;7.3.5_1.asm 7.3.5 有符号数除法
;1.符号扩展指令(CBW、CWD、CDQ)INCLUDE Irvine32.inc.data
byteVal SBYTE -101 ;9Bh
wordVal SWORD -101 ;FF9Bh
dwordVal SDWORD -101 ;FFFFFF9Bh.code
main PROC;CBW(字节转字)指令将AL的符号位扩展到 AH,保留了数据的符号。mov al, byteVal ;AL = 9Bhcbw ;AX = FF9Bh;CWD(字转双字)指令将AX的符号位扩展到DX:mov ax, wordVal ;AX = FF9Bh (+155)cwd ;DX:AX = FFFFFF9Bh (-101);CDQ(双字转四字)指令将EAX的符号位扩展到EDX:mov eax, dwordValcdq ;EDX:EAX = FFFFFFFFFFFFFF9BhINVOKE ExitProcess,0
main ENDPEND main
2.IDIV 指令
IDIV(有符号除法)指令执行有符号整数除法,其操作数与DIV指令相同。执行8位除法之前,被除数(AX)必须完成符号扩展。余数的符号总是与被除数相同。
示例1 下述指令实现-48除以5。IDIV执行后,AL中的商为-9,AH 中的余数为 -3:
.data
byteVal SBYTE -48 ;D0 十六进制
.code
mov al, byteVal ;被除数的低字节
cbw ;AL扩展到AH
mov bl, +5 ;除数
idiv bl ;AL=-9,AH=-3
下图展示了AL是如何通过CBW指令符号扩展为AX的:
为了理解被除数的符号扩展为什么这么重要,现在在不进行符号扩展的前提下重复之前的例子。下面的代码将AH初始化为0,这样它就有了确定值,然后没有用CBW指令转换被除数就直接进行了除法:
.data
byteVal SBYTE -48 ;D0 十六进制
.code
mov ah, 0 ;被除数高字节
mov al, byteVal ;被除数低字节
mov bl, +5 ;除数
idiv bl ;AL=41,AH=3
执行除法之前,AX=00D0h(十进制数208)。IDIV把这个数除以5,生成的商为十进制数41,余数为3。这显然不是正确答案。
示例2 16位除法要求AX符号扩展到DX。下例执行-5000除以256:
.data
wordVal SWORD -5000
.code
mov ax, wordVal ;被除数的低字
cwd ;AX扩展到DX
mov bx, +256 ;除数
idiv bx ;商AX=-19,余数DX=-136
示例3 32位除法要求EAX符号扩展到EDX。下例执行50000除以-256:
.data
dwordVal SDWORD +50000
.code
mov eax, dwordVal ;被除数的低双字
cdq ;EAX扩展到EDX
mov ebx, -256 ;除数
idiv ebx ;商EAX=-195,余数EDX=+80
执行DIV和IDIV后,所有算术运算状态标志位的值都不确定。
完整代码测试笔记
;7.3.5_2.asm 7.3.5 有符号数除法
;2.IDIV 指令INCLUDE Irvine32.inc.data
byteVal SBYTE -48 ;D0 十六进制
wordVal SWORD -5000
dwordVal SDWORD +50000.code
main PROC;示例1 下述指令实现-48除以5。IDIV执行后,AL中的商为-9,AH 中的余数为 -3:mov al, byteVal ;被除数的低字节cbw ;AL扩展到AHmov bl, +5 ;除数idiv bl ;AL=-9,AH=-3;为了理解被除数的符号扩展为什么这么重要,现在在不进行符号扩展的前提下重复之前的例子。;下面的代码将AH初始化为0,这样它就有了确定值,然后没有用CBW指令转换被除数就直接进行了除法:mov ah, 0 ;被除数高字节mov al, byteVal ;被除数低字节mov bl, +5 ;除数idiv bl ;AL=41,AH=3;示例2 16位除法要求AX符号扩展到DX。下例执行-5000除以256:mov ax, wordVal ;被除数的低字cwd ;AX扩展到DXmov bx, +256 ;除数idiv bx ;商AX=-19,余数DX=-136;示例3 32位除法要求EAX符号扩展到EDX。下例执行50000除以-256:mov eax, dwordVal ;被除数的低双字cdq ;EAX扩展到EDXmov ebx, -256 ;除数idiv ebx ;商EAX=-195,余数EDX=+80INVOKE ExitProcess,0
main ENDPEND main
3.除法溢出
如果除法操作数生成的商不适合目的操作数,则产生除法溢出(divideoverflow)。这将导致处理器异常并暂停执行当前程序。例如,下面的指令就产生了除法溢出,因为它的商(100h)对8位的AL目标寄存器来说太大了:
mov ax, 1000h
mov bl, 10h
div bl ;AL无法容纳100h
运行这段代码时,Visual Studio 就会产生如图7-1所示的结果错误对话框。如果试图运行除以零的代码,也会显示相同的对话框。
图7-1 除法溢出错误示例
对此有个建议:使用32位除数和64位被除数来减少出现除法溢出条件的可能性。如下面的代码所示,除数为EBX,被除数在EDX和EAX组成的64位寄存器对中:
mov eax, 1000h
cdq
mov ebx, 10h
div ebx ;EAX =00000100h
要预防除以零的操作,则在进行除法之前检查除数:
mov ax, dividendmov bl, divisorcmp bl, 0 ;检查除数je NoDivideZero ;为零?显示错误div bl ;不为零:继续.,
NoDivideZero: ;显示"Attmpt to divide by zero"
7.3.6 实现算术表达式
第4章介绍了如何用加减指令实现算术表达式,现在还可以再加上乘法和除法指令。初看上去,实现算术表达式的工作似乎最好是留给编译器的编写者,但是动手研究一下还是能学到不少东西。读者可以学习编译器怎样优化代码。此外,与典型编译器在乘法操作后检查乘积大小相比,还能实现更好的错误检查。进行32位操作数相乘时,绝大多数高级语言编译器都会忽略乘积的高32位。而在汇编语言中,可以用进位标志位和溢出标志位来说明乘积是否为 32 位。这些标志位用法的解释参见7.4.1节和7.4.2节。
提示 有两种简单的方法可以查看C++编译器生成的汇编代码:一种方法是用Visual Studio 调试时,在调试窗口中右键点击,选择Go to Disassembly。另一种方法是,在 Project菜单中选择Properties,生成一个列表文件。在Configuration Properties选择Microsoft MacroAssembler,再选择ListingFile。在对话窗口中,将GeneratePreprocessed Source Listing设置为Yes,List All Available Information 也设置为 Yes。
示例1 使用32位无符号整数,用汇编语言实现下述C++语句:
var4 =(varl+var2)* var3;
这个问题很简单,因为可以从左到右来处理(先加法再乘法)。执行了第二条指令后EAX存放的是 var1与var2之和。第三条指令中,EAX乘以var3,乘积存放在 EAX中:
mov eax, var1add eax, var2mul var3 ;EAX=EAX*var3jc tooBig ;无符号溢出?mov var4, eaxjmp next
tooBig:
如果 MUL指令产生的乘积大于 32 位,则 JC指令跳转到有标号指令来处理错误
示例2 使用32位无符号整数实现下述C++语句:
var4=(varl*5)/(var2 -3);
本例有两个用括号括起来的子表达式。左边的子表达式可以分配给EDX:EAX,因此不必检查溢出。右边的子表达式分配给EBX,最后用除法完成整个表达式:
mov eax, var1 ;左边的子表达式
mov ebx, 5
mul ebx ;EDX:EAX=乘积
mov ebx, var2 ;右边的子表达式
sub ebx, 3 ;var2 - 3
div ebx ;最后的除法
mov var4, eax
示例3 使用32位有符号整数实现下述C++语句:
var4=(varl*5)/(-var2 %var3);
与之前的例子相比,这个例子需要一些技巧。可以先从右边的表达式开始,并将其保存在EBX中。由于操作数是有符号的,因此必须将被除数符号扩展到EDX,再使用IDIV指令:
mov eax, var2 ;开始计算右边的表达式
neg eax
cdq ;符号扩展被除数
idiv var3 ;EDX=余数
mov ebx, edx ;EBX=右边表达式的结果
第二步,计算左边的表达式,并将乘积保存在EDX:EAX中:
mov eax, -5 ;开始计算左边表达式
imul var1 ;EDX:EAX=左边表达式的结果
最后,左边表达式结果(EDX:EAX)除以右边表达式结果(EBX):
idiv ebx ;最后计算除法
mov var4, eax ;商
完整代码测试笔记
;7.3.6.asm 7.3.6 实现算术表达式INCLUDE Irvine32.inc.data
var1 DWORD 10h
var2 DWORD 20h
var3 DWORD 30h
var4 DWORD 0
errorTips BYTE "Integer Overflow",0.code
main PROC;示例1 使用32位无符号整数,用汇编语言实现下述C++语句: var4 =(varl+var2)* var3;mov eax, var1add eax, var2mul var3 ;EAX=EAX*var3jc tooBig ;无符号溢出?mov var4, eaxjmp next
tooBig:mov edx, OFFSET errorTipscall WriteString ;显示错误消息call Crlf
next:nop;示例2 使用32位无符号整数实现下述C++语句: var4=(varl*5)/(var2 -3);mov eax, var1 ;左边的子表达式mov ebx, 5mul ebx ;EDX:EAX=乘积mov ebx, var2 ;右边的子表达式sub ebx, 3 ;var2 - 3div ebx ;最后的除法mov var4, eax;示例3 使用32位有符号整数实现下述C++语句: var4=(varl*5)/(-var2 %var3);mov eax, var2 ;开始计算右边的表达式neg eaxcdq ;符号扩展被除数idiv var3 ;EDX=余数mov ebx, edx ;EBX=右边表达式的结果;第二步,计算左边的表达式,并将乘积保存在EDX:EAX中:mov eax, -5 ;开始计算左边表达式imul var1 ;EDX:EAX=左边表达式的结果;最后,左边表达式结果(EDX:EAX)除以右边表达式结果(EBX):idiv ebx ;最后计算除法mov var4, eax ;商INVOKE ExitProcess,0
main ENDPEND main
7.3.7 本节回顾
1.请说明执行 MUL 指令和单操作数的IMUL指令时,不会发生溢出的原因。
答:保存乘积的寄存器大小是乘数和被乘数大小的两位。比如,计算0FFh乘以0FFh,则乘积(FE01h)很容易就扩展到16位。
2.生成乘积时,单操作数 IMUL指令与 MUL 指令有何不同?
答:当相乘结果正好可以完全存放在乘积的低位寄存器时,IMUL对乘积符号扩展到高位乘积寄存器。而MUL则对乘积进行全零扩展。
3.什么情况下单操作数IMUL指令会将进位标志位和溢出标志位置1?
答:对IMUL来说,若乘积的高半部分不是其低半部分的符号扩展,那么进位标志位和没溢出标志位置1.
4.DIV 指令中,若EBX为操作数,则商保存在哪个寄存器中?
答:EAX中
5.DIV 指令中,若BX为操作数,则商保存在哪个寄存器中?
答:AX中
6.MUL指令中,若BL为操作数,则乘积保存在哪个寄存器中?
答:乘积保存AX中
7.举例说明,在调用IDIV 指令前,如何对其16位操作数进行符号扩展。
答:cwd指令对16位操作数进行扩展。
mov ax, dividendLow
cwd ;被除数符号扩展
mov bx, divisor
idiv bx
7.4 扩展加减法
扩展精度加减法(extended precision addition and subtraction)是对基本没有大小限制的数进行加减法的技术。比如,在C++中,没有标准运算符会允许两个1024位整数相加。但是在汇编语言中,ADC(带进位加法)和SBB(带借位减法)指令就很适合进行这类操作。
7.4.1 ADC指令
ADC(带进位加法)指令将源操作数和进位标志位的值都与目的操作数相加。该指令格式与ADD指令一样,且操作数大小必须相同:
ADC reg,reg
ADC mem,reg
ADC reg,mem
ADC mem,imm
ADC reg,imm
例如,下述指令实现两个8位整数相加(FFh+FFh),产生的16位和数存入DL:AL.其值为01FEh:
mov dl, 0
mov al, 0FFh
add al, 0FFh ;AL = FEh
adc dl, 0 ;DL/AL = 01FEh
下图展示了这两个数相加过程中的数据活动。首先,FFh与AL相加,生成FEh存人AL寄存器,并将进位标志位置1。然后,将0和进位标志位与DL寄存器相加:
同样,下述指令实现两个32位整数相加(FFFFFFFFh+FFFFFFFFh),产生的64位和数存人EDX:EAX,其值为:00000001FFFFFFFEh:
mov edx,0
mov eax,0FFFFFFFFh
add eax,0FFFFFFFFh
adc edx,0
完整代码测试笔记
;7.4.1.asm 7.4.1 ADC指令
;ADC(带进位加法)指令将源操作数和进位标志位的值都与目的操作数相加。INCLUDE Irvine32.inc.code
main PROC;下述指令实现两个8位整数相加(FFh+FFh),产生的16位和数存入DL:AL.其值为01FEh:mov dl, 0mov al, 0FFhadd al, 0FFh ;AL = FEhadc dl, 0 ;DL/AL = 01FEh;下述指令实现两个32位整数相加(FFFFFFFFh+FFFFFFFFh),产生的64位和数存人EDX:EAX,其值为:00000001FFFFFFFEh:mov edx,0mov eax,0FFFFFFFFhadd eax,0FFFFFFFFhadc edx,0INVOKE ExitProcess,0
main ENDP
END main
7.4.2 扩展加法示例
接下来将说明过程Extended Add实现两个大小相同的扩展整数的加法。利用循环,该过程将两个扩展整数当作并行数组实现加法操作。数组中每对数值相加时,都要包括前一次循环迭代执行的加法所产生的进位标志位。实现过程时,假设整数存储在字节数组中,不过本例很容易就能修改为双字数组的加法。
该过程接收两个指针,存入ESI和EDI,分别指向参与加法的两个整数。EBX寄存器指向缓冲区,用于存放和数,该缓冲区的前提条件是必须比两个加数大一个字节。此外,过程还用 ECX接收最长加数的长度。两个加数都需要按小端顺序存放,即其最低字节存放在该数组的起始地址。过程代码如下所示,添加了代码行编号便于进行详细讨论:
;---------------------------------------------------------------------
;
;计算两个以字节数组存放的扩展整数之和。
;接收:ESI和EDI为两个加数的指针,
; EBX为和数变量指针,ECX为
; 相加的字节数。
;和数存储区必须比输入的操作数多一个字节.
;
;返回:无
;
;----------------------------------------------------------------------
Extended_Add PROCpushadclc ;清除进位标志位L1: mov al, [esi] ;取第一个数adc al, [edi] ;与第二个数相加pushfd ;保存进位标志位mov [ebx], al ;保存部分和add esi, 1 ;三个指针都加1add edi, 1add ebx, 1popfd ;恢复进位标志位loop L1 ;重复循环mov byte ptr [ebx], 0adc byte ptr [ebx], 0 ;清除和数高字节popad ;加上其他的进位ret
Extended_Add ENDP
当第16行和第17行将两个数组的最低字节相加时,加法运算可能会将进位标志位置1。因此,第18行将进位标志位压人堆栈进行保存就很重要,因为在循环重复时会用到进位标志位。第19行保存了和数的第一个字节,第20~22行将三个指针(两个操作数,一个和数)都加1。第23 行恢复进位标志位,第24行将循环返回到第16行。(LOOP指令不会修改CPU的状态标志位。)再次循环时,第17行进行的是第二对字节的加法,其中包括进位标志位的值。因此,如果第一次循环过程产生了进位,则第二次循环就要包括该进位。按照这种方式循环,直到所有的字节都完成了加法。然后,最后的第26行和第27行检查操作数最高字节相加是否产生进位,若产生了进位,就将该值加到和数多出来的那个字节中。
下面的代码示例调用Extended Add,并向其传递两个8字节的整数。要注意为和数多分配一个字节:
.data
op1 BYTE 34h, 12h, 98h, 74h, 06h, 0A4h, 0B2h, 0A2h
op2 BYTE 02h, 45h, 23h, 00h, 00h, 87h, 10h, 80h
sum BYTE 9 dup(0)
.code
main PROCmov esi, OFFSET op1 ;第一个操作数mov edi, OFFSET op2 ;第二个操作数mov ebx, OFFSET sum ;和数mov ecx, LENGTHOF op1 ;字节数call Extended_Add;显示和数。mov esi, OFFSET summov ecx, LENGTHOF sumcall Display_sumcall CrlfINVOKE ExitProcess,0
main ENDP
END main
上述程序的输出如下所示,加法产生了一个进位:
0122C32B0674BB5736
过程 Display_Sum(来自同一个程序)按照正确的顺序显示和数,即从最高字节开始依次显示到最低字节:
Display_Sum PROCpushad;指向最后一个数组元素add esi, ecxsub esi, TYPE BYTEmov ebx, TYPE BYTE
L1: mov al, [esi] ;取一个数组字节call WriteHexB ;显示该字节sub esi, TYPE BYTE ;指向前一个字节loop L1popadret
Display_Sum ENDP
7.4.3SBB 指令
SBB(带借位减法)指令从目的操作数中减去源操作数和进位标志位的值。允许使用的操作数与ADC指令相同。下面的示例代码用32位操作数实现64位减法,EDX:EAX的值为0000000700000001h,从该值中减去2。低32位先执行减法,并设置进位标志位,然后高32位再进行包括进位标志位的减法:
mov edx, 7 ;高32位
mov eax, 1 ;低32位
sub eax, 2 ;减2
sbb edx, 0 ;高32位减法
图7-2展示了这两个数相减过程中的数据活动。首先,EAX减2,差值FFFFFFFFh存放在EAX中。由于是从小数中减去大数,因此产生借位,将进位标志位置1。然后,用SBB指令从EDX中减去0和进位标志位。
完整代码测试笔记
;7.4.3.asm 7.4.3 SBB指令
;SBB(带借位减法)指令从目的操作数中减去源操作数和进位标志位的值。允许使用的操作数与ADC指令相同。INCLUDE Irvine32.inc.code
main PROCmov edx, 7 ;高32位mov eax, 1 ;低32位sub eax, 2 ;减2sbb edx, 0 ;高32位减法INVOKE ExitProcess,0
main ENDP
END main
7.4.4 本节回顾
1.请描述 ADC指令。
答:ADC指令将目的操作数与源操作数和进位标志位相加。
2.请描述SBB指令。
答:SBB指令将目的数减去源操作数和进位村志位
3.执行下述指令后,EDX:EAX中的值是多少?
mov edx, 10h
mov eax, 0A0000000h
add eax, 20000000h
adc edx,0
答:EDX:EAX的值为00000010 C0000000h
4.执行下述指令后,EDX:EAX中的值是多少?
mov edx, 100h
mov eax, 80000000h
sub eax, 90000000h
sbb edx, 0
答:EDX:EAX的值为000000FF F0000000h
5.执行下述指令后,DX中的值是多少(STC将进位标志位置1)?
mov dx, 5
stc ;进位标志
mov ax, 10h
adc dx, ax
答:DX中的值为10h+1+5 = 16h