第4章 数据操作相关运算符和指令

酥酥 发布于 2022-05-02 117 次阅读


1.汇编语言操作数类型

x86 的指令格式为:

[label:] mnemonic [operands][ ;comment ]

指令包含的操作数个数可以是:0 个,1 个,2 个或 3 个。这里,为了清晰起见,省略掉标号和注释:

mnemonic
mnemonic [destination]
mnemonic [destination] , [source]
mnemonic [destination] , [source-1] , [source-2]

操作数有 3 种基本类型:

  • 立即数——用数字文本表达式
  • 寄存器操作数——使用 CPU 内已命名的寄存器
  • 内存操作数——引用内存位置


下表说明了标准操作数类型,它使用了简单的操作数符号(32 位模式下),这些符号来自 Intel 手册并进行了改编。本教程将用这些符号来描述每条指令的语法。

操作数说明
reg88 位通用寄存器:AH、AL、BH、BL、CH、CL、DH、DL
reg1616 位通用寄存器:AX、BX、CX、DX、SI、DI、SP、BP
reg3232 位通用寄存器:EAX、EEX、ECX、EDX、ESI、EDI、ESP、EBP
reg通用寄存器
sreg16 位段寄存器:CS、DS、SS、ES、FS、GS
imm8 位、16 位或 32 位立即数
imm88 位立即数,字节型数值
imm1616 位立即数,字类型数值
imm3232 位立即数,双字型数值
reg/mem88 位操作数,可以是 8 位通用寄存器或内存字节
reg/mem1616 位立即数,可以是 16 位通用寄存器或内存字
reg/mem3232 位立即数,可以是 32 位通用寄存器或内存双字
mem8位、16 位或 32 位内存操作数

直接内存操作数

变量名引用的是数据段内的偏移量。例如,如下变量 varl 的声明表示,该变量的大小类型为字节,值为十六进制的10:

				
					.data
var1 BYTE 10h
				
			

可以编写指令,通过内存操作数的地址来解析(查找)这些操作数。假设 var1 的地址偏移量为 10400h。如下指令将该变量的值复制到 AL 寄存器中:

				
					mov al var1
				
			

MASM 允许这种表示法,因此只要愿意就可以在程序中使用。由于多数程序(包括 Microsoft 的程序)印刷时都没有用括号,所以,本书只在出现算术表达式时才使用这种带括号的表示法:

				
					mov al,[var1 + 5]
				
			

2.汇编语言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
				
			

提示:在将整型常数复制到一个变量或寄存器时,必须考虑该常量需要的最少字节数。

覆盖值

下述代码示例演示了怎样通过使用不同大小的数据来修改同一个 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=OOOOOOOOh
mov al,oneByte                            ;EAX=00000078h
mov ax,oneWord                            ;EAX=00001234h
mov eax,oneDword                          ;EAX=12345678h
mov ax, 0                                 ;EAX=12340000h
				
			

3.汇编语言MOVZX和MOVSX指令

尽管 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      ; FFF0h (-16)
.code
mov ecx,0
mov cx,signedVal         ; ECX = 0000FFF0h(+ 65,52 0)
				
			

ECX 中的值(+65 520)与 -16 完全不同。但是,如果先将 ECX 设置为 FFFFFFFFh,然后再把 signedVal 复制到 CX,那么最后的值就是完全正确的:

				
					mov ecx,0FFFFFFFFh
mov cx,signedVal    ;ECX = FFFFFFF0h(-16)
				
			

本例的有效结果是用源操作数的最高位(1)来填充目的操作数 ECX 的高 16 位,这种技术称为符号扩展(sign extension)。当然,不能总是假设源操作数的最高位是 1。幸运的是,Intel 的工程师在设计指令集时已经预见到了这个问题,因此,设置了 MOVZX 和 MOVSX 指令来分别处理无符号整数和有符号整数。

MOVZX 指令

MOVZX 指令(进行全零扩展并传送)将源操作数复制到目的操作数,并把目的操作数 0 扩展到 16 位或 32 位。这条指令只用于无符号整数,有三种不同的形式:

				
					MOVZX reg32,reg/mem8
MOVZX reg32,reg/mem16
MOVZX reg16,reg/mem8
				
			

在三种形式中,第一个操作数(寄存器)是目的操作数,第二个操作数是源操作数。注意,源操作数不能是常数。下例将二进制数 1000 1111 进行全零扩展并传送到 AX:

				
					.data
byteVal BYTE 10001111b
.code
movzx ax,byteVal ;AX = 0000000010001111b
				
			

下图展示了如何将源操作数进行全零扩展,并送入 16 位目的操作数。

下面例子的操作数是各种大小的寄存器:

				
					mov bx, 0A69Bh
movzx eax, bx     ;EAX = 0000A69Bh
movzx edx, bl     ;EDX = 0000009Bh
movzx cx, bl     ;CX = 009Bh
				
			

下面例子的源操作数是内存操作数,执行结果是一样的:

				
					.data
