算术运算是汇编语言中一个大得令人惊讶的主题!本节重点在于加法和减法的运算。
先从最简单、最有效的指令开始:INC(增加)和 DEC(减少)指令,即加 1 和减 1。然后是能提供更多操作的 ADD、SUB 和 NEG(非)指令。最后,将讨论算术运算指令如何影响 CPU 状态标志位(进位位、符号位、零标志位等)。请记住,汇编语言的细节很重要。
INC 和 DEC 指令
INC(增加)和DEC(减少)指令分别表示寄存器或内存操作数加 1 和减 1。语法如下所示:
INC reg/mem
DEC reg/mem
下面是一些例子:
.data myWord WORD 1000h .code inc myWord ; myWord = 1001h mov bx,myWord dec bx ; BX = 1000h
根据目标操作数的值,溢岀标志位、符号标志位、零标志位、辅助进位标志位、进位标志位和奇偶标志位会发生变化。INC 和 DEC 指令不会影响进位标志位(这还真让人吃惊)。
ADD 指令
ADD 指令将长度相同的源操作数和目的操作数进行相加操作。语法如下:
ADD dest,source
在操作中,源操作数不能改变,相加之和存放在目的操作数中。该指令可以使用的操作数与 MOV 指令相同。下面是两个 32 位整数相加的短代码示例:
.data var1 DWORD 10000h var2 DWORD 20000h .code mov eax,var1 ; EAX = 10000h add eax,var2 ; EAX = 30000h
标志位:进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标 志位根据存入目标操作数的数值进行变化。
SUB 指令
SUB 指令从目的操作数中减去源操作数。该指令对操作数的要求与 ADD 和 MOV 指令相同。指令语法如下:
SUB dest, source
下面是两个 32 位整数相减的短代码示例:
.data var1 DWORD 30000h var2 DWORD 10000h .code mov eax,var1 ;EAX = 30000h sub eax,var2 ;EAX = 20000h
标志位:进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标 志位根据存入目标操作数的数值进行变化。
NEG 指令
NEG(非)指令通过把操作数转换为其二进制补码,将操作数的符号取反。下述操作数可以用于该指令:
NEG reg
NEG mem
提示:将目标操作数按位取反再加 1,就可以得到这个数的二进制补码。
标志位:进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标志位根据存入目标操作数的数值进行变化。
执行算术表达式
使用 ADD、SUB 和 NEG 指令,就有办法来执行汇编语言中的算术表达式,包括加法、减法和取反。换句话说,当有下述表达式时,就可以模拟 C++ 编译器的行为:
Rval = -Xval + (Yval – Zval);
现在来看看,使用如下有符号 32 位变量,汇编语言是如何执行上述表达式的。
Rval SDWORD ?
Xval SDWORD 26
Yval SDWORD 30
Zval SDWORD 40
转换表达式时,先计算每个项,最后再将所有项结合起来。首先,对 Xval 的副本进行取反,并存入寄存器:
; first term: -Xval mov eax,Xval neg eax ; EAX = -26
然后,将 Yval 复制到寄存器中,再减去 Zval:
; second term: (Yval - Zval) mov ebx,Yval sub ebx,Zval ; EBX = -10
最后,将两个项(EAX 和 EBX 的内容)相加:
; add the terms and store: add eax,ebx mov Rval,eax ; -36
加减法影响的标志位
执行算术运算指令时,常常想要了解结果。它是负数、正数还是零?对目的操作数来说,它是太大,还是太小?这些问题的答案有助于发现计算错误,否则可能会导致程序的错误行为。
检查算术运算结果使用的是 CPU 状态标志位的值,同时,这些值还可以触发条件分支指令,即基本的程序逻辑工具。下面是对状态标志位的简要概述:
- 进位标志位意味着无符号整数溢出。比如,如果指令目的操作数为 8 位,而指令产生的结果大于二进制的 1111 1111,那么进位标志位置 1。
- 溢出标志位意味着有符号整数溢出。比如,指令目的操作数为 16 位,但其产生的负数结果小于十进制的 -32 768,那么溢出标志位置 1。
- 零标志位意味着操作结果为 0。比如,如果两个值相等的操作数相减,则零标志位置 1。
- 符号标志位意味着操作产生的结果为负数。如果目的操作数的最高有效位(MSE)置 1,则符号标志位置 1。
- 奇偶标志位是指,在一条算术或布尔运算指令执行后,立即判断目的操作数最低有效字节中 1 的个数是否为偶数。
- 辅助进位标志位置 1,意味着目的操作数最低有效字节中位 3 有进位。
要在调试时显示 CPU 状态标志位,打开 Register 窗口,右键点击该窗口,并选择 Flags。
1) 无符号数运算:零标志位、进位标志位和辅助进位标志位
当算术运算结果等于 0 时,零标志位置 1。下面的例子展示了执行 SUB、INC 和 DEC 指令后,目的寄存器和零标志位的状态:
mov ecx,1 sub ecx,1 ;ECX = 0, ZF = 1 mov eax,0FFFFFFFFh inc eax ;EAX = 0, ZF = 1 inc eax ;EAX = 1, ZF = 0 dec eax ;EAX = 0, ZF = 1
加法和进位标志位,如果将加法和减法分开考虑,那么进位标志位的操作是最容易解释的。两个无符号整数相加时,进位标志位是目的操作数最高有效位进位的副本。直观地说,如果和数超过了目的操作数的存储大小,就可以认为 CF = 1。在下面的例子里,ADD 指令将进位标志位置 1,原因是,相加的和数(100h)超过了 AL 的大小:
mov al,0FFh add al,1 ; AL = 00, CF = 1
下图演示了在 0FFh 上加 1 时,操作数的位是如何变化的。AL 最高有效位的进位复制到进位 标志位。
另一方面,如果 AX 的值为 00FFh,则对其进行加 1 操作后,和数不会超过 16 位,那么进位标志位清 0:
mov ax,00FFh add ax, 1 ; AX = 0100h, CF = 0
但是,如果 AX 的值为 FFFFh,则对其进行加 1 操作后,AX 的高位就会产生进位:
mov ax,0FFFFh add ax, 1 ; AX = 0000, CF = 1
减法和进位标志位,从较小的无符号整数中减去较大的无符号整数时,减法操作就会将进位标志位置 1。下图说明了,操作数为 8 位时,计算(1-2)会出现什么情况。下面是相应的汇编代码:
mov al, 1 sub al,2 ; AL = FFh, CF = 1
提示:INC 和 DEC 指令不会影响进位标志位。在非零操作数上应用 NEG 指令总是会将进位标志位置 1。
辅助进位标志位,辅助进位(AC)标志位意味着目的操作数位 3 有进位或借位。它主要用于二进制编码的十进制数(BCD)运算,也可以用于其他环境。现在,假设计算(1+0Fh),和数在位 4 上为 1,这是位 3 的进位:
mov al,0Fh add al, 1 ; AC = 1
计算过程如下:
00001111
+ 00000001
————–
00010000
奇偶标志位,目的操作数最低有效字节中 1 的个数为偶数时,奇偶(PF)标志位置 1。下例中,ADD 和 SUB 指令修改了 AL 的奇偶性:
mov al,10001100b add al,00000010b ; AL = 10001110, PF = 1 sub al,10000000b ; AL = 00001110, PF = 0
执行了 ADD 指令后,AL 的值为 1000 1110 (4 个 0, 4 个 1), PF=1。执行了 SUB 指令后,AL 的值包含了奇数个 1,因此奇偶标志位等于 0。
2) 有符号数运算:符号标志位和溢出标志位
符号标志位,有符号数算术操作结果为负数,则符号标志位置 1。下面的例子展示的是小数(4)减去大数(5):
mov eax, 4 sub eax,5 ; EAX = -1, SP = 1
从机器的角度来看,符号标志位是目的操作数高位的副本。下面的例子表示产生了负数结果后,BL 中的十六进制的值:
mov bl,1 ; BL = 01h sub bl,2 ; BL = FFh(-1), SF = 1
溢出标志位,有符号数算术操作结果与目的操作数相比,如果发生上溢或下溢,则溢出标志位置 1。例如,8 位有符号整数的最大值为 +127,再加 1 就会溢出:
mov al,+127 add al, 1 ; OF = 1
同样,最小的负数为-128,再减1就发生下溢。如果目的操作数不能容纳一个有效算 术运算结果,那么溢出标志位置 1:
mov al,-128 sub al,1 ;OF = 1
加法测试,两数相加时,有个很简单的方法可以判断是否发生溢出。溢出发生的情况有:
- 两个正数相加,结果为负数
- 两个负数相加,结果为正数
如果两个加数的符号相反,则不会发生溢出。
硬件如何检测溢出,加法或减法操作后,CPU 用一种有趣的机制来检测溢出标志位的状态。计算结果的最高有效位产生的进位与结果的最高位进行 异或操作,异或的结果存入溢岀标志位。如下图所示,两个 8 位二进制数 1000 0000 和 1111 1110 相加,产生进位 CF=1,和数最高位(位 7)= 0,即 1 XOR 0=1,则 OF=1。
NEG 指令,如果 NEG 指令的目的操作数不能正确存储,则该结果是无效的。例如, AL 中存放的是 -128,对其求反,正确的结果为 +128,但是这个值无法存入 AL。则溢出标志位置 1 就表示 AL 中存放的是一个无效的结果:
mov al,-128 ;AL = 10000000b neg al ;AL = 10000000b, OF = 1
反之,如果对 +127 求反,结果是有效的,则溢出标志位清 0:
mov al,+127 ;AL = 01111111b neg al ;AL = 10000001b, OF = 0
CPU 如何知道一个算术运算是有符号的还是无符号的?答案看上去似乎有点愚蠢:它不知道!在算术运算之后,不论标志位是否与之相关,CPU 都会根据一组布尔规则来设置所有的状态标志位。程序员要根据执行操作的类型,来决定哪些标志位需要分析,哪些可以忽略。
示例程序(AddSubTest)
AddSubTest 程序利用 ADD、SUB、INC、DEC 和 NEG 指令执行各种算术运算表达式,并展示了相关状态标志位是如何受到影响的:
;加法和减法 (AddSubTest.asm) .386 .model flat,stdcall .stack 4096 ExitProcess proto,dwExitCode:dword .data Rval SDWORD ? Xval SDWORD 26 Yval SDWORD 30 Zval SD
4.1 操作数类型
4.2 MOV指令
4.3 MOVZX和MOVSX指令
4.4 LAHF和SAHF指令
4.5 XCHG指令
4.6 直接偏移量操作数
4.7 汇编语言数据传送示例
4.8 加法和减法详解
4.9 OFFSET运算符
4.10 ALIGN伪指令
4.11 PTR运算符
4.12 TYPE运算符
4.13 LENGTHOF运算符
4.14 LABEL伪指令
4.15 间接寻址
4.16 JMP和LOOP指令
4.17 64位MOV指令
4.18 64位加法和减法