《汇编语言:基于X86处理器》第4章 数据传送、寻址和算术运算(2)

发布于:2025-06-25 ⋅ 阅读:(22) ⋅ 点赞:(0)

本章介绍了数据传送和算术运算的若干必要指令,用大量的篇幅说明了基本寻址模式,如直接寻址、立即寻址和可以用于处理数组的间接寻址。同时,还展示了怎样创建循环和怎样使用一些基本运算符,如 OFFSET,PTR 和LENGTHOF。阅读本章后,将会了解除条件语句之外的汇编语言的基本工作知识。

4.4 间接寻址

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

4.4.1间接操作数

保护模式 任何一个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]

完整的测试代码如下:

;4.4.1_p91.asm   4.4.1间接操作数 

.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD

.data
byteVal BYTE 10h

.code
main PROC
	mov esi, OFFSET byteVal
	mov al,[esi]				;AL = 10h 

	mov [esi], bl
	inc BYTE PTR [esi]

	INVOKE ExitProcess,0
main ENDP
END main 

运行调试:

4.4.2 数组

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

.data
arrayB BYTE 10h,20h,30h
.code
mov esi, OFFSET arrayB
mov al,[esi]				;AL = 10h
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 ax,[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]				;EAX = 10000h   第1个数
add esi,4
mov eax,[esi]				;EAX = 20000h   第2个数
add esi,4
mov eax,[esi]				;EAX = 30000h   第3个数

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

完整的测试代码如下:

;4.4.2._p92.asm    

.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD

.data
arrayB BYTE 10h,20h,30h
arrayW WORD 1000h,2000h,3000h
arrayD DWORD 10000h,20000h,30000h

.code
main PROC
	mov esi, OFFSET arrayB
	mov al,[esi]				;AL = 10h
	inc esi
	mov al,[esi]				;AL = 20h
	inc esi
	mov al,[esi]				;AL = 30h
	;数组是16位整数类型,则ESI加2就可以顺序寻址每个数组元素;
	mov esi, OFFSET arrayW
	mov ax,[esi]				;AX = 1000h
	add esi,2
	mov ax,[esi]				;AX = 2000h
	add esi,2
	mov ax,[esi]				;AX = 3000h
	;双字是4个字节的,因此,ESI要加4才能顺序指向每个数组数值
	mov esi, OFFSET arrayD
	mov eax,[esi]				;EAX = 10000h   第1个数
	add esi,4
	mov eax,[esi]				;EAX = 20000h   第2个数
	add esi,4
	mov eax,[esi]				;EAX = 30000h   第3个数

	INVOKE ExitProcess,0
main ENDP
END main

4.4.3 变址操作数

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

constant[reg]

[constant + reg]

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

变址操作数非常适合于数组处理。在访问第一个数组元素之前,变址寄存器需要初始化为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,arrayD2[esi*TYPE arrayD]	;EAX = 4

完整的测试代码如下:

;4.4.3_p93.asm          变址操作数 
;变址操作数是指,在寄存器上加上常数产生一个有效地址。

.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD

.data
arrayB BYTE 10h,20h,30h
arrayW WORD 1000h,2000h,3000h
arrayD DWORD 100h,200h,300h,400h
arrayD2 DWORD 1,2,3,4

.code
main PROC
	mov esi,0
	mov al,arrayB[esi]						;AL = 10h

	mov esi,OFFSET arrayW
	mov ax,[esi]									;AX = 1000h
	mov ax,[esi+2]								;AX = 2000h	
	mov ax,[esi+4]								;AX = 3000h

	mov esi,3 * TYPE arrayD				;arrayD[3]的偏移量
	mov eax,arrayD[esi]						;EAX = 400h

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

	mov esi,3											;下标
	mov eax,arrayD2[esi*(TYPE arrayD2)]		;EAX = 4

	INVOKE ExitProcess,0
main ENDP
END main

4.4.4 指针