byte1 BYTE  9Bh
word1 WORD 0A69Bh
.code
movzx eax, word1 ;EAX = 0000A69Bh
movzx edx, byte1 ;EDX = 0000009Bh
movzx ex, byte1     ;CX = 009Bh
				
			

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
				
			

如下图所示,复制最低 8 位,同时,将源操作数的最高位复制到目的操作数高 8 位的每一位上。

如果一个十六进制常数的最大有效数字大于 7,那么它的最高位等于 1。如下例所示,传送到 BX 的十六进制数值为 A69B,因此,数字“A”就意味着最高位是 1。(A69B 前面的 0 是一种方便的表示法,用于防止汇编器将常数误认为标识符。)

4.汇编语言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 寄存器
				
			

5.汇编语言XCHG指令:交换两个操作数内容

XCHG(交换数据)指令交换两个操作数内容。该指令有三种形式:

				
					XCHG reg, reg
XCHG reg, mem
XCHG mem, reg
				
			

除了 XCHG 指令不使用立即数作操作数之外,XCHG 指令操作数的要求与《MOV指令》一节中介绍的 MOV 指令操作数要求是一样的。

在数组排序应用中,XCHG 指令提供了一种简单的方法来交换两个数组元素。下面是几个使用 XCHG 指令的例子。

				
					xchg ax,bx      ;交换 16 位寄存器内容
xchg ah,al      ;交换 8 位寄存器内容
xchg var1,bx    ;交换 16 位内存操作数与 BX 寄存器内容
xchg eax,ebx    ;交换 32 位寄存器内容
				
			

如果要交换两个内存操作数,则用寄存器作为临时容器,把 MOV 指令与 XCHG 指令一起使用:

				
					mov ax,val1
xchg ax,val2
mov val1,ax
				
			

6.汇编语言直接偏移量操作数

变量名加上一个位移就形成了一个直接 – 偏移量操作数。这样可以访问那些没有显式标记的内存位置。假设现有一个字节数组 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 l0000h,20000h
.code
mov eax, arrayD                            ;EAX = 10000h
mov eax,[arrayD+4]                         ;EAX = 20000h
				
			

7.汇编语言数据传送示例

该程序中包含了本章迄今介绍的所有指令,包括:MOV、XCHG、MOVSX 和 MOVZX,展示了字节、字和双字是如何受到它们的影响。同时,程序中还包括了一些直接 – 偏移量操作数。

				
					;数据传送示例
.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,0A69Bh
    movzx eax,bx        ;EAX = 0000A69Bh
    movzx edx,bl        ;EDX = 0000009Bh
    movzx cx,bl         ;CX     = 009Bh
;演示 MOVSX 指令
    mov bx,0A69Bh
    movsx eax,bx        ;EAX = FFFFA69Bh
    movsx edx,bl        ;EDX = FFFFFF9Bh
    mov bl,7Bh
    movsx cx,bl         ;CX = 007Bh
;内存-内存的交换
    mov ax,val1         ;AX = 1000h
    xchg ax val2        ;AX = 2000h,val2 = 1000h
    mov val1,ax         ;val1 = 2000h
;直接-偏移量寻址(字节数组)
    mov al,arrayB        ;AL = 10h
    mov al,[arrayB+1]    ;AL = 20h
    mov al,[arrayB+2]    ;AL = 30h
;直接-偏移量寻址(字数组)
    mov ax,arrayW        ;AX = 100h
    mov ax,[arrayW+2]    ;AX = 200h
;直接-偏移量寻址(双字数组)
    mov eax,arrayD        ;EAX = 10000h
    mov eax,[arrayD+4]    ;EAX = 20000h
    mov eax,[arrayD+4]    ;EAX = 20000h

    INVOKE ExitProcess,0
main ENDP
END main
				
			

该程序不会产生屏幕输出,但是可以用调试器(debugger)运行。

在 Visual Studio 调试器中显示 CPU 标志位

在调试期间显示 CPU 状态标志位时,在 Debug 菜单中选择 Windows 子菜单,再选择 Register。在 Register 窗口,右键选择下拉列表中的 Flags。要想查看这些菜单选项,必须调 试程序。下表是 Register 窗口中用到的标志位符号:

标志名称溢岀方向中断符号辅助进位奇偶进位
符号OVUPEIPLZRACPECY


每个标志位有两个值:0(清除)或 1(置位)。示例如下:

OV = 0     UP = 0      EI = 1
PL = 0      ZR = 1      AC = 0
PE = 1      CY = 0   

调试程序期间,当逐步执行代码时,指令只要修改了标志位的值,则标志位就会显示为红色。这样就可以通过单步执行来了解指令是如何影响标志位的,并可以密切关注这些标志位值的变化。

8.汇编语言加法和减法详解

算术运算是汇编语言中一个大得令人惊讶的主题!本节重点在于加法和减法的运算。

先从最简单、最有效的指令开始: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       
				
			

加减法影响的标志位

执行算术运算指令时,常常想要了解结果。它是负数、正数还是零?对目的操作数来说,它是太大,还是太小?这些问题的答案有助于发现计算错误,否则可能会导致程序的错误行为。

