1.11 基本的机器指令
根据用户定义机器指令的方式,x86-64 CPU系列提供了多达几百条到数千条的机器指令。但是大多数汇编语言程序使用大约30条到50条机器指令[6],而且只需要几条指令就可以编写若干有意义的程序。本节介绍少量机器指令,以便用户可以立即开始编写简单的MASM汇编语言程序。
1.11.1 mov指令
毫无疑问,mov指令是最常用的汇编语言语句。在典型的程序中,25%到40%的指令都是mov指令。顾名思义,这条指令将数据从一个位置移动到另一个位置[7]。mov指令的通用MASM语法形式如下所示:
mov destination_operand,source_operand
source_operand(源操作数)可以是(通用)寄存器、内存变量或常量。destination_operand(目标操作数)可以是寄存器或者内存变量。x86-64指令集不允许两个操作数都是内存变量。在高级程序设计语言(例如Pascal或C/C++)中,mov指令大致相当于以下的赋值语句:
destination_operand=source_operand;
mov指令的两个操作数必须大小相同。也就是说,用户可以在两个字节(8位)对象、字(16位)对象、双字(32位)对象或者四字(64位)对象之间移动数据,但是不能混合操作数的大小。表1-5列出了mov指令的所有合法操作数组合。
建议用户仔细研究一下表1-5,因为大多数通用x86-64指令都使用此语法。
表1-5 x86-64 mov指令的合法操作数组合
(续)
①regn表示n位寄存器,memn表示n位内存位置。
②常量必须足够小,以便可以放入指定的目标操作数。
在表1-5中,需要注意一个重要事项:x86-64仅允许将32位的常量值移动到64位的内存单元(对32位的常量值进行符号扩展,达到64位。有关符号扩展的更多信息,请参见2.8节中的相关内容)。允许64位常量操作数的唯一一条x86-64指令是将64位常量移动到64位寄存器的指令。x86-64指令集中的这种不一致性会令人感到恼火,但是仍然推荐使用x86-64!
1.11.2 指令操作数的类型检查
MASM对指令操作数强制执行某些类型检查,特别是要求指令操作数的大小必须一致。例如,对于以下的代码,MASM将生成错误信息:
i8 byte ?
.
.
.
mov ax,i8
问题在于,用户试图将8位变量(i8)加载到16位寄存器(AX)中。由于两者的大小不兼容,因此MASM假定这是程序中的逻辑错误,并报告错误信息[8]。
在大多数情况下,MASM将忽略有符号变量和无符号变量之间的差异。对于以下两条mov指令,MASM不会报错:
i8 sbyte ?
u8 byte ?
.
.
.
mov al,i8
mov bl,u8
MASM关心的只是我们是否将字节变量移动到了字节大小的寄存器中。区分这些寄存器中有符号和无符号值的任务则交给应用程序。MASM甚至允许以下的操作:
r4v real4 ?
r8v real8 ?
.
.
.
mov eax,r4v
mov rbx,r8v
同样,MASM真正关心的只是内存操作数的大小,至于将浮点变量加载到通用寄存器(通常保存整数值)中的操作,MASM将不加干涉。
在表1-4中,包含proc、label和constant类型。如果用户试图在mov指令中使用proc(过程)或label(标签)保留字,则MASM将报告错误信息。过程或标签类型与机器指令的地址相关联,而不是与变量相关联,况且将过程加载到寄存器中是没有任何意义的。
但是,用户可以指定一个constant符号名称作为指令的源操作数,例如:
someConst=5
.
.
.
mov eax,someConst
由于常量没有大小,因此MASM对常量操作数进行的唯一类型检查是验证常量是否可以放入目标操作数中。例如,MASM将拒绝执行以下的代码:
wordConst=1000
.
.
.
mov al,wordConst
1.11.3 add和sub指令
x86-64中的add和sub指令分别对两个操作数进行加法和减法运算。这两条指令的语法与mov指令几乎相同:
add destination_operand,source_operand
sub destination_operand,source_operand
但是,常量操作数的最大值限制为32位。如果目标操作数为64位,CPU仅允许32位的立即源操作数(则它将该操作数进行符号扩展至64位。有关符号扩展的详细信息,请参见2.8节中的相关内容)。
add指令执行以下的操作:
destination_operand=destination_operand+source_operand
sub指令执行以下的操作:
destination_operand=destination_operand-source_operand
有了这三条指令,再加上一些MASM的控制结构,用户就可以编写复杂的汇编程序了。
1.11.4 lea指令
有时用户需要将变量的地址加载到寄存器中,而不是加载该变量的值。为此,可以使用lea(load effective address,加载有效地址)指令。lea指令采用以下的语法形式:
lea reg64,memory_var
这里,reg64是任何通用的64位寄存器,memory_var是变量名。请注意,该指令与memory_var的类型无关,不要求其类型为qword变量(与mov、add和sub指令一样)。每个变量都有一个与其相关联的内存地址,并且该地址始终为64位。以下示例将strVar字符串中第一个字符的地址加载到RCX寄存器中:
strVar byte"Some String",0
.
.
.
lea rcx,strVar
lea指令大致相当于C/C++的一元运算符“&”(address-of,取地址)。上述汇编程序示例在概念上等同于以下C/C++代码:
char strVar[]="Some String";
char *RCX;
.
.
.
RCX=&strVar[o];
1.11.5 call和ret指令以及MASM过程
为了进行函数调用(以及编写自己的简单函数),用户需要使用call和ret指令。
ret指令在汇编语言程序中的作用与return语句在C/C++中的作用相同:该指令从汇编语言过程(汇编语言中称函数为过程)中返回控制权。本书暂且使用ret指令的变体,该变体没有操作数:
ret
ret指令允许使用单个操作数,但与C/C++语言不同,操作数不指定函数返回值。
用户可以使用call指令调用MASM过程。这个指令可以采取两种形式,最常见的语法形式如下:
call proc_name其中,proc_name是用户想要调用的过程的名称。
正如我们已经讨论的若干代码示例所示,MASM过程包含以下代码行:
proc_name proc然后是过程主体(通常以ret指令结束)。在过程结束时(通常紧随ret指令之后),使用以下语句来结束过程:
proc_name endp
endp伪指令上的标签必须与用户为proc语句提供的标签相同。
在程序清单1-4中的独立汇编语言程序中,主程序调用myProc过程,该过程将立即返回到主程序,然后主程序立即返回到Windows。
程序清单1-4 汇编语言程序中的用户定义过程示例
用户可以使用以下命令编译以上的程序,并尝试运行该程序:
C:\>ml64 listing1-4.asm/link/subsystem:console/entry:main
Microsoft(R)Macro Assembler(x64)Version 14.15.26730.0
Copyright(C)Microsoft Corporation.All rights reserved.
Assembling:listing1-4.asm
Microsoft(R)Incremental Linker Version 14.15.26730.0
Copyright(C)Microsoft Corporation.All rights reserved.
/OUT:listing1-4.exe
listing1-4.obj
/subsystem:console
/entry:main
C:\>listing1-4