如果一个变量包含另一个变量的地址,则该变量称为指针。指针是控制数组和数据结构的重要工具,因为,它包含的地址在运行时是可以修改的。比如,可以使用系统调用来分配(保留)一个内存块,再把这个块的地址保存在一个变量中。指针的大小受处理器当前模式(32位或64位)的影响。下例为32 位的代码,ptrB 包含了arrayB 的偏移量:

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

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

ptrB dword OFFSET arrayB

本书中32位模式程序使用的是近指针,因此,它们保存在双字变量中。这里有两个例子:ptrB包含arrayB的偏移量,ptrW包含arrayW的偏移量:

arrayB BYTE 10h,20h,30h,40h
arrayW WORD 1000h,2000h,3000h
ptrB DWORD arrayB
ptrW DWORD arrayW

同样,也还可以用OFFSET运算符使这种关系更加明确:

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]			;al = 10h
	mov esi,ptr2
	mov ax,[esi]			;ax = 1
	mov esi,ptr3
	mov eax,[esi]			;eax = 4
	INVOKE ExitProcess,0
main ENDP
END main

4.4.5 本节回顾

1.(真/假):任何一个32位通用寄存器都可以用作间接操作数。

答:真

2.(真/假):EBX 寄存器通常是保留的,用于寻址堆栈。

答:假

3.(真/假):指令inc [esi]是非法的。

答:真,需要PTR运行符

4.(真/假):array[esi]是变址操作数。

答:真

问题5~问题6使用如下数据定义:

myBytes BYTE 10h, 20h, 30h, 40h
myWords WORD 8Ah, 3Bh, 72h, 44h, 66h
myDoubles DWORD 1, 2, 3, 4, 5
myPointer DWORD myDoubles

5.有如下指令序列,填写右侧要求的寄存器的值。

;4.4.5_5.asm   4.4.5本节回顾  5.有如下指令序列,填写右侧要求的寄存器的值。

.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD

.data
myBytes BYTE 10h,20h,30h,40h
myWords WORD 8Ah,3Bh,72h,44h,66h
myDoubles DWORD 1,2,3,4,5
myPointer DWORD myDoubles

.code
main PROC
	mov esi,OFFSET myBytes
	mov al,[esi]									;a.AL=10h
	mov al,[esi+3]								;b.AL=40h
	mov esi,OFFSET myWords + 2
	mov ax,[esi]									;c.AX=003Bh
	mov edi,8
	mov edx,[myDoubles+edi]				;d.EDX=3
	mov edx,myDoubles[edi]				;e.EDX=3
	mov ebx,myPointer
	mov eax,[ebx+4]								;f.EAX=2

	INVOKE ExitProcess,0
main ENDP
END main

6.有如下指令序列,填写右侧要求的寄存器的值。

;4.4.5_6.asm     4.4.5本节回顾     6.有如下指令序列,填写右侧要求的寄存器的值。

.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD

.data
myBytes BYTE 10h,20h,30h,40h
myWords WORD 8Ah,3Bh,72h,44h,66h
myDoubles DWORD 1,2,3,4,5
myPointer DWORD myDoubles

.code
main PROC
	mov esi,OFFSET myBytes
	mov ax,[esi]								;a.AX=2010h
	mov eax,DWORD PTR myWords		;b.EAX=003B008Ah
	mov esi,myPointer
	mov ax,[esi+2]							;c.AX=0
	mov ax,[esi+6]							;d.AX=0
	mov ax,[esi-4]							;e.AX=0044h

	INVOKE ExitProcess,0
main ENDP
END main

4.5 JMP 和 LOOP 指令

默认情况下,CPU 是顺序加载并执行程序。但是,当前指令有可能是有条件的,也就是说,它按照 CPU 状态标志(零标志、符号标志、进位标志等)的值,把控制转向程序中的新位置。汇编语言程序使用条件指令来实现如 IF 语句的高级语句与循环。每条条件指令都包含了一个可能的转向不同内存地址的转移(跳转)。控制转移,或分支,是一种改变语句执行顺序的方法,它有两种基本类型:

无条件转移: 无论什么情况都会转移到新地址。新地址加载到指令指针寄存器,使得程序在新地址进行执行。JMP 指令实现这种转移。

条件转移: 满足某种条件,则程序出现分支。各种条件转移指令还可以组合起来,形成条件逻辑结构。CPU基于ECX和标志寄存器的内容来解释真/假条件。

4.5.1 JMP指令

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

JMP destination

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

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

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

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

4.5.2 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,那么循环次数就变成了4294967296!如果计数器是CX(实地址模式下),那么循环次数就为65536。

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

error A2075: jump destination too far: by 14 bytes(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						;重复外层循环

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

4.5.3 在Visual Studio调试器中显示数组

在调试期间,如果想要显示数组的内容,步骤如下:选择Debug菜单一选择Windows一选择Memory→选择Memory1。则出现内存窗口,可以用鼠标拖动并停靠在Visual Studio工作区的任何一边。还可以右键点击该窗口的标题栏,表明要这个窗口浮动在编辑窗口之上。在内存窗口上端的Address栏里,键入&符号和数组名称,然后点击Enter。比如,&myArray就是一个有效的地址表达式。内存窗口将显示从这个数组地址开始的内存块,如图4-8所示。

图 4-8使用调试器的内存窗口显示数组

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

图 4-9调试器内存窗口的弹出菜单

4.5.4 整数数组求和

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

1)指定一个寄存器作变址操作数,存放数组地址。

2)循环计数器初始化为数组的长度。

3)指定一个寄存器存放累积和数,并赋值为0。

4)创建标号来标记循环开始的地方。

5)在循环体内,将和数与一个数组元素相加。

6)指向下一个数组元素。

7)用 LOOP 指令重复循环。

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

;SumArray.asm    4.5.4 整数数组求和

.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

运行结果:

4.5.5 复制字符串

程序常常要将大块数据从一个位置复制到另一个位置。这些数据可能是数组或字符串,但是它们可以包括任何类型的对象。现在看看在汇编语言中如何实现这种操作,用循环来复制一个字符串,而字符串表示为带有一个空终止值的字节数组。变址寻址很适合于这种操作,因为可以用同一个变址寄存器来引用两个字符串。目标字符串必须有足够的空间来接收被复制的字符,包括最后的空字节:

;CopyStr.asm    4.5.5 复制字符串

.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,OFFSET source
	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 送入目标字符串。

4.5.6 本节回顾

1.(真/假):JMP指令只能跳转到当前过程中的标号。

答:真

2.(真/假):JMP 是条件跳转指令。

答:假

3.循环开始时,如果 ECX 初始化为 0,那么LOOP 指令要循环多少次?(假设在循环中,没有其他指令修改ECX。)

答:0FFFFFFFFh次=4294967296

4.(真/假):LOOP指令首先检查ECX是否等于0,然后ECX减1,再跳转到目标标号。

答:假,先减1再检查ECX是否等于0。

5.(真/假):LOOP指令执行过程如下:ECX减1;如果ECX 不等于 0,LOOP 跳转到目标标号。

答:真

6.实地址模式中,LOOP 指令使用哪一个寄存器作计数器?

答:CX寄存器

7.实地址模式中,LOOPD指令使用哪一个寄存器作计数器?

答:ECX寄存器

8.(真/假):LOOP指令的跳转目标必须在距离当前地址 256 个字节的范围内。

答:假(与当前地址距离范围为-128~127字节)

9.(挑战):程序如下所示,EAX最后的值是多少?

	  mov eax,0
	  mov ecx,10			;外层循环计数器
L1:	mov eax,3
	  mov ecx,5				;内层循环计数器
L2:	add eax,5
	  loop L2				;重复内层循环
	  loop L1				;重复外层循环