检查算术运算结果使用的是 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    SDWORD 40

.code
main PROC
    ;INC和DEC
    mov    ax,1000h
    inc    ax        ;1001h
    dec    ax        ;1000h
    ;表达式:Rval=-Xval+(Yval-Zval)
    mov    eax,Xval
    neg    eax        ;-26
    mov    ebx,Yval
    sub    ebx,Zval   ;-10
    add    eax,ebx
    mov    Rval,eax;36
    ;零标志位示例
    mov    cx,1
    sub    cx,1       ;ZF = 1
    mov    ax,0FFFFh
    inc    ax         ;ZF = 1
    ;符号标志位示例
    mov    cx,0
    sub    cx,1       ;SF = 1
    mov    ax,7FFFh
    add    ax,2       ;SF = 1
    ;进位标志位示例
    mov    al,0FFh
    add    al,1       ;CF = 1,AL = 00
    ;溢出标志位示例
    mov    al,+127
    add    al,1       ;OF = 1
    mov    al,-128
    sub    al,1       ;OF = 1

    INVOKE ExitProcess,0
main ENDP
END main
				
			

9.汇编语言OFFSET运算符:返回数据标号的偏移量

OFFSET 运算符返回数据标号的偏移量。这个偏移量按字节计算,表示的是该数据标号距离数据段起始地址的距离。如下图所示为数据段内名为 myByte 的变量。

OFFSET 示例

在下面的例子中,将用到如下三种类型的变量:

				
					.data
bVal BYTE ?
wVal WORD ?
dVal DWORD ?
dVal2 DWORD ?
				
			

假设 bVal 在偏移量为 0040 4000(十六进制)的位置,则 OFFSET 运算符返回值如下:

				
					mov esi,OFFSET bVal             ; ESI = 00404000h
mov esi,OFFSET wVal             ; ESI = 00404001h
mov esi,OFFSET dVal             ; ESI = 00404003h
mov esi,OFFSET dVal2            ; ESI = 00404007h
				
			

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
				
			

10.汇编语言ALIGN伪指令:对齐一个变量

				
					ALIGN bound
				
			

Bound 可取值有:1、2、4、8、16。当取值为 1 时,则下一个变量对齐于 1 字节边界(默认情况)。当取值为 2 时,则下一个变量对齐于偶数地址。当取值为 4 时,则下一个变量地址为 4 的倍数。当取值为 16 时,则下一个变量地址为 16 的倍数,即一个段落的边界。

为了满足对齐要求,汇编器会在变量前插入一个或多个空字节。为什么要对齐数据?因为,对于存储于偶地址和奇地址的数据来说,CPU 处理偶地址数据的速度要快得多。

下述例子中,bVal 处于任意位置,但其偏移量为 0040 4000。在 wVal 之前插入 ALIGN 2 伪指令,这使得 wVal 对齐于偶地址偏移量:

				
					bVal BYTE ?           ;00404000h
ALIGN 2 
wVal WORD ?           ;00404002h
bVal2 BYTE ?          ;00404004h
ALIGN 4 
dVal DWORD ?          ;00404008h
dVal2 DWORD ?         ;0040400Ch
				
			

请注意,dVal 的偏移量原本是 0040 4005,但是 ALIGN 4 伪指令使它的偏移量成为 0040 4008。

11.汇编语言PTR运算符:重写操作数的大小类型

PTR 运算符可以用来重写一个已经被声明过的操作数的大小类型。只要试图用不同于汇编器设定的大小属性来访问操作数,那么这个运算符就是必需的。

例如,假设想要将一个双字变量 myDouble 的低 16 位传送给 AXO 由于操作数大小不匹配,因此,汇编器不会允许这种操作:

				
					.data
myDouble DWORD 12345678h
.code
mov ax,myDouble
				
			

但是,使用 WORD PTR 运算符就能将低位字(5678h)送入 AX:

				
					mov ax,WORD PTR myDouble
				
			

为什么送入 AX 的不是 1234h ?因为,x86 处理器采用的是小端存储格式,即低位字节存放于变量的起始地址。如下图所示,用三种方式表示 myDouble 的内存布局:第一列是一个双字,第二列是两个字(5678h、1234h),第三列是四个字节(78h、56h、34h、12h)。

不论该变量是如何定义的,都可以用三种方法中的任何一种来访问内存。比如,如果 myDouble 的偏移量为 0000,则以这个偏移量为首地址存放的 16 位值是 5678h。同时也可以检索到 1234h,其字地址为 myDouble+2,指令如下:

				
					mov ax,WORD PTR [myDouble+2]     ; 1234h
				
			

同样,用 BYTE PTR 运算符能够把 myDouble 的单个字节传送到 BL:

				
					mov b1,BYTE PTR myDouble       ; 78h
				
			

注意,PTR 必须与一个标准汇编数据类型一起使用,这些类型包括:BYTE、SEYTE、WORD、SWORD、DWORD、SDWORD、FWORD、QWORD 或 TBYTE。

