当前位置: 首页 > news >正文

《汇编语言:基于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运算符返回操作数的字节数。

答:真

相关文章:

  • Python----OpenCV(图像増强——图像平滑、均值滤波、高斯滤波、中值滤波、双边滤波)
  • RealSense 相机 | 读取IMU | 解决权限问题 | 提供示例程序
  • 阿里云无影:开启云端办公娱乐新时代
  • Re:从零开始的地址映射基本分页存储管理方式(考研向)
  • 设计模式 - 抽象工厂
  • 域名解析(DNS)相关
  • Typora - Typora 表格删除列与行
  • 光伏电站 “智慧大脑”:安科瑞 Acrel-1000DP 分布式监控系统技术解析
  • react gsap动画库使用详解之text文本动画
  • zlibrary镜像网站,zlibrary中文入口(持续更新)
  • 【Java开发日记】我们详细讲解一下 Java 中 new 一个对象的流程
  • JavaWeb学习——day8(MVC模式与session、cookie)
  • Few-Shot革命:PhotoDoodle艺术图像编辑实战——零样本门槛玩转3D/流体/手绘特效
  • 软件项目管理挣值计算
  • Spring-去除IDEA中的黄色波浪线提示
  • 错误 C2365 “SID”: 重定义;以前的定义是“typedef”
  • LNMP一键自动化部署
  • Three.js入门第一步:两种方式搭建你的3D项目[特殊字符]️
  • 通过使用gitee发布项目到Maven中央仓库最新教程
  • 以产教协同推进老年生活照护实训室虚拟仿真建设策略
  • 网站域名使用费用/百度指数免费添加
  • 新乡网站建设策划/外贸网站制作
  • 东莞常平碧桂园铂悦府/seo推广要多少钱
  • ( )是网站可以提供给用户的价值/精准客户信息一条多少钱
  • 视频门户网站建设方案/seo推广沧州公司电话
  • 广州响应式网站建设/公司网络营销实施计划