答:程序不会结束,当L2结束时ECX为0,当loop L1时,ECX-1变成一个非常大的值0FFFFFFFFh。

10.修改上题代码,使得内层循环开始时,外层循环计数器不会被擦除。

答:将指令push ecx插入到标号L1处,同时将指令pop ecx插入到Loop L2与Loop L1之间。计算的结果EAX=0000001ch。

;4.5.6_10.asm     4.5.6本节回顾     10.修改上题代码,使得内层循环开始时,外层循环计数器不会被擦除。

.386
.model flat,stdcall
.stack 4096
ExitProcess proto,dwExitCode:DWORD

.data

.code
main PROC
	mov eax,0
	mov ecx,10				;外层循环计数器

L1:	push ecx				;保存外层循环计数器
	mov eax,3
	mov ecx,5				;内层循环计数器
L2:	add eax,5
	loop L2					;重复内层循环
	pop ecx					;弹出外层循环计数器
	loop L1					;重复外层循环

	invoke ExitProcess,0
main ENDP
END main

运行调试:

4.6 64位编程

4.6.1 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=0000000080000000h

但是,如果是将 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 码字符串是一种特殊情况,因为它们总是包含字节。一般在处理时,采用间接或变址寻址。

完整代码测试笔记:

; 4.6.1.asm - 4.6.1 MOV指令. x64编程

ExitProcess proto

.data
myDword DWORD 80000000h
myByte BYTE 55h
myWord WORD 6666h
myArray WORD 10,20,30,40

.code
main proc
	;sub rsp, 28h         ; 预留影子空间(32字节)+ 8字节对齐(x64调用约定)
	
	mov rax,0ABCDEF0AFFFFFFFFh		;64立即操作数
	mov rax,0FFFFFFFFh				;rax=00000000FFFFFFFFh
	mov rax,06666h					;清位16位63
	mov rax,055h					;清位8一位 63

	mov rax,0FFFFFFFFFFFFFFFFh
	mov eax,myDword					;rax=0000000080000000h
	;如果是将 8 位或 16 位内存操作数送人 RAX 的低位,那么,目标寄存器的高位不受影响:
	mov ax,myWord					;位16-位63不受影响
	mov al,myByte					;位8-位63不受影响
	;OFFSET 运算符产生 64 位地址,必须用 64 位寄存器或变量来保存。下例中使用的是RSI 寄存器:
	mov rsi,OFFSET myArray

	xor ecx, ecx         ; 退出码 = 0
    call ExitProcess
    ;add rsp, 28h         ; 恢复栈(不会执行到这里)
main endp
end

运行调试:

4.6.2 64位的SumArray程序

; SumArray_64.asm  4.6.2 64位的SumArray程序
; 数据求和

ExitProcess proto

.data
intArray QWORD 1000000000000000h,2000000000000000h
		 QWORD 3000000000000000h,4000000000000000h

.code
main proc
	mov rdi, OFFSET intArray			;RDI=intArray 地址
	mov rcx, LENGTHOF intArray			;循环计数器初始化
	mov rax,0							;sum=0
L1:										;标记循环开始的地方
	add rax,[rdi]						;加一个整数
	add rdi,TYPE intArray				;指向下一个元素
	loop L1								;重复,直到RCX=0
	mov ecx,0							;ExitProcess返回函数

	call ExitProcess
main endp
end

运行调试:

4.6.3 加法和减法

如同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 = 000000000000FFFFh
mov bx,1						
add ax,bx						;RAX = 0000000000000000h  进位不影响高位的内容

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

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

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

mov rax,0					;RAX = 0000000000000000h
mov ebx,1						
sub eax,ebx					;RAX = 00000000FFFFFFFFh
mov rax,0					;RAX = 0000000000000000h
mov bx,1
sub ax,bx					;RAX = 000000000000FFFFh

当指令包含间接操作数时,必须使用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						;下标 (会把高32位清零)
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

完整代码测试笔记:

; 4.6.3.asm - 4.6.3 加法和减法     x64编程