将较小的值送入较大的目的操作数

程序可能需要将两个较小的值送入一个较大的目的操作数。如下例所示,第一个字复制到 EAX 的低半部分,第二个字复制到高半部分。而 DWORD PTR 运算符能实现这种操作:

				
					.data
wordList WORD 5678h,1234h
.code
mov eax, DWORD PTR wordList      ; EAX = 12345678h
				
			

12.汇编语言TYPE运算符:返回变量的大小

TYPE 运算符返回变量单个元素的大小,这个大小是以字节为单位计算的。比如,TYPE 为字节,返回值是 1;TYPE 为字,返回值是 2;TYPE 为双字,返回值是 4;TYPE 为四字,返回值是 8。示例如下:

				
					.data
var1 BYTE ?
var2 WORD ?
var3 DWORD ?
var4 QWORD ?
				
			

下表是每个 TYPE 表达式的值。

表达式表达式
TYPE var11TYPE var34
TYPE var22TYPE var48

13.汇编语言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 byte13LENGTHOF array34
LENGTHOF array130+2LENGTHOF digitStr9
LENGTHOF array25*3  


如果数组定义占据了多个程序行,那么 LENGTHOF 只针对第一行定义的数据。比如有如下数据,则 LENGTHOF myArray 返回值为 5 :

				
					myArray BYTE 10,20,30,40,50
        BYTE 60,70,80,90,100
				
			

另外,也可以在第一行结尾处用逗号,并在下一行继续进行数组初始化。若有如下数据定义, LENGTHOF myArray 返回值为 10:

				
					myArray BYTE 10,20,30,40,50,
             60,70,80,90,100
				
			

13.汇编语言LABEL伪指令

LABEL 伪指令可以插入一个标号,并定义它的大小属性,但是不为这个标号分配存储空间。LABEL 中可以使用所有的标准大小属性,如 BYTE、WORD、DWORD、QWORD 或 TBYTE。

LABEL 常见的用法是,为数据段中定义的下一个变量提供不同的名称和大小属性。如下例所示,在变量 val32 前定义了一个变量,名称为 val16 属性为 WORD:

				
					.data
val16 LABEL WORD
val32 DWORD 12345678h
.code
mov ax,val16          ; AX = 5678h
mov dx,[val16+2]      ; DX = 1234h
				
			

val16 与 val32 共享同一个内存位置。LABEL 伪指令自身不分配内存。

有时需要用两个较小的整数组成一个较大的整数,如下例所示,两个 16 位变量组成一个 32 位变量并加载到 EAX 中:

				
					.data
LongValue LABEL DWORD
val1 WORD 5678h
val2 WORD 1234h
.code
mov eax,LongValue         ; EAX = 12345678h
				
			

14.汇编语言间接寻址

直接寻址很少用于数组处理,因为,用常数偏移量来寻址多个数组元素时,直接寻址不实用。反之,会用寄存器作为指针(称为间接寻址)并控制该寄存器的值。如果一个操作数使用的是间接寻址,就称之为间接操作数。

间接操作数

保护模式

任何一个 32 位通用寄存器(EAX、EBX、ECX、EDX、ESI、EDI、EBP 和 ESP)加上括号就能构成一个间接操作数。

寄存器中存放的是数据的地址。示例如下,ESI 存放的是 byteVal 的偏移量,MOV 指令使用间接操作数作为源操作数,解析 ESI 中的偏移量,并将一个字节送入 AL:

				
					.data
byteVal BYTE 10h
.code
mov esi,OFFSET byteVal
mov al,[esi]                              ; AL = 10h
				
			

如果目的操作数也是间接操作数,那么新值将存入由寄存器提供地址的内存位置。在下面的例子中,BL 寄存器的内容复制到 ESI 寻址的内存地址中:

				
					mov [esi],bl
				
			
PTR 与间接操作数一起使用

一个操作数的大小可能无法从指令中直接看出来。下面的指令会导致汇编器产生“operand must have size(操作数必须有大小)”的错误信息:

				
					inc [esi]    ;错误:operand must have size
				
			

汇编器不知道 ESI 指针的类型是字节、字、双字,还是其他的类型。而 PTR 运算符则可以确定操作数的大小类型:

				
					inc BYTE PTR [esi]
				
			

数组

间接操作数是步进遍历数组的理想工具。下例中,arrayB 有 3 个字节,随着 ESI 不断加 1,它就能顺序指向每一个字节:

				
					.data
arrayB BYTE 10h,20h,30h
.code
mov esi,OFFSET arrayB
mov alz [esi]                        ;AL = lOh
inc esi
mov al, [esi]                        ;AL = 20h
inc esi
mov al, [esi]                        ;AL = 30h
				
			

如果数组是 16 位整数类型,则 ESI 加 2 就可以顺序寻址每个数组元素:

				
					.data
arrayW WORD 1000h,2000h,3000h
.code
mov esi,OFFSET arrayW
mov ax,[esi]                         ; AX = 1000h
add esi, 2
mov ax,[esi]                         ; AX = 2000h
add esi, 2
mov axz [esi]                        ; AX = 3000h
				
			

