0.前言
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 手册并进行了改编。本教程将用这些符号来描述每条指令的语法。
操作数 | 说明 |
---|---|
reg8 | 8 位通用寄存器:AH、AL、BH、BL、CH、CL、DH、DL |
reg16 | 16 位通用寄存器:AX、BX、CX、DX、SI、DI、SP、BP |
reg32 | 32 位通用寄存器:EAX、EEX、ECX、EDX、ESI、EDI、ESP、EBP |
reg | 通用寄存器 |
sreg | 16 位段寄存器:CS、DS、SS、ES、FS、GS |
imm | 8 位、16 位或 32 位立即数 |
imm8 | 8 位立即数,字节型数值 |
imm16 | 16 位立即数,字类型数值 |
imm32 | 32 位立即数,双字型数值 |
reg/mem8 | 8 位操作数,可以是 8 位通用寄存器或内存字节 |
reg/mem16 | 16 位立即数,可以是 16 位通用寄存器或内存字 |
reg/mem32 | 32 位立即数,可以是 32 位通用寄存器或内存双字 |
mem | 8位、16 位或 32 位内存操作数 |
变量名引用的是数据段内的偏移量。可以编写指令,通过内存操作数的地址来解析(查找)这些操作数。另一种表示法。一些程序员更喜欢使用下面这种直接操作数的表达方式。因为,括号意味着解析操作。
mov al, [var1]
MASM 允许这种表示法,因此只要愿意就可以在程序中使用。由于多数程序(包括 Microsoft 的程序)印刷时都没有用括号,所以,本书只在出现算术表达式时才使用这种带括号的表示法:
mov al,[var1 + 5]
1.MOV指令
MOV 指令将源操作数复制到目的操作数。作为数据传送(data transfer)指令,它几乎用在所有程序中。在它的基本格式中,第一个操作数是目的操作数,第二个操作数是源操作数:
MOV destination,source
其中,目的操作数的内容会发生改变,而源操作数不会改变。
在几乎所有的汇编语言指令中,左边的操作数是目标操作数,而右边的操作数是源操作数。只要按照如下原则,MOV 指令使用操作数是非常灵活的。
- 两个操作数必须是同样的大小。
- 两个操作数不能同时为内存操作数。
- 指令指针寄存器(IP、EIP 或 RIP)不能作为目标操作数。
单条 MOV 指令不能用于直接将数据从一个内存位置传送到另一个内存位置。相反,在将源操作数的值赋给内存操作数之前,必须先将该数值传送给一个寄存器。在将整型常数复制到一个变量或寄存器时,必须考虑该常量需要的最少字节数。尽管 MOV 指令不能直接将较小的操作数复制到较大的操作数中,但是程序员可以想办法解决这个问题。假设要将 count(无符号,16 位)传送到 ECX(32 位),可以先将 ECX 设置为 0,然后将 count 传送到 CX。
MOVZX 指令(进行全零扩展并传送)将源操作数复制到目的操作数,并把目的操作数 0 扩展到 16 位或 32 位。这条指令只用于无符号整数,有三种不同的形式:
MOVZX reg32,reg/mem8
MOVZX reg32,reg/mem16
MOVZX reg16,reg/mem8
在三种形式中,第一个操作数(寄存器)是目的操作数,第二个操作数是源操作数。注意,源操作数不能是常数。
MOVSX 指令(进行符号扩展并传送)将源操作数内容复制到目的操作数,并把目的操作数符号扩展到 16 位或 32 位。这条指令只用于有符号整数,有三种不同的形式:
MOVSX reg32, reg/mem8
MOVSX reg32, reg/mem16
MOVSX reg16, reg/mem8
操作数进行符号扩展时,在目的操作数的全部扩展位上重复(复制)长度较小操作数的最高位。
1.LAHF指令
LAHF(加载状态标志位到 AH)指令将 EFLAGS 寄存器的低字节复制到 AH。被复制的标志位包括:符号标志位、零标志位、辅助进位标志位、奇偶标志位和进位标志位。使用这条指令,可以方便地把标志位副本保管在变量中。
2.SAHF指令
SAHF(保存 AH 内容到状态标志位)指令将 AH 内容复制到 EFLAGS(或 RFLAGS)寄存器低字节。
3.XCHG指令
XCHG(交换数据)指令交换两个操作数内容。该指令有三种形式:
XCHG reg, reg
XCHG reg, mem
XCHG mem, reg
4.直接偏移量操作数
变量名加上一个位移就形成了一个直接 - 偏移量操作数。这样可以访问那些没有显式标记的内存位置。在 16 位的字数组中,每个数组元素的偏移量比前一个多 2 个字节。同样,如果是双字数组,则第一个元素偏移量加 4 才能指向第二个元素。
5.INC 和 DEC 指令
INC(增加)和DEC(减少)指令分别表示寄存器或内存操作数加 1 和减 1。语法如下所示:
INC reg/mem
DEC reg/mem
根据目标操作数的值,溢岀标志位、符号标志位、零标志位、辅助进位标志位、进位标志位和奇偶标志位会发生变化。INC 和 DEC 指令不会影响进位标志位(这还真让人吃惊)。
6.运算指令
ADD 指令将长度相同的源操作数和目的操作数进行相加操作。语法如下:
ADD dest,source
SUB 指令从目的操作数中减去源操作数。指令语法如下:
SUB dest, source
NEG(非)指令通过把操作数转换为其二进制补码,将操作数的符号取反。下述操作数可以用于该指令:
NEG reg
NEG mem
提示:将目标操作数按位取反再加 1,就可以得到这个数的二进制补码。
标志位:进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标 志位根据存入目标操作数的数值进行变化。检查算术运算结果使用的是 CPU 状态标志位的值,同时,这些值还可以触发条件分支指令,即基本的程序逻辑工具。下面是对状态标志位的简要概述:
- 进位标志位意味着无符号整数溢出。比如,如果指令目的操作数为 8 位,而指令产生的结果大于二进制的 1111 1111,那么进位标志位置 1。
- 溢出标志位意味着有符号整数溢出。比如,指令目的操作数为 16 位,但其产生的负数结果小于十进制的 -32 768,那么溢出标志位置 1。
- 零标志位意味着操作结果为 0。比如,如果两个值相等的操作数相减,则零标志位置 1。
- 符号标志位意味着操作产生的结果为负数。如果目的操作数的最高有效位(MSE)置 1,则符号标志位置 1。
- 奇偶标志位是指,在一条算术或布尔运算指令执行后,立即判断目的操作数最低有效字节中 1 的个数是否为偶数。
- 辅助进位标志位置 1,意味着目的操作数最低有效字节中位 3 有进位。
7.OFFSET运算符
OFFSET 运算符返回数据标号的偏移量。这个偏移量按字节计算,表示的是该数据标号距离数据段起始地址的距离。
8.ALIGN伪指令
ALIGN 伪指令将一个变量对齐到字节边界、字边界、双字边界或段落边界。语法如下:
ALIGN bound
9.PTR运算符
PTR 运算符可以用来重写一个已经被声明过的操作数的大小类型。只要试图用不同于汇编器设定的大小属性来访问操作数,那么这个运算符就是必需的。
10.TYPE运算符
TYPE 运算符返回变量单个元素的大小,这个大小是以字节为单位计算的。
11.LENGTHOF运算符
LENGTHOF 运算符计算数组中元素的个数,元素个数是由数组标号同一行出现的数值来定义的。如果数组定义中出现了嵌套的 DUP 运算符,那么 LENGTHOF 返回的是两个数值的乘积。如果数组定义占据了多个程序行,那么 LENGTHOF 只针对第一行定义的数据。
12.LABEL伪指令
LABEL 伪指令可以插入一个标号,并定义它的大小属性,但是不为这个标号分配存储空间。LABEL 中可以使用所有的标准大小属性,如 BYTE、WORD、DWORD、QWORD 或 TBYTE。LABEL 常见的用法是,为数据段中定义的下一个变量提供不同的名称和大小属性。
13.间接寻址
直接寻址很少用于数组处理,因为,用常数偏移量来寻址多个数组元素时,直接寻址不实用。反之,会用寄存器作为指针(称为间接寻址)并控制该寄存器的值。如果一个操作数使用的是间接寻址,就称之为间接操作数。任何一个 32 位通用寄存器(EAX、EBX、ECX、EDX、ESI、EDI、EBP 和 ESP)加上括号就能构成一个间接操作数。寄存器中存放的是数据的地址。如果目的操作数也是间接操作数,那么新值将存入由寄存器提供地址的内存位置。变址操作数是指,在寄存器上加上常数产生一个有效地址。
如果一个变量包含另一个变量的地址,则该变量称为指针。指针是控制数组和数据结构的重要工具,因为,它包含的地址在运行时是可以修改的。比如,可以使用系统调用来分配(保留)一个内存块,再把这个块的地址保存在一个变量中。
14.JMP和LOOP(转移)指令
汇编语言程序使用条件指令来实现如 IF 语句的高级语句与循环。每条条件指令都包含了一个可能的转向不同内存地址的转移(跳转)。控制转移,或分支,是一种改变语句执行顺序的方法,它有两种基本类型:
- 无条件转移:无论什么情况都会转移到新地址。新地址加载到指令指针寄存器,使得程序在新地址进行执行。JMP 指令实现这种转移。
- 条件转移:满足某种条件,则程序出现分支。各种条件转移指令还可以组合起来,形成条件逻辑结构。CPU 基于 ECX 和标志寄存器的内容来解释真 / 假条件。
JMP 指令无条件跳转到目标地址,该地址用代码标号来标识,并被汇编器转换为偏移 量。语法如下所示:
JMP destination
当 CPU 执行一个无条件转移时,目标地址的偏移量被送入指令指针寄存器,从而导致迈从新地址开始继续执行。
LOOP 指令,正式称为按照 ECX 计数器循环,将程序块重复特定次数。ECX 自动成为计数器,每循环一次计数值减 1。语法如下所示:
LOOP destination
循环目标必须距离当前地址计数器 -128 到 +127 字节范围内。LOOP 指令的执行有两个步骤:
- 第一步,ECX 减 1。
- 第二步,将 ECX 与 0 比较。
如果 ECX 不等于 0,则跳转到由目标给岀的标号。否则,如果 ECX 等于 0,则不发生跳转,并将控制传递到循环后面的指令。实地址模式中,CX 是 LOOP 指令的默认循环计数器。同时,LOOPD 指令使用 ECX 为循环计数器,LOOPW 指令使用 CX 为循环计数器。