ExitProcess proto

.data
array QWORD 1,2,3,4
arrayB BYTE 10h,20h,30h,40h
ptrB QWORD arrayB

.code
main proc
	mov rax,0FFFFFFFFh				;低32位是全1,RAX = 00000000FFFFFFFFh
	add rax,1						;RAX =100000000h
	;使用16位寄存器相加
	mov rax,0FFFFh					;RAX = 000000000000FFFFh
	mov bx,1						
	add ax,bx						;RAX = 0000000000000000h  进位不影响高位的内容
	;同样,8位寄存器相加也不影响高位
	mov rax,0FFh					;RAX = 00000000000000FFh
	mov bl,1
	add al,bl						;RAX = 0000000000000000h
	;减法也是同样的原理
	mov rax,0						;RAX = 0000000000000000h
	mov ebx,1						
	sub eax,ebx						;RAX = 00000000FFFFFFFFh
	mov rax,0						;RAX = 0000000000000000h
	mov bx,1
	sub ax,bx						;RAX = 000000000000FFFFh
	;64位模式间接操作
	mov rsi,50000000FFFFFFFFh
	mov esi,3						;下标 (会把高32位清零)
	mov rax,array[rsi*8]			;RAX = 4
	;64位偏移量
	mov rsi,ptrB			
	mov ax,[rsi]					;RAX = 0000000000002010h
	mov eax,[rsi]					;RAX = 0000000040302010h

	mov ecx,0
	call ExitProcess
main endp
end

4.6.4 本节回顾

1.(真/假):将常数值 0FFh 送入 RAX 寄存器,将清除其位8一位63。

答:真

2.(真/假):一个32 位常数可以被送入64 位寄存器中,但是64 位常数不可以。

答:假

3.执行下列指令后,RCX的值是多少?

mov rcx, 1234567800000000h
sub ecx, 1

答:rcx的值为00000000FFFFFFFFh

  • 这里的关键点是使用了32位的ecx寄存器而不是64位的rcx
  • 在x86-64架构中,对32位寄存器的操作会零扩展到64位寄存器的高32位
  • ecx当前值是 00000000h (因为 1234567800000000h 的低32位是 00000000h)
  • 执行 00000000h - 1 = FFFFFFFFh
  • 然后这个结果会被零扩展到64位,即高32位补0

完整代码测试笔记:

; 4.6.4_3.asm - 4.6.4 本节回顾. x64编程
;3.执行下列指令后,RCX的值是多少?

ExitProcess proto

.code
main proc
	mov rcx,1234567800000000h
	sub ecx,1

	mov ecx,0
	call ExitProcess
main endp
end

运行调试:

4.执行下列指令后,RCX的值是多少?

mov rcx, 1234567800000000h
add rcx, 0ABABABABh

答:rcx的值为12345678ABABABABh

完整代码测试笔记:

;4.6.4_4.asm - 4.6.4 本节回顾. x64编程
;4.执行下列指令后,RCX的值是多少?

ExitProcess proto

.code
main proc
	mov rcx,1234567800000000h
	add rcx,0ABABABABh

	mov ecx,0
	call ExitProcess
main endp
end

运行调试:

5.执行下列指令后,AL寄存器的值是多少?

; 4.6.4_5.asm - 4.6.4 本节回顾. x64编程
;5.执行下列指令后,AL寄存器的值是多少?

ExitProcess proto

.data
bArray BYTE 10h,20h,30h,40h,50h

.code 
main proc
	mov rdi, OFFSET bArray
	dec BYTE PTR [rdi+1]
	inc rdi
	mov al,[rdi]

	mov ecx,0
	call ExitProcess
main endp
end

运行调试:

答:AL寄存器的值为1Fh.

6.执行下列指令后,RCX 的值是多少?

; 4.6.4_6.asm - 4.6.4 本节回顾. x64编程
;6.执行下列指令后,RCX 的值是多少?