假设 arrayW 的偏移量为 10200h,下图展示的是 ESI 初始值相对数组数据的位置。

示例:32 位整数相加下面的代码示例实现的是 3 个双字相加。由于双字是 4 个字节的,因此,ESI 要加 4 才能顺序指向每个数组数值:

				
					.data
arrayD DWORD 10000h,20000h,30000h
.code
mov esi,OFFSET arrayD
mov eax, [esi]                  ;(第一个数)
add esi, 4
add eax, [esi]                   ;(第二个数)
add esi, 4
add eax, [esi]                   ;(第三个数)
				
			

假设 arrayD 的偏移量为 10200h。下图展示的是 ESI 初始值相对数组数据的位置:

变址操作数

变址操作数是指,在寄存器上加上常数产生一个有效地址。每个 32 位通用寄存器都可以用作变址寄存器。MASM 可以用不同的符号来表示变址操作数(括号是表示符号的一部分):

				
					constant [reg]
[constant + reg]
				
			

第一种形式是变量名加上寄存器。变量名由汇编器转换为常数,代表的是该变量的偏移量。下面给岀的是两种符号形式的例子:

arrayB[esi][arrayB + esi]
arrayD[ebx][arrayD + ebx]


变址操作数非常适合于数组处理。在访问第一个数组元素之前,变址寄存器需要初始化为 0:

				
					.data
arrayB BYTE 10h,20h,30h
.code
mov esi, 0
mov al, arrayB[esi]                ; AL = 10h
				
			

最后一条语句将 ESI 和 arrayB 的偏移量相加,表达式 [arrayB+ESI] 产生的地址被解析,并将相应内存字节的内容复制到AL。

增加位移量变址寻址的第二种形式是寄存器加上常数偏移量。变址寄存器保存数组或结构的基址,常数标识各个数组元素的偏移量。下例展示了在一个 16 位字数组中如何使用这种形式:

				
					.data
arrayW WORD 1000h,2000h,3000h
.code
mov esi,OFFSET arrayW
mov ax, [esi]                   ;AX = 1000h
mov ax, [esi+2]                 ;AX = 2000h
mov ax, [esi+4]                 ;AX = 3000h
				
			
使用 16 位寄存器

在实地址模式中,一般用 16 位寄存器作为变址操作数。在这种情况下,能被使用的寄存器只有 SI、DI、BX 和 BP:

				
					mov al,arrayB[si]
mov ax,arrayW[di]
mov eax,arrayD[bx]
				
			

如果有间接操作数,则要避免使用 BP 寄存器,除非是寻址堆栈数据。

变址操作数中的比例因子

在计算偏移量时,变址操作数必须考虑每个数组元素的大小。比如下例中的双字数组,下标(3 )要乘以 4(一个双字的大小)才能生成内容为 400h 的数组元素的偏移量:

				
					.data
arrayD DWORD 100h, 200h, 300h, 400h
.code
mov esi , 3 * TYPE arrayD                            ; arrayD [ 3 ]的偏移量
mov eax,arrayD[esi]                                  ; EAX = 400h
				
			

Intel 设计师希望能让编译器编写者的常用操作更容易,因此,他们提供了一种计算偏移量的方法,即使用比例因子。比例因子是数组元素的大小(字 = 2,双字 =4,四字 =8)。现在对刚才的例子进行修改,将数组下标(3)送入 ESI,然后 ESI 乘以双字的比例因子(4):

				
					.data
arrayD DWORD 1,2,3,4
.code 
mov esi, 3                               ;下标
mov eax,arrayD[esi*4]                    ;EAX = 4
				
			

TYPE 运算符能让变址更加灵活,它可以让 arrayD 在以后重新定义为别的类型:

				
					mov esi, 3                         ;下标 
mov eax,arrayD[esi*TYPE arrayD]    ;EAX = 4
				
			

指针

如果一个变量包含另一个变量的地址,则该变量称为指针。指针是控制数组和数据结构的重要工具,因为,它包含的地址在运行时是可以修改的。比如,可以使用系统调用来分配(保留)一个内存块,再把这个块的地址保存在一个变量中。

指针的大小受处理器当前模式(32位或64位)的影响。下例为 32 位的代码,ptrB 包含了 arrayB 的偏移量:

				
					.data
arrayB byte 10h,20h,30h,40h
ptrB dword arrayB
				
			

还可以用 OFFSET 运算符来定义 ptrB,从而使得这种关系更加明确:

				
					ptrB DWORD OFFSET arrayB
ptrW DWORD OFFSET arrayW
				
			

高级语言刻意隐藏了指针的物理细节,这是因为机器结构不同,指针的实现也有差异。汇编语言中,由于面对的是单一实现,因此是在物理层上检查和使用指针。这样有助于消除围绕着指针的一些神秘感。

使用 TYPEDEF 运算符

TYPEDEF 运算符可以创建用户定义类型,这些类型包含了定义变量时内置类型的所有状态。它是创建指针变量的理想工具。比如,下面声明创建的一个新数据类型 PBYTE 就是一个字节指针:

