《汇编语言:基于X86处理器》第4章 数据传送、寻址和算术运算(1)
本章介绍了数据传送和算术运算的若干必要指令,用大量的篇幅说明了基本寻址模式,如直接寻址、立即寻址和可以用于处理数组的间接寻址。同时,还展示了怎样创建循环和怎样使用一些基本运算符,如 OFFSET,PTR 和LENGTHOF。阅读本章后,将会了解除条件语句之外的汇编语言的基本工作知识。
4.1.1 引言
用Java或C++这样的语言编程时,编译器产生的大量语法错误信息很容易让初学者感到心烦。编译器执行严格类型检查,以避免可能出现诸如不匹配变量和数据的错误。另一方面,只要处理器指令集允许,汇编器就能完成任何操作请求。换句话说,汇编语言就是将程序员的注意力集中在数据存储和具体机器细节上。编写汇编语言代码时,必须要了解处理器的限制。而 x86 处理器具有众所周知的复杂指令集(complex instruction set),因此,可以用许多方法来完成任务。
如果花时间深人了解本章介绍的材料,则阅读本书其他内容会更加顺利。随着示例程序越来越复杂,需要依赖对本章介绍的基础工具的掌握。
4.1.2 操作数类型第
3 章介绍过x86 指令格式:
[label:] mnemonie [operands] [ ;comment]
指令包含的操作数个数可以是:0 个,1 个,2 个或 3 个。这里,为了清晰起见,省略掉标号和注释:
mnemonic(助记符)
mnemonic [destination]
mnemonic [destination], [source]
mnemonic [destinationl, [source-1l, [source-2]
操作数有3 种基本类型:
●立即数--使用数字文本表达式
●寄存器操作数--使用CPU 内已命名的寄存器
●内存操作数--引用内存位置
表4-1说明了标准操作数类型,它使用了简单的操作数符号(32位模式下),这些符号来自Intel 手册并进行了改编。从现在开始,本书将用这些符号来描述每条指令的语法。
4.1.3 直接内存操作数
变量名引用的是数据段内的偏移量。例如,如下变量varl 的声明表示,该变量的大小类型为字节,值为十六进制的 10:
.data
varl BYTE 10h
可以编写指令,通过内存操作数的地址来解析(查找)这些操作数。假设varl 的地址偏移量为10400h。如下指令将该变量的值复制到AL寄存器中:
mov al varl
指令会被汇编为下面的机器指令:
A0 00010400
这条机器指令的第一个字节是操作代码(即操作码(opcode))。剩余部分是var1的32位十六进制地址。虽然编程时有可能只使用数字地址,但是如同var1一样的符号标号会让使用内存更加容易。
另一种表示法。一些程序员更喜欢使用下面这种直接操作数的表达方式,因为,括号意味着解析操作:
mov al.[var1]
MASM 允许这种表示法,因此只要愿意就可以在程序中使用。由于多数程序(包括Microsoft的程序)印刷时都没有用括号,所以,本书只在出现算术表达式时才使用这种带括号的表示法:
mov al,[var1 +5]
(这就是直接偏移量操作数,将在4.1.8节中作为一个主题进行详细讨论。)
4.1.4 MOV指令
MOV指令将源操作数复制到目的操作数。作为数据传送(data transfer)指令,它几乎用在所有程序中。在它的基本格式中,第一个操作数是目的操作数,第二个操作数是源操作数:
MOV destination,source
其中,目的操作数的内容会发生改变,而源操作数不会改变。这种数据从右到左的移动与C++或Java 中的赋值语句相似:
dest = source
在几乎所有的汇编语言指令中,左边的操作数是目标操作数,而右边的操作数是源操作数。只要按照如下原则,MOV 指令使用操作数是非常灵活的。
●两个操作数必须是同样的大小。
●两个操作数不能同时为内存操作数。
●指令指针寄存器(IP、EIP或RIP)不能作为目标操作数。
下面是MOV指令的标准格式:
MOV reg, reg
MOV mem, reg
MOV reg, mem
MOV mem, imm
MOV reg, imm
内存到内存 单条MOV指令不能用于直接将数据从一个内存位置传送到另一个内存位置。相反,在将源操作数的值赋给内存操作数之前,必须先将该数值传送给一个寄存器:
.data
var1 WORD ?
var2 WORD ?
.code
mov ax, var1
mov var2, ax
在将整型常数复制到一个变量或寄存器时,必须考虑该常量需要的最少字节数。第1章的表 1-4 给出了无符号整型常数的大小,表 1-7给出了有符号整型常数的大小。
覆盖值
下述代码示例演示了怎样通过使用不同大小的数据来修改同一个32位寄存器。当oneWord 字传送到AX 时,它就覆盖了AL中已有的值。当 oneDword 传送到EAX时,它就覆盖了AX的值。最后,当0被传送到AX 时,它就覆盖了EAX的低半部分。
.data
oneByte BYTE 78h
oneWord WORD 1234h
oneDword DWORD 12345678h
.code
mov eax,0 ;EAX = 00000000h
mov al,oneByte ;EAX = 00000078h
mov ax,oneWord ;EAX = 00001234h
mov eax,oneDword ;EAX = 12345678h
mov ax,0 ;EAX = 12340000h
完整代码测试笔记:
;4.1.4_p75.asm MOV指令 内存到内存, 覆盖值 的示例, 第75页.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.data
var1 WORD 16
var2 WORD 32oneByte BYTE 78h
oneWord WORD 1234h
oneDword DWORD 12345678h.code
main PROCmov eax, 0mov ax, var1mov var2, axmov eax, 0 ;EAX = 00000000hmov al, oneByte ;EAX = 00000078hmov ax, oneWord ;EAX = 00001234hmov eax, oneDword ;EAX = 12345678hmov ax, 0 ;EAX = 12340000h INVOKE ExitProcess, 0
main ENDP
END main
4.1.5 整数的全零/符号扩展
1.把一个较小的值复制到一个较大的操作数
尽管 MOV指令不能直接将较小的操作数复制到较大的操作数中,但是程序员可以想办法解决这个问题。假设要将count(无符号,16位)传送到ECX(32位),可以先将ECX设置为0,然后将count传送到CX:
.data
count WORD 1
.code
mov ecx,0
mov cx, count
如果对一个有符号整数-16进行同样的操作会发生什么呢?
.data
signedVal SWORD -16 ;FFFOh(-16)
.code
mov ecx,0
mov cx, signedVal :ECX = 0000FFF0h(+65,520)
ECX中的值(+65520)与-16完全不同。但是,如果先将ECX设置为FFFFFFFFh然后再把 signedVal复制到CX,那么最后的值就是完全正确的:
mov ecx, 0FFFFFFFFh
mov cx, signedVal :ECX =FFFFFFFOh(-16)
本例的有效结果是用源操作数的最高位(1)来填充目的操作数 ECX 的高 16 位,这种技术称为符号扩展(sign extension)。当然,不能总是假设源操作数的最高位是1。幸运的是,Intel 的工程师在设计指令集时已经预见到了这个问题,因此,设置了 MOVZX 和MOVSX指令来分别处理无符号整数和有符号整数。
2.MOVZX指令
MOVZX指令(进行全零扩展并传送)将源操作数复制到目的操作数,并把目的操作数0 扩展到 16 位或 32 位。这条指令只用于无符号整数,有三种不同的形式:
MOVZX reg32, reg/mem8
MOVZX reg32, reg/mem16
MOVZX reg16, reg/mem8
(操作数符号含义见表 4-1。)在三种形式中,第一个操作数(寄存器)是目的操作数,第二个操作数是源操作数。注意,源操作数不能是常数。下例将二进制数10001111进行全零扩展并传送到 AX:
.data
byteVal BYTE 10001111b
.code
movzx ax, byteVal :AX=0000000010001111b
图4-1展示了如何将源操作数进行全零扩展,并送入16位目的操作数下面例子的操作数是各种大小的寄存器:
mov bx, 0A69Bh
movzx eax, bx ;EAX =0000A69Bh
movzx edx, bl ;EDX =0000009Bh
movzx cx, bl ;CX = 009Bh
下面例子的源操作数是内存操作数,执行结果是一样的:
.data
bytel BYTE 9Bh
wordl WORD 0A69Bh
.code
movzx eax, word1 ;EAX = 0000A69Bh
movzx edx, bytel ;EDX =0000009Bh
movzx cx, bytel ;CX = 009Bh
完整代码测试笔记:
;4.1.5_p76.asm 整数的全零 movzx指令示例.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.data
count WORD 1
byteVal BYTE 10001111b.code
main PROCmov ecx, 0 ;没有这行,下面一行执行后ecx的值不会是1mov cx, count ;ecx = 1movzx ax, byteVal ;ax = 0000000010001111b = 143d = 8Fh, eax的高16是随机值 EAX = 00AF008Fmovzx eax, byteVal ;eax = 143d = 8Fhmov bx, 0A69hmovzx eax, bx ;EAX = 0000A69Bhmovzx edx, bl ;EDX = 0000009Bhmovzx cx, bl ;ECX = 0000009BhINVOKE ExitProcess, 0
main ENDP
END main
3.MOVSX 指令
MOVSX指令(进行符号扩展并传送)将源操作数内容复制到目的操作数,并把目的操作数符号扩展到16位或32位。这条指令只用于有符号整数,有三种不同的形式:
movsx reg32, reg/mem8
movsx reg32, reg/mem16
movsx reg16, reg/mem8
操作数进行符号扩展时,在目的操作数的全部扩展位上重复(复制)长度较小操作数的最高位。下面的例子是将二进制数1000 1111b进行符号扩展并传送到AX:
.data
byteVal BYTE 10001111b
.code
movsx ax, byteVal ;AX=1111111110001111b
如图 4-2 所示,复制最低8 位,同时,将源操作数的最高位复制到目的操作数高 8位的每一位上。
如果一个十六进制常数的最大有效数字大于 7,那么它的最高位等于1。如下例所示,传送到 BX 的十六进制数值为A69B,因此,数字“A”就意味着最高位是1。(A69B 前面的0是一种方便的表示法,用于防止汇编器将常数误认为标识符。)
mov bx,0A69Bh
movsx eax,bx ;EAX = FFFFA69Bh
movsx edx, b1 ;EDX = FFFFFF9Bh
movsx cx, b1 ;CX = FF9Bh
完整代码测试笔记:
;4.1.5_p77.asm 整数的全零 movsx指令示例(进行答号扩展并传送).386
.model flat,stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.data
signedVal SWORD -16 ;FFFOh (-16)
byteVal BYTE 10001111b.code
main PROCmov ecx,0mov cx,signedVal ;ECX = 0000FFF0h(+65,520)mov ecx,0FFFFFFFFhmov cx,signedVal ;ECX = FFFFFFFOh(-16)movsx ax,byteVal ;AX = 1111111110001111bmov bx,0A698hmovsx eax,bx ;EAX = FFFFA69Bhmovsx edx,bl ;EDX = FFFFFF9Bhmovsx cx,bl ;CX = FF9BhINVOKE ExitProcess,0
main ENDP
END main
4.1.6 LAHF 和SAHF
指令LAHF(加载状态标志位到AH)指令将EFLAGS寄存器的低字节复制到AH。被复制的标志位包括:符号标志位、零标志位、辅助进位标志位、奇偶标志位和进位标志位。使用这条指令,可以方便地把标志位副本保管在变量中:
.data
saveflags BYTE ?
.code
lahf ;将标志位加载到AH
mov saveflags, ah ;用变量保存这些标志位
SAHF(保存AH内容到状态标志位)指令将AH内容复制到EFLAGS(或RFLAGS)寄存器低字节。例如,可以检索之前保存到变量中的标志位数值:
mov ah,saveflags ;加载被保存标志位到AH
sahf ;复制到FLAGS寄存器
完整代码测试笔记:
;4.1.6_p78.asm LAHF和SAHF指令 第78页
;LAHF(加载状态标志位到AH)指令将EFLAGS寄存器的低字节复制到AH。被复制的标志位包括:符号标志位、零标志位、辅助进位标志位、奇偶标志位和进位标志位。
;使用这条指令,可以方便地把标志位副本保管在变量中:
;SAHF(保存AH内容到状态标志位)指令将AH内容复制到EFLAGS(或RFLAGS)寄存器低字节。.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
saveflags BYTE ?.code
main PROClahf ;将标志位加载到AHmov saveflags,ah ;用变量保存这些标志位mov ah,saveflags ;加载被保存标志位到AHsahf ;复制到FLAGS寄存器INVOKE ExitProcess,0
main ENDP
END main
4.1.7 XCHG指令
XCHG(交换数据)指令交换两个操作数内容。该指令有三种形式:
XCHG reg, reg
XCHG reg, mem
XCHG mem, reg
除了XCHG指令不使用立即数作操作数之外,XCHG指令操作数的要求与MOV指令操作数要求(参见4.1.4 节)是一样的。在数组排序应用中,XCHG 指令提供了一种简单的方法来交换两个数组元素。下面是几个使用XCHG 指令的例子。
xchg ax, bx ;交换16位寄存器内容
xchg ah, al ;交换8位寄存器内容
xchg var1, bx ;交换16位内存操作数与BX寄存器内容
xchg eax, ebx ;交换32位寄存器内容
如果要交换两个内存操作数,则用寄存器作为临时容器,把MOV指令与XCHG指令一起使用:
mov ax, va11
xchg ax, va12
mov va11, ax
完整代码测试笔记:
;4.1.7_p78.asm XCHG指令
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
val1 WORD 16
val2 WORD 32.code
main PROCxchg ax,bx ;交换16位寄存器内容xchg ah,al ;交换8位寄存器内容xchg val1,bx ;交换16位内存操作数与BX寄存器内容xchg eax,ebx ;交换32位寄存器内容mov val1,16 ;还原初始val1的值mov ax,val1xchg ax,val2mov val1,axINVOKE ExitProcess,0
main ENDP
END main
4.1.8 直接-偏移量操作数
变量名加上一个位移就形成了一个直接-偏移量操作数。这样可以访问那些没有显式标记的内存位置。假设现有一个字节数组arrayB:
arrayB BYTE 10h, 20h, 30h, 40h, 50h
用该数组作为MOV指令的源操作数,则自动传送数组的第一个字节:
mov al, arrayB ;AL=10h
通过在arrayB偏移量上加1就可以访问该数组的第二个字节:
mov al, [arrayB+1] ;AL=20h
如果加2就可以访问该数组的第三个字节:
mov al, [arrayB+2] ;AL=30h
形如arrayB+1一样的表达式通过在变量偏移量上加常数来形成所谓的有效地址。有效地址外面的括号表明,通过解析这个表达式就可以得到该内存地址指示的内容。汇编器并不要求在地址表达式之外加括号,但为了清晰明了,本书还是强烈建议使用括号。
MASM 没有内置的有效地址范围检查。在下面的例子中,假设数组arrayB 有 5个字节,而指令访问的是该数组范围之外的一个内存字节。其结果是一种难以发现的逻辑错误,因此,在检查数组引用时要非常小心:
mov al, [arrayB+20] ;AL=?
字和双字数组 在16位的字数组中,每个数组元素的偏移量比前一个多2个字节。这就是为什么在下面的例子中,数组ArrayW加2才能指向该数组的第二个元素:
.data
arrayW WORD 100h, 200h, 300h
.code
mov ax, arrayW ;AX=100h
mov ax, [arrayW+2] ;AX=200h
同样,如果是双字数组,则第一个元素偏移量加4才能指向第二个元素:
.data
arrayD DWORD 10000h, 20000h
.code
mov eax, arrayD ;EAX=10000h
mov eax, [arrayD+4] ;EAX=20000h
完整代码测试笔记:
;4.1.8_p79.asm 直接-偏移量操作数.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
arrayB BYTE 10h,20h,30h,40h,50h
arrayW WORD 100h,200h,300h
arrayD DWORD 10000h,20000h.code
main PROCmov eax,0mov al,arrayB ;AL = 10hmov al,[arrayB+1] ;AL = 20hmov al,[arrayB+2] ;AL = 30hmov ax,arrayW ;AX = 100hmov ax,[arrayW+2] ;AX = 200hmov eax,arrayD ;EAX = 10000hmov eax,[arrayD+4] ;EAX = 20000hINVOKE ExitProcess,0
main ENDP
END main
4.1.9示例程序(Moves)
该程序中包含了本章迄今介绍的所有指令,包括:MOV、XCHG、MOVSX 和MOVZX,展示了字节、字和双字是如何受到它们的影响。同时,程序中还包括了一些直接-偏移量操作数。
;数据传送示例 (Moves.asm)
;数据传送示例(Moves.asm)
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
val1 WORD 1000h
val2 WORD 2000h
arrayB BYTE 10h,20h,30h,40h,50h
arrayW WORD 100h,200h,300h
arrayD DWORD 10000h,20000h.code
main PROC;演示 MOVZX指令mov bx,0A69Bhmovzx eax,bx ;EAX = 0000A69Bhmovzx edx,bl ;EDX = 0000009Bhmovzx cx,bl ;CX = 009Bh;演示MOVSX指令mov bx,0A69Bhmovsx eax,bx ;EAX = FFFFA69Bhmovsx edx,bl ;EDX = FFFFFF9Bhmov bl,7Bhmovsx cx,bl ;CX = 007Bh;内存-内存的交换mov ax,val1 ;AX = 1000hxchg ax,val2 ;AX = 2000h,va12 = 1000hmov val1,ax ;vall=2000h;直接-偏移量寻址(字节数组)mov al,arrayB ;AL = 10hmov al,[arrayB+1] ;AL = 20hmov al,[arrayB+2] ;AL = 30h;直接-偏移量寻址(字数组)mov ax,arrayW ;AX = 100hmov ax,[arrayW+2] ;AX = 200h;直接-偏移量寻址(双字数组)mov eax,arrayD ;EAX = 10000hmov eax,[arrayD+4] ;EAX = 20000hmov eax,[arrayD+4] ;EAX = 20000hINVOKE ExitProcess,0
main ENDP
END main
该程序不会产生屏幕输出,但是可以用调试器(debugger)运行。
在Visual Studio调试器中显示CPU标志位
在调试期间显示CPU 状态标志位时,在 Debug菜单中选择Windows子菜单,再选择Register。在Register 窗口,右键选择下拉列表中的Flags。要想查看这些菜单选项,必须调试程序。下表是Register 窗口中用到的标志位符号:
每个标志位有两个值:0(清除)或1(置位)。示例如下:
调试程序期间,当逐步执行代码时,指令只要修改了标志位的值,则标志位就会显示为红色。这样就可以通过单步执行来了解指令是如何影响标志位的,并可以密切关注这些标志位值的变化。
4.1.10 本节回顾
1.操作数的三种基本类型是什么?
答:寄存器操作数、立即数操作数和内存操作数
2.(真/假):MOV指令的目的操作数不能为段寄存器。
答:假,例如:mov ax, 1000h
mov cs,ax
3.(真/假):MOV指令中的第二个操作数是目的操作数
答:假, mov 目的操作数,源操作数
4.(真/假):EIP寄存器不能作为 MOV 指令的目的操作数。
答:真
5.Intel使用的操作数符号中,reg/mem32的含义是什么?
答:32位寄存器或内存操作数
6.Intel使用的操作数符号中,imm16的含义是什么?
答:16位立即(常数)操作数
4.2加法和减法
算术运算是汇编语言中一个大得令人惊讶的主题!本节重点在于加法和减法,乘法和除法将在第 7章讨论,浮点运算将在第 12章讨论。
先从最简单、最有效的指令开始:INC(增加)和 DEC(减少)指令,即加1和减 1。然后是能提供更多操作的 ADD、SUB 和NEG(非)指令。最后,将讨论算术运算指令如何影响 CPU 状态标志位(进位位、符号位、零标志位等)。请记住,汇编语言的细节很重要。
4.2.1 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 指令不会影响进位标志位(这还真让人吃惊)。
完整代码测试笔记:
;4.2.1_p81.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
myWord WORD 0FFFFh.code
main PROCinc myWord ;myWord = 0hmov bx,myWorddec bx ;BX = FFFFhadd bx,1 ;BX = 0hINVOKE ExitProcess,0
main ENDP
END main
运行调试:
inc myWord指令执行后,myWord = 0,进位标志没有变化。
add bx,1指令执行后,BX=0,进位标志有变化了。
4.2.2 ADD指令
ADD指令将长度相同的源操作数和目的操作数进行相加操作。语法如下:
ADD dest, source
在操作中,源操作数不能改变,相加之和存放在目的操作数中。该指令可以使用的操作数与MOV指令相同(参见4.1.4节)。下面是两个32位整数相加的短代码示例:
.data
var1 DWORD 10000h
var2 DWORD 20000hmov eax,var1 ;EAX = 10000h
add eax,var2 ;EAX = 30000h
标志位进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标志位根据存人目标操作数的数值进行变化。4.2.6节将介绍标志位如何发生作用。
完整代码测试笔记:
;4.2.2_p81.asm.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
var1 DWORD 10000h
var2 DWORD 20000h.code
main PROCmov eax,var1 ;EAX = 10000hadd eax,var2 ;EAX = 30000hINVOKE ExitProcess,0
main ENDP
END main
运行结果:
4.2.3 SUB 指令
SUB dest, source
下面是两个32位整数相减的短代码示例:
.data
var1 DWORD 30000h
var2 DWORD 10000h
.code
mov eax,var1 ;EAX = 30000h
sub eax,var2 ;EAX = 20000h
标志位 进位、零标位、符号标志位、溢出标志位、辅助进位标志位和奇偶标志位根据存人目标操作数的数值进行变化。
完整代码测试笔记:
;4.2.3_p82.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
var1 DWORD 30000h
var2 DWORD 10000h.code
main PROCmov eax,var1 ;EAX = 30000hsub eax,var2 ;EAX = 20000hINVOKE ExitProcess,0
main ENDP
END main
运行调试:
4.2.4 NEG指令
NEG(非)指令通过把操作数转换为其二进制补码,将操作数的符号取反。下述操作数可以用于该指令:
NEG reg
NEG mem
(将目标操作数按位取反再加1,就可以得到这个数的二进制补码。
标志位 进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标志位根据存入目标操作数的数值进行变化。
完整代码测试笔记:
;4.2.4_p82.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
var1 DWORD 1.code
main PROCmov eax,var1neg eaxneg var1INVOKE ExitProcess,0
main ENDP
END main
运行调试:
4.2.5 执行算术表达式
使用 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
完整代码测试笔记:
;4.2.5_p82.asm 执行算术表达式 Rval = -Xval + (Yval - Zval).386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
Rval SDWORD ?
Xval SDWORD 26
Yval SDWORD 30
Zval SDWORD 40.code
main PROC;first term; -Xvalmov eax,Xvalneg eax ;EAX = -26;second term: (Yval - Zval)mov ebx,Yvalsub ebx,Zval ;EBX = -10;add the terms and store:add eax,ebxmov Rval,eax ;-36INVOKE ExitProcess,0
main ENDP
END main
运行调试:
4.2.6 加减法影响的标志位
执行算术运算指令时,常常想要了解结果。它是负数、正数还是零?对目的操作数来说,它是太大,还是太小?这些问题的答案有助于发现计算错误,否则可能会导致程序的错误行为。检查算术运算结果使用的是 CPU 状态标志位的值,同时,这些值还可以触发条件分支指令,即基本的程序逻辑工具。下面是对状态标志位的简要概述:
●进位标志位意味着无符号整数溢出。比如,如果指令目的操作数为8位,而指令产生的结果大于二进制的1111 1111,那么进位标志位置1。
●溢出标志位意味着有符号整数溢出。比如,指令目的操作数为 16位,但其产生的负数结果小于十进制的 -32 768,那么溢出标志位置1。
●零标志位意味着操作结果为0。比如,如果两个值相等的操作数相减,则零标志位置1。
●符号标志位意味着操作产生的结果为负数。如果目的操作数的最高有效位(MSB)置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
图4-3演示了在0FFh上加1时,操作数的位是如何变化的。AL最高有效位的进位复制到进位标志位。
另一方面,如果AX的值为00FFh,则对其进行加1操作后,和数不会超过16位,那么进位标志位清0:
mov ax, 00FFh
add ax, 1 ;AX = 0100h, CF = 1
但是,如果AX的值为FFFFh,则对其进行加1操作后,AX的高位就会产生进位:
mov ax, 0FFFFh
add ax, 1 ;AL = 0000, CF = 1
减法和进位标志位 从较小的无符号整数中减去较大的无符号整数时,减法操作就会将进位标志位置1。图4-4说明了,操作数为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
计算过程如下:
奇偶标志位 目的操作数最低有效字节中1的个数为偶数时,奇偶(PF)标志位置1。下例中,ADD 和 SUB 指令修改了 AL 的奇偶性:
mov al, 10001100b
add al, 00000010b ;AL = 1000 1110, PF = 1
sub al, 10000000b ;AL = 0000 1110, 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, SF = 1
从机器的角度来看,符号标志位是目的操作数高位的副本。下面的例子表示产生了负数结果后,BL 中的十六进制的值:
mov bl, 1 ;BL = 01h
sub bl, 2 ;BL = FFh(-1), SF = 1
溢出标志位 有符号数算术操作结果与目的操作数相比,如果发生上溢或下溢,则溢出标志位置 1。例如,在第 1 章就了解到,8 位有符号整数的最大值为+127,再加 1 就会溢出:
mov al, +127
add al, 1 ;OF = 1
同样,最小的负数为-128,再减 1就发生下溢。如果目的操作数不能容纳一个有效算术运算结果,那么溢出标志位置1:
mov al, -128
sub a1, 1 ;OF = 1
加法测试 两数相加时,有个很简单的方法可以判断是否发生溢出。溢出发生的情况有:
●两个正数相加,结果为负数
●两个负数相加,结果为正数
如果两个加数的符号相反,则不会发生溢出。
硬件如何检测溢出 加法或减法操作后,CPU用一种有趣的机制来检测溢出标志位的状态。计算结果的最高有效位产生的进位与结果的最高位进行异或操作,异或的结果存人溢出标志位。如图4-5所示,两个8位二进制数10000000和11111110相加,产生进位 CF=1,和数最高位(位7)=0,即1XOR0=1,则OF=1。
NEG指令 如果NEG指令的目的操作数不能正确存储,则该结果是无效的。例如,AL 中存放的是-128,对其求反,正确的结果为+128,但是这个值无法存人AL。则溢出标志位置1就表示AL中存放的是一个无效的结果:
mov al, -128 ;AL = 1000 0000b
neg al ;AL = 1000 0000b, OF = 1
反之,如果对 +127 求反,结果是有效的,则溢出标志位清 0:
mov al, +127 ;AL = 1000 0000b
neg al ;AL = 1000 0000b, OF = 0
CPU 如何知道一个算术运算是有符号的还是无符号的?答案看上去似乎有点愚蠢它不知道!在算术运算之后,不论标志位是否与之相关,CPU都会根据一组布尔规则来设置所有的状态标志位。程序员要根据执行操作的类型,来决定哪些标志位需要分析哪些可以忽略。
4.2.7 示例程序(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 SDWORD 40.code
main PROC;INC和DECmov ax,1000hinc ax ;ax = 1001hdec ax ;ax = 1000h;表达式:Rval=-Xval+(Yval-Zval)mov eax,Xval neg eax ;eax = -26mov ebx,Yvalsub ebx,Zval ;ebx = -10add eax,ebxmov Rval,eax ;Rval = -36;零标志位示例mov cx,1sub cx,1 ;ZF = 1mov ax,0FFFFhinc ax ;ZF = 1;符号标志位示例mov cx,0sub cx,1 ;SF = 1mov ax,7FFFhadd ax,2 ;SF = 1;进位标志位示例mov al,0FFhadd al,1 ;CF = 1, AL = 00;溢出标志位示例mov al,+127add al,1 ;OF = 1mov al,-128sub al,1 ;OF = 1INVOKE ExitProcess,0
main ENDP
END main
4.2.8 本节回顾
问题1~问题 5 使用如下数据:
.data
val1 BYTE 10h
val2 WORD 8000h
val3 OFFFFh DWORD
val4 WORD 7FFFh
1.编写一条指令实现 val2加1。
答:inc val2
2.编写一条指令实现从 EAX 中减去 va13。
答:sub eax,val3
3.编写指令实现从 val2 中减去 val4。
答:mov ax,val4
sub val2,ax
4.如果用 ADD 指令实现 val2 加1,则进位标志位和符号标志位的值是多少?
答:进位标志位CF = 0,答号标志位SF=1
5.如果用ADD 指令实现 val4 加1,则溢出标志位和符号标志位的值是多少?
答:溢出标志位OF=1,符号标志位SF=1
6.有如下程序段,每条指令执行后,写出进位标志位、符号标志位、零标志位和溢出标志位的值:
mov ax, 7FF0h
add al,10h ;a. CF = 1 SF = 0 ZF = 1 OF = 0
add ah,1 ;b. CF = 0 SF = 1 ZF = 0 OF = 1
add ax,2 ;c. CF = 0 SF = 1 ZF = 0 OF = 0
完整的测试代码如下:
;4.2.8_6.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
val1 BYTE 10h
val2 WORD 8000h
val3 DWORD 0FFFFh
val4 WORD 7FFFh.code
main PROCmov ax,7FF0hadd al,10h ;a. CF = 1 SF = 0 ZF = 1 OF = 0add ah,1 ;b. CF = 0 SF = 1 ZF = 0 OF = 1add ax,2 ;c. CF = 0 SF = 1 ZF = 0 OF = 0INVOKE ExitProcess,0
main ENDP
END main
4.3 与数据相关的运算符和伪指令
此外,LABEL伪指令可以用不同的大小类型来重新定义同一个变量。本章的运算符和伪指令只代表MASM支持的一小部分运算符,完整内容参见附录D。
4.3.1 OFFSET运算符
OFFSET运算符返回数据标号的偏移量。这个偏移量按字节计算,表示的是该数据标号距离数据段起始地址的距离。图 4-6 所示为数据段内名为myByte的变量。
OFFSET 示例
在下面的例子中,将用到如下三种类型的变量:
.data
bVal BYTE ?
wVal WORD ?
dVal DWORD ?
dVal2 DWORD ?
假设bVal 在偏移量为00404000(十六进制)的位置,则 OFFSET运算符返回值如下:
mov esi, OFFSET bVal ;ESI=0040 4000h
mov esi, OFFSET wVal ;ESI=0040 4001h
mov esi, OFFSET dVal ;ESI=0040 403h
mov esi, OFFSET dVal2 ;ESI=0040 4007h
OFFSET也可以应用于直接-偏移量操作数。设myArray包含5个16位的字。下面的MOV指令首先得到myArray的偏移量,然后加4,再将形成的结果地址直接传送给ESI。因此,现在可以说ESI指向数组中的第3个整数。
.data
myArray WORD 1, 2, 3, 4, 5
.code
mov esi, OFFSET myArray + 4
还可以用一个变量的偏移量来初始化另一个双字变量,从而有效地创建一个指针。如下例所示,pArray就指向bigArray的起始地址:
.data
bigArray DWORD 500 DUP(?)
pArray DWORD bigArray
下面的指令把该指针的值加载到ESI 中,因此,这个ESI 寄存器就可以指向数组的起始地址:
mov esi, pArray
完整的测试代码如下:
;4.3.1_p87.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
bVal BYTE ?
wVal WORD ?
dVal DWORD ?
dVal2 DWORD ?
myArray WORD 1,2,3,4,5
bigArray DWORD 500 DUP(?)
pArray DWORD bigArray.code
main PROCmov esi,offset bVal ;ESI=002F 4000hmov esi,offset wVal ;ESI=002F 4001hmov esi,offset dVal ;ESI=002F 4003hmov esi,offset dVal2 ;ESI=002F 4007hmov esi,offset myArray ;ESI=002F 400Bhmov esi,offset myArray+4 ;ESI=002F 400Fh;下面两行效果一样mov esi,offset bigArray ;ESI=002F 4015hmov esi,pArray ;ESI=002F 4015hINVOKE ExitProcess,0
main ENDP
END main
4.3.2 ALIGN 伪指令
ALIGN伪指令将一个变量对齐到字节边界、字边界、双字边界或段落边界。语法如下:
ALIGN bound
Bound 可取值有:1、2、4、8、16。当取值为1时,则下一个变量对齐于1字节边界(默认情况)。当取值为2时,则下一个变量对齐于偶数地址。当取值为4时,则下一个变量地址为 4 的倍数。当取值为16 时,则下一个变量地址为 16 的倍数,即一个段落的边界。为了满足对齐要求,汇编器会在变量前插入一个或多个空字节。为什么要对齐数据?因为,对于存储于偶地址和奇地址的数据来说,CPU 处理偶地址数据的速度要快得多。
下述例子中,bVal 处于任意位置,但其偏移量为00404000。在wVal之前插入ALIGN2 伪指令,这使得wVal对齐于偶地址偏移量:
bVal BYTE ? ; 0040 4000h
ALIGN 2
wVal WORD ? ; 0040 4002h
bVal2 BYTE ? ; 0040 4004h
ALIGN 4
dVal DWORD ? ; 0040 4008h
dVal2 DWORD ? ; 0040 400Ch
请注意,dVal 的偏移量原本是0040 4005,但是ALIGN4伪指令使它的偏移量成为00404008。
完整的测试代码如下:
;4.3.2_p88.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
bVal BYTE ?
ALIGN 2
wVal WORD ?
bVal2 BYTE ?
ALIGN 4
dVal DWORD ?
dVal2 DWORD ?
.code
main PROCmov esi,offset bVal ;0040 4000hmov esi,offset wVal ;0040 4002hmov esi,offset bVal2 ;0040 4004h;如果注释ALIGN 4, offset dVal为0040 4005h,再注释ALIGN 2 offset dVal为0040 4004hmov esi,offset dVal ;0040 4008h mov esi,offset dVal2 ;0040 400ChINVOKE ExitProcess,0
main ENDP
END main
4.3.3 PTR运算符
PTR运算符可以用来重写一个已经被声明过的操作数的大小类型。只要试图用不同于汇编器设定的大小属性来访问操作数,那么这个运算符就是必需的。
例如,假设想要将一个双字变量myDouble 的低16 位传送给AX。由于操作数大小不匹配,因此,汇编器不会允许这种操作:
.data
myDouble DWORD 12345678h
.code
mov ax,myDouble
但是,使用WORDPTR运算符就能将低位字(5678h)送人AX:
mov ax, WORD PTR myDouble
为什么送人 AX 的不是1234h?因为,x86 处理器采用的是小端存储格式(参见3.4.9节),即低位字节存放于变量的起始地址。如图 4-7所示,用三种方式表示 myDouble 的内存布局:第一列是一个双字,第二列是两个字(5678h、1234h),第三列是四个字节(78h、56h、34h、12h)。
不论该变量是如何定义的,都可以用三种方法中的任何一种来访问内存。比如,如果myDouble的偏移量为0000,则以这个偏移量为首地址存放的16位值是5678h。同时也可以检索到1234h,其字地址为myDouble+2,指令如下:
mov ax, BYTE PTR [myDouble+2] ;1234h
同样,用BYTE PTR运行符能够把myDouble的单个字节传送到BL:
mov b1, BYTE PTR myDouble ;78h
注意,PTR 必须与一个标准汇编数据类型一起使用,这些类型包括:BYTE、SBYTEWORD、SWORD、DWORD、SDWORD、FWORD、QWORD或TBYTE。
将较小的值送入较大的目的操作数 程序可能需要将两个较小的值送入一个较大的目的操作数。如下例所示,第一个字复制到 EAX 的低半部分,第二个字复制到高半部分。而DWORD PTR 运算符能实现这种操作:
完整的测试代码如下:
;4.3.3_p89.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
wordList WORD 5678h,1234h.code
main PROCmov eax, DWORD PTR wordList ;EAX = 12345678hINVOKE ExitProcess,0
main ENDP
END main
运行调试:
4.3.4 TYPE运算符
TYPE运算符返回变量单个元素的大小,这个大小是以字节为单位计算的。比如,TYPE为字节,返回值是1;TYPE为字,返回值是2;TYPE为双字,返回值是4;TYPE为四字,返回值是 8。示例如下:
.data
var1 BYTE ?
var2 WORD ?
var3 DWORD ?
var4 QWORD ?
下表是每个 TYPE 表达式的值。
表达式 | 值 | 表达式 | 值 |
TYPE var1 | 1 | TYPE var3 | 4 |
TYPE var2 | 2 | TYPE var4 | 8 |
完整的测试代码如下:
;4.3.4_p89.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
var1 BYTE ?
var2 WORD ?
var3 DWORD ?
var4 QWORD ?.code
main PROC mov eax, TYPE var1 ;eax = 1mov eax, TYPE var2 ;eax = 2mov eax, TYPE var3 ;eax = 4mov eax, TYPE var4 ;eax = 8INVOKE ExitProcess,0
main ENDP
END main
4.3.5 LENGTHOF运算符
LENGTHOF运算符计算数组中元素的个数,元素个数是由数组标号同一行出现的数值来定义的。示例如下:
.data
byte1 BYTE 10,20,30
array1 WORD 30 DUP(?), 0, 0
array2 WORD 5 DUP(3 DUP(?))
array3 DWORD 1,2,3,4
digitStr BYTE "12345678",0
如果数组定义中出现了嵌套的DUP 运算符,那么LENGTHOF返回的是两个数值的乘积。下表列出了每个 LENGTHOF 表达式返回的数值。
表达式 | 值 | 表达式 | 值 |
LENGTHOF byte1 | 3 | LENGTHOF array3 | 4 |
LENGTHOF array1 | 30+2 | LENGTHOF digitStr | 9 |
LENGTHOF array2 | 5*3 |
如果数组定义占据了多个程序行,那么LENGTHOF只针对第一行定义的数据。比如有如下数据,则LENGHTOF myArray返回值为5:
myArray BYTE 10, 20, 30, 40, 50
BYTE 60, 70, 80, 90, 100
另外,也可以在第一行结尾处用逗号,并在下一行继续进行数组初始化。若有如下数据定义,LENGHOF myArray返回值为10:
myArray BYTE 10, 20, 30, 40, 50,
60, 70, 80, 90, 100
完整的测试代码如下:
;4.3.5_p90.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
byte1 BYTE 10,20,30
array1 WORD 30 DUP(?), 0, 0
array2 WORD 5 DUP(3 DUP(?))
array3 DWORD 1,2,3,4
digitStr BYTE "12345678",0
array4 BYTE 10,20,30,40,50BYTE 60,70,80,90,100
array5 BYTE 10,20,30,40,50,60,70,80,90,100.code
main PROCmov eax, LENGTHOF byte1 ;eax = 3mov eax, lengthof array1 ;eax = 30+2 = 32mov eax, LENGTHOF array2 ;eax = 5*3 = 15mov eax, LENGTHOF array3 ;eax = 4mov eax, LENGTHOF digitStr ;eax = 9mov eax, LENGTHOF array4 ;eax = 5mov eax, LENGTHOF array5 ;eax = 10INVOKE ExitProcess,0
main ENDP
END main
4.3.6 SIZEOF运算符
SIZEOF运算符返回值等于LENGTHOF 与TYPE 返回值的乘积。如下例所示,intArray数组的TYPE=2LENGTHOF=32,因此,SIZEOF intArray = 64:
完整的测试代码如下:
;4.3.6_p90.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
intArray WORD 32 DUP(0).code
main PROCmov eax,SIZEOF intArray ;eax = 64INVOKE ExitProcess,0
main ENDP
END main
4.3.7 LABEL 伪指令
LABEL 伪指令可以插入一个标号,并定义它的大小属性,但是不为这个标号分配存储空间。LABEL 中可以使用所有的标准大小属性,如BYTE、WORD、DWORD、QWORD或TBYTE。LABEL常见的用法是,为数据段中定义的下一个变量提供不同的名称和大小属性。如下例所示,在变量val32 前定义了一个变量,名称为val16,属性为 WORD:
.data
val16 LABEL WORD
val32 DWORD 12345678h
.code
mov ax,val16
mov dx,[val16+2]
val16与val32共享同一个内存位置。LABEL伪指令自身不分配内存。有时需要用两个较小的整数组成一个较大的整数,如下例所示,两个 16 位变量组成一个32 位变量并加载到 EAX 中:
完整的测试代码如下:
;4.3.7_p90.asm .386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
val16 LABEL WORD
val32 DWORD 12345678hLongValue LABEL DWORD
val1 WORD 5678h
val2 WORD 1234h.code
main PROCmov ax,val16 ;ax = 5678hmov dx,[val16+2] ;dx = 1234hmov eax,LongValue ;eax = 12345678hINVOKE ExitProcess,0
main ENDP
END main
4.3.8 本节回顾
1.(真/假):OFFSET 运算符总是返回一个 16 位的数值。
答:假,返回数据标号的偏移量
2.(真/假):PTR运算符返回变量的32位地址。
答:假,返回变量操作类型的大小地址。
3.(真/假):对双字操作数,TYPE 运算符返回值为 4。
答:真
4.(真/假):LENGTHOF运算符返回操作数的字节数。
答:假,LENGTHOF运算符返回操作数的元素个数。
5.(真/假):SIZEOF运算符返回操作数的字节数。
答:真