ExitProcess proto

.code
main proc
	mov rcx,0DFFFh
	mov bx,3
	add cx,bx

	mov ecx,0
	call ExitProcess
main endp
end

运行调试:

答:RCX寄存器的值为0E002h.

4.7 本章小结

MOV,数据传送指令,将源操作数复制到目的操作数。MOVZX 指令将一个较小的操作数零扩展为较大的操作数。MOVSX指令将一个较小的操作数符号扩展为较大的操作数。

XCHG指令交换两个操作数的内容,指令中至少有一个操作数是寄存器。

操作数类型 本章中出现了下列操作数类型:

●直接操作数是变量的名字,表示该变量的地址。

●直接-偏移量操作数是在变量名上加位移,生成新的偏移量。可以用它来访问内存数据。

●间接操作数是寄存器,其中存放了数据地址。通过在寄存器名外面加方括号(如[esi]),程序就能解析该地址,并检索内存数据。

●变址操作数将间接操作数与常数组合在一起。常数与寄存器值相加,并解析结果偏移量。如,[array+esi]和 [esi] 都是变址操作数。

下面列出了重要的算术运算指令:

●INC 指令实现操作数加1。

●DEC 指令实现操作数减1。

●ADD 指令实现源操作数与目的操作数相加。

●SUB 指令实现目的操作数减去源操作数。

●NEG 指令实现操作数符号翻转。

当把简单算术运算表达式转换为汇编语言时,利用标准运算符优先级原则来选择首先实现哪个表达式。

状态标志 下面列出了受算术运算操作影响的 CPU 状态标志:。

●算术运算操作结果为负时,符号标志位置 1。

●与目标操作数相比,无符号算术运算操作结果太大时,进位标志位置1。

●执行算术或布尔指令后,奇偶标志位能立即反映出目标操作数最低有效字节中 1 的个数是奇数还是偶数。

●目标操作数的位3有进位或借位时,辅助进位标志位置1。

●算术操作结果为0时,零标志位置1。

●有符号算术运算操作结果超过目标操作数范围时,溢出标志位置1。

运算符 下面列出了汇编语言中常用的运算符:

●OFFSET运算符返回的是变量与其所在段首地址的距离(按字节计)。

●PTR运算符重新定义变量的大小。

●TYPE运算符返回的是单个变量或数组中单个元素的大小(按字节计)。

●LENGTHOF 运算符返回的是,数组元素的个数。

●SIZEOF 运算符返回的是,数组初始化的字节数。

●TYPEDEF运算符创建用户定义类型。

循环 JMP(跳转)指令无条件分支到另一个位置。LOOP(按ECX计数器内容进行循环)指令用于计数型循环。32 位模式下,LOOP 用ECX 作计数器;64位模式下,用 RCX 作计数器。两种模式下,LOOPD用ECX作计数器;LOOPW用CX作计数器。

MOV指令的操作在32 位模式和64 位模式下几乎相同。但是,向 64位寄存器送常数和内存操作数则有点棘手。只要有可能,在 64 位模式下尽量使用 64 位操作数,间接操作数和变址操作数也总是使用64 位寄存器。

4.8关键术语

4.8.1 术语

Auxiliary Carry flag 辅助进位标志

memory operand(内存操作数)

Carry flag 进位标志

Overflowflag(溢出标志)

conditional transfer 有条件转移

Parity flag(奇偶标志)

data transfer instruction 数据传送指令

pointer(指针)

direct memory operand 直接内存操作数

register operand(寄存器操作数)

direct-offset operand 直接-偏移量操作数

scale factor(比例因子)

effective address(有效地址)

sign extension(符号扩展)

immediate operand(立即操作数)

unconditionaltransfer(无条件转移)

indexed operand 变址操作数

zero extension(零扩展)

indirect operand 间接操作数

Zero flag(零标志)

4.8.2 指令、运算符和伪指令


网站公告

今日签到

点亮在社区的每一天
去签到