PBYTE TYPEDEF PTR BYTE

这个声明通常放在靠近程序开始的地方,在数据段之前。然后,变量就可以用 PBYTE 来定义:

				
					.data
arrayB BYTE 10h,20h,30h,40h
ptr1 PBYTE ?                              ;未初始化
ptr2 PBYTE arrayB                     ;指向一个数组
				
			
示例程序:Pointers

下面的程序(pointers.asm)用 TYPEDEF 创建了 3 个指针类型(PBYTE、PWORD、PDWORD)。此外,程序还创建了几个指针,分配了一些数组偏移量,并解析了这些指针:

				
					TITLE Pointers            (Pointers.asm)
.386
.model flat,stdcall
.stack 4096
ExitProcess proto,dwExitCode:dword
;创建用户定义类型
PBYTE TYPEDEF PTR BYTE                ;字节指针
PWORD TYPEDEF PTR WORD                ;字指针
PDWORD TYPEDEF PTR DWORD            ;双字指针
.data
arrayB BYTE 10h,20h,30h
arrayW WORD 1,2,3
arrayD DWORD 4,5,6
;创建几个指针变量
ptr1 PBYTE arrayB
ptr2 PWORD arrayW
ptr3 PDWORD arrayD
.code
main PROC
;使用指针访问数据
    mov esi,ptr1
    mov al,[esi]                    ;10h
    mov esi,ptr2
    mov ax,[esi]                    ;1
    mov esi,ptr3
    mov eax,[esi]                    ;4
    invoke ExitProcess,0
main ENDP
END main
				
			

16.汇编语言JMP和LOOP(转移)指令

默认情况下,CPU 是顺序加载并执行程序。但是,当前指令有可能是有条件的,也就是说,它按照 CPU 状态标志(零标志、符号标志、进位标志等)的值,把控制转向程序中的新位置。

汇编语言程序使用条件指令来实现如 IF 语句的高级语句与循环。每条条件指令都包含了一个可能的转向不同内存地址的转移(跳转)。控制转移,或分支,是一种改变语句执行顺序的方法,它有两种基本类型:

  • 无条件转移:无论什么情况都会转移到新地址。新地址加载到指令指针寄存器,使得程序在新地址进行执行。JMP 指令实现这种转移。
  • 条件转移:满足某种条件,则程序出现分支。各种条件转移指令还可以组合起来,形成条件逻辑结构。CPU 基于 ECX 和标志寄存器的内容来解释真 / 假条件。

JMP 指令

JMP 指令无条件跳转到目标地址,该地址用代码标号来标识,并被汇编器转换为偏移 量。语法如下所示:

				
					JMP destination
				
			

当 CPU 执行一个无条件转移时,目标地址的偏移量被送入指令指针寄存器,从而导致迈从新地址开始继续执行。

JMP 指令提供了一种简单的方法来创建循环,即跳转到循环开始时的标号:

				
					top:
    .
    .
    jmp top     ;不断地循环
				
			

JMP 是无条件的,因此循环会无休止地进行下去,除非找到其他方法退岀循环。

LOOP 指令

LOOP 指令,正式称为按照 ECX 计数器循环,将程序块重复特定次数。ECX 自动成为计数器,每循环一次计数值减 1。语法如下所示:

				
					LOOP destination
				
			

循环目标必须距离当前地址计数器 -128 到 +127 字节范围内。LOOP 指令的执行有两个步骤:

  • 第一步,ECX 减 1。
  • 第二步,将 ECX 与 0 比较。


如果 ECX 不等于 0,则跳转到由目标给岀的标号。否则,如果 ECX 等于 0,则不发生跳转,并将控制传递到循环后面的指令。

实地址模式中,CX 是 LOOP 指令的默认循环计数器。同时,LOOPD 指令使用 ECX 为循环计数器,LOOPW 指令使用 CX 为循环计数器。

下面的例子中,每次循环是将 AX 加 1。当循环结束时,AX=5, ECX=0:

				
					    mov ax,0
    mov ecx,5
L1:
    inc ax
    loop L1
				
			

一个常见的编程错误是,在循环开始之前,无意间将 ECX 初始化为 0。如果执行了这个操作,LOOP 指令将 ECX 减 1 后,其值就为 FFFFFFFFh,那么循环次数就变成了 4 294 967 296!如果计数器是 CX (实地址模式下),那么循环次数就为 65 536。

有时,可能会创建一个太大的循环,以至于超过了 LOOP 指令允许的相对跳转范围。下面给出是 MASM 产生的一条错误信息,其原因就是 LOOP 指令的跳转目标太远了:

				
					error A2075: jump destination too far : by 14 byte(s)
				
			

基本上,在一个循环中不用显式的修改 ECX,否则,LOOP 指令可能无法正常工作。下例中,每次循环 ECX 加 1。这样 ECX 的值永远不能到 0,因此循环也永远不会停止:

				
					top:
    .
    .
    inc ecx
    loop top
				
			

