《汇编语言:基于X86处理器》第7章 整数运算(3)
本章将介绍汇编语言最大的优势之一:基本的二进制移位和循环移位技术。实际上,位操作是计算机图形学、数据加密和硬件控制的固有部分。实现位操作的指令是功能强大的工具,但是高级语言只能实现其中的一部分,并且由于高级语言要求与平台无关,所以这些指令在一定程度上被弱化了。本章将展示一些对移位操作的应用,包括乘除法的优化。
并非所有的高级编程语言都支持任意长度整数的运算。但是汇编语言指令使得它能够加减几乎任何长度的整数。本章还将介绍执行压缩十进制整数和整数字符串运算的专用指令。
7.5 ASCI和非压缩十进制运算
(7.5节讨论的指令只能用于32位模式编程。)到目前为止,本书讨论的整数运算处理的都是二进制数。虽然CPU用二进制运算,但是也可以执行ASCI十进制串的运算。使用后者进行运算,对用户而言既便于输入也便于在控制台窗口显示,因为不用进行二进制转换假设程序需要用户输人两个数,并将它们相加。若用户输人3402和1256,则程序输出如下所示:
输入第一个数:3402
输入第二个数:1256
和 数: 4658
有两种方法可以计算并显示和数:
1)将两个操作数都转换为二进制,进行二进制加法,再将和数从二进制转换为ASCII数字串。
2)直接进行数字串的加法,按序相加每对ASCI数字(2+6、0+5、4+2、3+1)。和数为ASCII数字串,因此可以直接显示在屏幕上。
第二种方法需要在执行每对ASCI数字相加后,用特殊指令来调整和数。有四类指令用于处理 ASCII加法、减法、乘法和除法,如下所示:
AAA | (执行加法后进行 ASCI 调整) | AAM | (执行乘法后进行 ASCI 调整) |
AAS | (执行减法后进行 ASCII 调整) | AAD | (执行除法前进行 ASCII 调整) |
ASCII 十进制数和非压缩十进制数 非压缩十进制整数的高4位总是为零,而ASCII十进制数的高4位则应该等于0011b。在任何情况下,这两种类型的每个数字都占用一个字节。下面的例子展示了3402用这两种类型存放的格式:
尽管ASCI运算执行速度比二进制运算要慢很多,但是它有两个明显的优点:
●不必在执行运算之前转换串格式。
●使用假设的十进制小数点,使得实数操作不会出现浮点运算的舍入误差的危险。
ASCII 加减法运行操作数为ASCI格式或非压缩十进制格式,但是乘除法只能使用非压缩十进制数。
7.5.1 AAA 指令
在 32位模式下,AAA(加法后的ASCI调整)指令调整ADD或ADC指令的二进制运算结果。设两个ASCI数字相加,其二进制结果存放在AL中,则AAA将AL转换为两个非压缩十进制数字存人AH和AL。一旦成为非压缩格式,通过将AH和AL与30h进OR运算,很容易就能把它们转换为 ASCII码。
下例展示了如何用AAA指令正确地实现ASCI数字8加2。在执行加法之前,必须把AH清零,否则它将影响AAA执行的结果。最后一条指令将AH和AL转换为ASCI数字:
mov ah, 0
mov al, '8' ;AX = 0038h
add al, '2' ;AX = 006Ah
aaa ;AX = 0100h(结果进行ASCII调整)
or ax, 3030h ;AX = 3130h ='10'(转换为ASCII码)
使用 AAA 实现多字节加法
现在来查看一个过程,其功能为实现包含了隐含小数点的ASCII十进制数值相加。由于每次数字相加的进位标志位都要传递到更高位,因此,过程的实现要比想象的更复杂一些。下面的伪代码中,acc代表的是一个8位的累加寄存器:
esi (index)=length of first number - 1
edi (index)=length of first number
ecx =lengthoffirst number
set carry value to 0
Loopacc = first number[esi]add previous carry to accsave carry in carrylacc += second_number[esi]OR the carry with carry1sum[edi] = accdec edi
Until ecx == 0
Store last carry digit in sum
进位值必须总是被转换为ASCI码。将进位值与第一个操作数相加时,就需要用AAA来调整结果。程序清单如下:
;ASCII_add.asm ASCII加法
;对有隐含固定小数点的串执行ASCII运算。INCLUDE Irvine32.incDECIMAL_OFFSET = 5 ;距离串右侧的偏移量
.data
decimal_one BYTE '100123456789765' ;1001234567.89765
decimal_two BYTE '900402076502015' ;9004020765.02015
sum BYTE (SIZEOF decimal_one + 1) DUP(0), 0.code
main PROC;从最后一个数字位开始mov esi, SIZEOF decimal_one - 1mov edi, SIZEOF decimal_onemov ecx, SIZEOF decimal_onemov bh, 0 ;进位值清零
L1: mov ah, 0 ;执行加法前清除AHmov al, decimal_one[esi] ;取第一个数字add al, bh ;加上之前的进位值aaa ;调整和数AH=进位值mov bh, ah ;将进位保存到carry1or bh, 30h ;将其转换为ASCII码add al, decimal_two[esi] ;加第二个数字aaa ;调整和数AH=进位值or bh, ah ;进位值与 carry1进行 OR运算or bh, 30h ;将其转换为ASCII 码or al, 30h ;将AL转换为ASCII码mov sum[edi], al ;将AL保存到sumdec esi ;后退一个数字dec edi loop L1mov sum[edi], bh ;保存最后的进位值;显示和数字符串。mov edx, OFFSET sumcall WriteStringcall CrlfINVOKE ExitProcess,0
main ENDP
END main
程序输出如下所示,和数没有显示十进制小数点:
7.5.2 AAS 指令
32位模式下,AAS(减法后的ASCII调整)指令紧随SUB或SBB指令之后,这两条指令执行两个非压缩十进制数的减法,并将结果保存到AL中。AAS指令将AL转换为ASCII码的数字形式。只有减法结果为负时,调整才是必需的。比如,下面的语句实现ASCI码数字8减去9:
;7.5.2.asm 7.5.2 AAS 指令
;下面的语句实现ASCI码数字8减去9:.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.data
val1 BYTE '8'
val2 BYTE '9'.code
main PROCmov ah, 0mov al, val1 ;AX = 0038hsub al, val2 ;AX = 00FFhaas ;AX = 0FF09hpushf ;保存进位标志位or al, 30h ;AX = 0FF39hpopf ;恢复进位标志位INVOKE ExitProcess,0
main ENDP
END main
执行SUB指令后,AX等于00FFh。AAS指令将AL转换为09h,AH减1等于FFh并且把进位标志位置1。
7.5.3 AAM 指令
32位模式下,MUL执行非压缩十进制乘法,AAM(乘法后的ASCII调整)指令转换由其产生的二进制乘积。乘法只能使用非压缩十进制数。下面的例子实现5乘以6,并调整AX中的结果。调整后,AX=0300h,非压缩十进制表示为30:
;7.5.3.asm 7.5.3 AAM指令
;下面的例子实现5乘以6,并调整AX中的结果。
;调整后,AX=0300h,非压缩十进制表示为30:.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.data
ascVal BYTE 05h, 06h.code
main PROCmov bl, ascVal ;第1个操作数mov al, [ascVal+1] ;第2个操作数mul bl ;AX=001Eh aam ;AX=0300h INVOKE ExitProcess,0
main ENDP
END main
7.5.4 AAD 指令
32位模式下,AAD(除法之前的ASCII调整)指令将AX中的非压缩十进制被除数转换为二进制,为执行DIV指令做准备。下面的例子把非压缩0307h转换为二进制数,然后除以5。DIV指令在AL中生成商07h,在AH中生成余数02h:
;7.5.4.asm 7.5.4 AAD指令
;下面的例子把非压缩0307h转换为二进制数,然后除以5。
;DIV指令在AL中生成商07h,在AH中生成余数02h:.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.data
quotient BYTE ?
remainder BYTE ?.code
main PROCmov ax, 0307h ;被除数aad ;AX = 0025hmov bl, 5 ;除数div bl ;AX=0207hmov quotient, almov remainder, ahINVOKE ExitProcess,0
main ENDP
END main
7.5.5 本节回顾
1.编写一条指令,将 AX中的一个两位非压缩十进制整数转换为十进制的 ASCII码。
答:or ax, 3030h
2.编写一条指令,将 AX中的一个两位 ASCII码十进制整数转换为非压缩十进制形式
答:and ax, 0F0Fh
3.编写有两条指令的序列,将 AX中的一个两位 ASCII 码十进制整数转换为二进制。
答:and ax, 0F0Fh ;转换为非压缩形式
aad
4.编写一条指令,将 AX中的一个无符号二进制整数转换为非压缩十进制数。
答:aam
7.6 压缩十进制运算
(7.6节讨论的指令仅用于32位编程模式。)压缩十进制数的每个字节存放两个十进制数字,每个数字用4位表示。如果数字个数为奇数,则最高的半字节用零填充。存储大小可变:
bcd1 QWORD 2345673928737285h ;十进制数 2345673928737285
bcd2 DWORD 12345678h ;十进制数12345678
bcd3 DWORD 08723654h ;十进制数8723654
bcd4 WORD 9345h ;十进制数9345
bcd5 WORD 0237h ;十进制数237
bcd6 BYTE 34h ;十进制数34
压缩十进制存储至少有两个优势:
●数据几乎可以包含任何个数的有效数字。这使得以很高的精度执行计算成为可能
●实现压缩十进制数与 ASCII码之间的相互转换相对简单。
DAA(加法后的十进制调整)和DAS(减法后的十进制调整)这两条指令调整压缩十进制数加减法的结果。可惜的是,目前还没有与乘除法有关的相似指令。在这些情况下,相乘或相除的数必须是非压缩的,执行后再压缩。
7.6.1 DAA 指令
32位模式下,ADD或ADC指令在AL中生成二进制和数,DAA(加法后的十进制调整)指令将和数转换为压缩十进制格式。比如,下述指令执行压缩十进制数35加48。二进制和数(7Dh)被调整为83h,即35和48的压缩进制和数。
mov al, 35h
add al, 48h ;AL=7Dh
daa ;AL=83h(调整后的结果)
DAA的内部逻辑请参阅Intel指令集参考手册。示例 下面的程序执行两个16位压缩十进制整数加法,并将和数保存在一个压缩双字中。加法要求和数变量的存储大小比操作数多一个数字:
;AddPacked.asm 7.6.1 DAA指令 压缩十进制示例
;下面的程序执行两个16位压缩十进制整数加法,并将和数保存在一个压缩双字中。
;加法要求和数变量的存储大小比操作数多一个数字:INCLUDE Irvine32.inc.data
packed_1 WORD 4536h
packed_2 WORD 7207h
sum DWORD ?.code
main PROC;初始化和数与索引:mov sum, 0mov esi, 0;低字节相加。mov al, BYTE PTR packed_1[esi]add al, BYTE PTR packed_2[esi]daamov BYTE PTR sum[esi], al;高字节相加,包括进位标志位。inc esimov al, BYTE PTR packed_1[esi]adc al, BYTE PTR packed_2[esi]daamov BYTE PTR sum[esi], al;若还有进位,则加上该进位值。inc esimov al, 0adc al, 0mov BYTE PTR sum[esi], al;用十六进制显示和数,mov eax, sumcall WriteHexcall Crlfexit;INVOKE ExitProcess,0
main ENDP
END main
显然,这个程序包含重复代码,因此建议使用循环结构。本章的一道习题将会要求编写一个过程,实现任意大小的压缩十进制整数加法。
7.6.2 DAS指令
32位模式下,SUB或SBB指令在AL中生成二进制结果,DAS(减法后的十进制调整)指令将其转换为压缩十进制格式。比如,下面的语句计算压缩十进制数85减48,并调整结果:
mov bl, 48h
mov al, 85h
sub al, bl ;AL = 3Dh
das ;AL = 37h (调整后)
DAS的内部逻辑请参阅Intel指令集参考手册.
7.6.3 本节回顾
1.举例说明,什么情况下DAA指令会把进位标志位置1?
答:当压缩十进制加法的和数大于99时,DAA将进位标志位置1,例如:
;7.6.3_1.asm 7.6.3 本节回顾
;1.举例说明,什么情况下DAA指令会把进位标志位置1?.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.code
main PROCmov al, 56hadd al, 92h ;AL = E8hdaa ;AL = 48h, CF = 1INVOKE ExitProcess,0
main ENDP
END main
2.举例说明,什么情况下DAS指令会把进位标志位置1?
答:若从小的压缩十进制整数中减去大的压缩十进制整数,则DAS将进位标志位置1.例如:
;7.6.3_2.asm 7.6.3 本节回顾
;2.举例说明,什么情况下DAS指令会把进位标志位置1?.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.code
main PROCmov al, 56hsub al, 92h ;AL = C4hdas ;AL = 64h, CF = 1INVOKE ExitProcess,0
main ENDP
END main
3.两个长度为"字节的压缩十进制整数相加时,和数应该保留多少字节?
答:和数应该保留n+1个字节。
7.7 本章小结
与前面章节介绍的位元指令一样,移位指令也是汇编语言最显著的特点之一。一个数移位就意味着把它的位元进行右移或左移。
SHL(左移)指令把目标操作数的每一位都向左移动,最低位用0填充。SHL最大的作用之一是快速实现与2的幂相乘。任何操作数左移位即为乘以2"。SHR(右移)指令则把每一位都向右移动,最高位用0填充。任何操作数右移位即为除以2"。
SAL(算术左移)和SAR(算术右移)是特别为有符号数移位设计的指令。
ROL(循环左移)指令把每一位向左移动,并将最高位复制到进位标志位和最低位。ROR(循环右移)指令把每一位向右移动,并将最低位复制到进位标志位和最高位。
RCL(带进位循环左移)指令把每一位都左移,并先将进位标志位复制到移位结果的最低位,再将最高位复制到进位标志位。RCR(带进位循环右移)指令把每一位都右移,并将最低位复制到进位标志位,而进位标志位则复制到结果的最高位。
x86处理器可使用的SHLD(双精度左移)和SHRD(双精度右移)指令对大数的移位非常有用。
32位模式下,MUL指令实现一个8位、16位或32位的操作数与AL、AX或EAX相乘64位模式下,一个数还可以实现与RAX寄存器相乘。IMUL指令执行有符号数乘法,它有三种格式:单操作数、双操作数和三操作数。
32位模式下,DIV指令实现8位、16位或32位操作数的除法。64位模式下,还可以实现 64位除法。IDIV指令执行有符号数乘法,其格式与DIV指令相同。
CBW(字节转字)指令把AL的符号位扩展到AH寄存器。CDO(双字转四字)指令把EAX的符号位扩展到EDX寄存器。CWD(字转双字)指令把AX的符号位扩展到DX寄存器。
扩展加减法是指加减任意大小的数,ADC和SBB指令可以用于实现这种加减运算ADC(带进位加法)指令实现源操作数与进位标志位的内容和目的操作数相加。SBB(带借位减法)指令实现目的操作数减去源操作数和进位标志位的值。
ASCII十进制数每个字节存放一个数字,并编码为ASCI形式。AAA(加法后的ASCII调整)指令将ADD或ADC指令的二进制结果转换为ASCII十进制。AAS(减法后的ASCII调整)指令将SUB或SBB指令的二进制结果转换为ASCII十进制。所有这些指令都只能用于32位模式。
非压缩十进制数每个字节存放一个十进制数字,表现为二进制数值。AAM(乘法后的ASCII 调整)指令转换的是MUL指令执行非压缩十进制数乘法所生成的二进制结果。AAD(除法前的 ASCI 调整)指令在执行 DIV指令之前,将非压缩十进制被除数转换为二进制。所有这些指令都只能用于32位模式。
压缩十进制数每个字节存放两个十进制数字。DAA(加法后的十进制调整)指令转换的是 ADD或 ADC指令执行压缩十进制加法所生成的二进制结果。DAS(减法后的十进制调整)指令转换的是SUB或SBB指令执行压缩十进制减法所生成的二进制结果。所有这些指令都只能用于 32 位模式,