《汇编语言:基于X86处理器》第4章 数据传送、寻址和算术运算(2)
本章介绍了数据传送和算术运算的若干必要指令,用大量的篇幅说明了基本寻址模式,如直接寻址、立即寻址和可以用于处理数组的间接寻址。同时,还展示了怎样创建循环和怎样使用一些基本运算符,如 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 PROCmov esi, OFFSET byteValmov al,[esi] ;AL = 10h mov [esi], blinc 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 PROCmov esi, OFFSET arrayBmov al,[esi] ;AL = 10hinc esimov al,[esi] ;AL = 20hinc esimov al,[esi] ;AL = 30h;数组是16位整数类型,则ESI加2就可以顺序寻址每个数组元素;mov esi, OFFSET arrayWmov ax,[esi] ;AX = 1000hadd esi,2mov ax,[esi] ;AX = 2000hadd esi,2mov ax,[esi] ;AX = 3000h;双字是4个字节的,因此,ESI要加4才能顺序指向每个数组数值mov esi, OFFSET arrayDmov eax,[esi] ;EAX = 10000h 第1个数add esi,4mov eax,[esi] ;EAX = 20000h 第2个数add esi,4mov 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 PROCmov esi,0mov al,arrayB[esi] ;AL = 10hmov esi,OFFSET arrayWmov ax,[esi] ;AX = 1000hmov ax,[esi+2] ;AX = 2000h mov ax,[esi+4] ;AX = 3000hmov esi,3 * TYPE arrayD ;arrayD[3]的偏移量mov eax,arrayD[esi] ;EAX = 400hmov esi,3 ;下标mov eax,arrayD2[esi*4] ;EAX = 4mov esi,3 ;下标mov eax,arrayD2[esi*(TYPE arrayD2)] ;EAX = 4INVOKE 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,ptr1mov al,[esi] ;al = 10hmov esi,ptr2mov ax,[esi] ;ax = 1mov esi,ptr3mov eax,[esi] ;eax = 4INVOKE 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 PROCmov esi,OFFSET myBytesmov al,[esi] ;a.AL=10hmov al,[esi+3] ;b.AL=40hmov esi,OFFSET myWords + 2mov ax,[esi] ;c.AX=003Bhmov edi,8mov edx,[myDoubles+edi] ;d.EDX=3mov edx,myDoubles[edi] ;e.EDX=3mov ebx,myPointermov eax,[ebx+4] ;f.EAX=2INVOKE 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 PROCmov esi,OFFSET myBytesmov ax,[esi] ;a.AX=2010hmov eax,DWORD PTR myWords ;b.EAX=003B008Ahmov esi,myPointermov ax,[esi+2] ;c.AX=0mov ax,[esi+6] ;d.AX=0mov ax,[esi-4] ;e.AX=0044hINVOKE 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,0mov 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 ?
.codemov ecx,100 ;设置循环计数值
top:mov count,ecx ;保存计数值.mov ecx,20 ;修改 ECX.mov ecx,count ;恢复计数值loop top
循环嵌套 当在一个循环中再创建一个循环时,就必须特别考虑外层循环的计数器ECX,可以将它保存在一个变量中:
.data
count DWORD ?
.codemov 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 PROCmov 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=0INVOKE 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 PROCmov esi,OFFSET sourcemov esi,0 ;变址寄存器mov ecx,SIZEOF source ;循环计数器
L1: ;从源字符串获取一个字符mov al,source[esi] ;保存到目标字符串mov target[esi],al ;指向下一个字符inc esi ;重复,直到整个字符串完成loop L1invoke 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,0mov ecx,10 ;外层循环计数器
L1: mov eax,3mov ecx,5 ;内层循环计数器
L2: add eax,5loop 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 PROCmov eax,0mov ecx,10 ;外层循环计数器L1: push ecx ;保存外层循环计数器mov eax,3mov ecx,5 ;内层循环计数器
L2: add eax,5loop 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=00000000FFFFFFFFhmov rax,06666h ;清位16位63mov rax,055h ;清位8一位 63mov rax,0FFFFFFFFFFFFFFFFhmov eax,myDword ;rax=0000000080000000h;如果是将 8 位或 16 位内存操作数送人 RAX 的低位,那么,目标寄存器的高位不受影响:mov ax,myWord ;位16-位63不受影响mov al,myByte ;位8-位63不受影响;OFFSET 运算符产生 64 位地址,必须用 64 位寄存器或变量来保存。下例中使用的是RSI 寄存器:mov rsi,OFFSET myArrayxor ecx, ecx ; 退出码 = 0call 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,2000000000000000hQWORD 3000000000000000h,4000000000000000h.code
main procmov 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=0mov 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 procmov rax,0FFFFFFFFh ;低32位是全1,RAX = 00000000FFFFFFFFhadd rax,1 ;RAX =100000000h;使用16位寄存器相加mov rax,0FFFFh ;RAX = 000000000000FFFFhmov bx,1 add ax,bx ;RAX = 0000000000000000h 进位不影响高位的内容;同样,8位寄存器相加也不影响高位mov rax,0FFh ;RAX = 00000000000000FFhmov bl,1add al,bl ;RAX = 0000000000000000h;减法也是同样的原理mov rax,0 ;RAX = 0000000000000000hmov ebx,1 sub eax,ebx ;RAX = 00000000FFFFFFFFhmov rax,0 ;RAX = 0000000000000000hmov bx,1sub ax,bx ;RAX = 000000000000FFFFh;64位模式间接操作mov rsi,50000000FFFFFFFFhmov esi,3 ;下标 (会把高32位清零)mov rax,array[rsi*8] ;RAX = 4;64位偏移量mov rsi,ptrB mov ax,[rsi] ;RAX = 0000000000002010hmov eax,[rsi] ;RAX = 0000000040302010hmov ecx,0call 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 procmov rcx,1234567800000000hsub ecx,1mov ecx,0call 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 procmov rcx,1234567800000000hadd rcx,0ABABABABhmov ecx,0call 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 procmov rdi, OFFSET bArraydec BYTE PTR [rdi+1]inc rdimov al,[rdi]mov ecx,0call ExitProcess
main endp
end
运行调试:
答:AL寄存器的值为1Fh.
6.执行下列指令后,RCX 的值是多少?
; 4.6.4_6.asm - 4.6.4 本节回顾. x64编程
;6.执行下列指令后,RCX 的值是多少?ExitProcess proto.code
main procmov rcx,0DFFFhmov bx,3add cx,bxmov ecx,0call 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(零标志) |