如果需要在循环中修改 ECX,可以在循环开始时,将 ECX 的值保存在变量中,再在 LOOP 指令之前恢复被保存的计数值:

				
					.data
count DWORD ?
.code
    mov ecx, 100        ;设置循环计数值
top:
    mov count, ecx      ;保存计数值
    .
    mov ecx, 20         ;修改 ECX
    .
    mov ecx, count      ;恢复计数值
    loop top
				
			
循环嵌套

当在一个循环中再创建一个循环时,就必须特别考虑外层循环的计数器 ECX,可以将它保存在一个变量中:

				
					.data
count DWORD ?
.code
    mov ecx, 100    ;设置外层循环计数值
L1:
    mov count, ecx  ;保存外层循环计数值
    mov ecx, 20     ;设置内层循环计数值
L2 :
    loop L2         ;重复内层循环
    mov ecx, count  ;恢复外层循环计数值
    loop L1         ;重复外层循环
				
			

提示:作为一般规则,多于两重的循环嵌套难以编写。如果使用的算法需要多重循环,则将一些内层循环用子程序来实现。

在 Visual Studio 调试器中显示数组

在调试期间,如果想要显示数组的内容,步骤如下:选择 Debug 菜单 -> 选择 Windows -> 选择 Memory -> 选择Memory 1。则出现内存窗口,可以用鼠标拖动并停靠在 Visual Studio 工作区的任何一边。还可以右键点击该窗口的标题栏,表明要这个窗口浮动在编辑窗口之上。

在内存窗口上端的 Address 栏里, 键入 & 符号和数组名称,然后点击 Enter。比如,&myArray 就是一个有效的地址表达式。内存窗口将显示从这个数组地址开始的内存块,如下图所示。

如果数组的值是双字,可以在内存窗口中,点击右键并在弹出菜单里选择 4-byte integer。还有不同的格式可供选择,包括 Hexadecimal Display,Signed Display(有符号显示),和 Unsigned Display(无符号显示)。下图显示了所有的选项。

整数数组求和

在刚开始编程时,几乎没有任务比计算数组元素总和更常见了。汇编语言实现数组求和步骤如下:

  • 指定一个寄存器作变址操作数,存放数组地址。
  • 循环计数器初始化为数组的长度。
  • 指定一个寄存器存放累积和数,并赋值为0。
  • 创建标号来标记循环开始的地方。
  • 在循环体内,将和数与一个数组元素相加。
  • 指向下一个数组元素。
  • 用LOOP指令重复循环。


步骤 1 到步骤 3 可以按照任何顺序执行。下面的短程序实现对一个 16 位整数数组求和。

				
					; 数组求和(SumArray. asm)
.386
.model flat,stdcall
.stack 4096
ExitProcess proto,dwExitCode:dword
.data
intarray DWORD 10000h,20000h,30000h,40000h
.code
main PROC
    mov edi, OFFSET intarray   ; 1: EDI=intarray 地址
    mov ecx, LENGTHOF intarray ; 2 :循环计数器初始化
    mov    eax,0               ; 3:    sum=0
L1:                            ; 4:标记循环开始的地方
    add    eax,    [edi]       ; 5:加一个整数
    add edi, TYPE intarray     ; 6:指向下一个元素
    loop    L1                 ; 7:重复,直到 ECX=0
    invoke ExitProcess, 0
main ENDP
END main
				
			

复制字符串

程序常常要将大块数据从一个位置复制到另一个位置。这些数据可能是数组或字符串,但是它们可以包括任何类型的对象。

现在看看在汇编语言中如何实现这种操作,用循环来复制一个字符串,而字符串表示为带有一个空终止值的字节数组。变址寻址很适合于这种操作,因为可以用同一个变址寄存器来引用两个字符串。目标字符串必须有足够的空间来接收 被复制的字符,包括最后的空字节:

				
					;复制字符串    (CopyStr.asm)
.386
.model flat,stdcall
.stack 4096
ExitProcess proto,dwExitCode:dword
.data
source BYTE "This is the source string", 0
target BYTE SIZEOF source DUP(0)
.code
main PROC
    mov    esi, 0                      ;变址寄存器
    mov ecx, SIZEOF source             ;循环计数器
L1:                                   ;从源字符串获取一个字符
    mov    al, source [esi]            ;保存到目标字符串
    mov target [esi] , al              ;指向下一个字符
    inc esi                            ;重复,直到整个字符串完成
    loop L1
    invoke ExitProcess,0
main ENDP
END main
				
			

MOV 指令不能同时有两个内存操作数,所以,每个源字符串字符送入 AL,然后再从 AL 送入目标字符串。

17.汇编语言64位MOV指令

64 位模式下的 MOV 指令与 32 位模式下的有很多共同点,只有几点区别,现在讨论一下。立即操作数(常数)可以是 8 位、16 位、32 位或 64 位。下面为一个 64 位示例:

				
					mov rax, 0ABCDEF0AFFFFFFFFh ; 64 位立即操作数
				
			

当一个 32 位常数送入 64 位寄存器时,目标操作数的高 32 位(位 32—位 63)被清除(等于 0):

				
					mov rax, 0FFFFFFFFh ;rax = 00000000FFFFFFFF
				
			

向 64 位寄存器送入 16 位或 8 位常数,其高位也要清零:

				
					mov rax, 06666h  ;清位 16—位 63
mov rax, 055h      ;清位 8—位 63
				
			

如果将内存操作数送入 64 位寄存器,则结果是确定的。比如,传送一个 32 位内存操作数到 EAX(RAX 寄存器的低半部分),就会清除 RAX 的高 32 位:

				
					.data
myDword DWORD 80000000h
.code
mov rax,0FFFFFFFFFFFFFFFFh
mov eax,myDword                     ; RAX = 0000000080000000
				
			

但是,如果是将 8 位或 16 位内存操作数送入 RAX 的低位,那么,目标寄存器的高位不受影响:

				
					.data
myByte BYTE 55h
myWord WORD 6666h
.code
mov ax,myWord                ;位 16—位 63 不受影响
mov al, myByte                  ;位 8—位 63 不受影响
				
			

MOVSXD 指令(符号扩展传送)允许源操作数为 32 位寄存器或内存操作数。下面的指令使得 RAX 的值为 FFFFFFFFFFFFFFFFh:

				
					mov ebx, 0FFFFFFFFh
movsxd rax,ebx
				
			

OFFSET 运算符产生 64 位地址,必须用 64 位寄存器或变量来保存。下例中使用的是 RSI 寄存器:

				
					.data
myArray WORD 10,20,30,40
.code
mov rsi,OFFSET myArray
				
			

64 位模式中,LOOP 指令用 RCX 作为循环计数器。

有了这些基本概念,就可以编写许多 64 位模式程序了。大多数情况下,如果一直使用 64 位整数变量或 64 位寄存器,那么编程比较容易。ASCII 码字符串是一种特殊情况,因为它们总是包含字节。一般在处理时,采用间接或变址寻址。

18.汇编语言64位加法和减法

如同 32 位模式下一样,ADD、SUB、INC 和 DEC 指令在 64 位模式下,也会影响 CPU 状态标志位。在下面的例子中,RAX 寄存器存放一个 32 位数,执行加 1,每一位都向左产生一个进位,因此,在位 32 生成 1:

				
					mov rax, 0FFFFFFFFh ;低 32 位是全 1
add rax,1           ; RAX = 100000000h
				
			

需要时刻留意操作数的大小,当操作数只使用部分寄存器时,要注意寄存器的其他部分是没有被修改的。如下例所示,AX 中的 16 位总和翻转为全 0,但是不影响 RAX 的高位。这是因为该操作只使用 16 位寄存器(AX 和 BX):

				
					mov rax,0FFFFh        ; RAX = 000000000000FFFF
mov bx, 1
add ax,bx             ; RAX = 0000000000000000
				
			

同样,在下面的例子中,由于 AL 中的进位不会进入 RAX 的其他位,所以执行 ADD 指令后,RAX 等于 0:

				
					mov rax,0FFh         ; RAX = 00000000000000FF
mov bl, 1
add al,bl            ; RAX = 0000000000000000
				
			

减法也使用相同的原则。在下面的代码段中,EAX 内容为 0,对其进行减 1 操作,将会使得 RAX 低 3 2位变为 -1(FFFFFFFFh)。同样,AX 内容为 0,对其进行减 1 操作,使得 RAX 低 16 位等于 -1(FFFFh)。

				
					mov rax,0               ; RAX = 0000000000000000
mov ebx, 1
sub eax,ebx             ; RAX = 00000000FFFFFFFF
mov rax,0               ; RAX = 0000000000000000
mov bx,1
sub ax,bx               ; RAX = 000000000000FFFF
				
			

当指令包含间接操作数时,必须使用 64 位通用寄存器。记住,一定要使用 PTR 运算符来明确目标操作数的大小。下面是一些包含了 64 位目标操作数的例子:

				
					dec BYTE PTR [rdi]              ;8 位目标操作数
inc WORD PTR [rbx]              ;16 位目标操作数
inc QWORD PTR [rsi]             ;64 位目标操作数
				
			

64 位模式下,可以对间接操作数使用比例因子,就像在 32 位模式下一样。如下例所示,如果处理的是 64 位整数数组,比例因子就是 8:

				
					.data
array QWORD 1,2,3,4
.code
mov esi, 3                   ;下标
mov rax,array[rsi*8]         ; RAX = 4
				
			

64 位模式的指针变量包含的是 64 位偏移量。在下面的例子中,ptrB 变量包含了数组 B 的偏移量:

				
					.data
arrayB BYTE 10h, 20h, 30h, 40h
ptrB QWORD arrayB
				
			

或者,还可以用 OFFSET 运算符来定义 ptrB,使得这个关系更加明确:

				
					ptrB QWORD OFFSET arrayB
				
			
仰天大笑出门去,我辈岂是蓬蒿人
最后更新于 2022-05-03