汇编语言:基于x86处理器-学习笔记-第四章
第四章学习笔记
操作数
操作数类型
《汇编语言:基于x86处理器(原书第7版)》 Page 73
x86 指令格式:[label:] mnemonic [operands] [; comment]
。其中,指令包含的操作数个数可以是 0个,1个,2个或3个。
操作数有 3 种基本类型:
- 立即数 (
imm
) ——使用数字文本表达式 - 寄存器操作数 (
reg
) ——使用 CPU 内已命名的寄存器 - 内存操作数 (
mem
) ——引用内存位置
直接内存操作数
《汇编语言:基于x86处理器(原书第7版)》 Page 74
变量名引用的是数据段内的偏移量。
例如,如下变量 var1 的声明表示,该变量的大小类型为字节,值为十六进制的 10:
1 |
|
可以编写指令,通过内存操作数的地址来解析(查找)这些操作数。
假设 var1 的地址偏移量为 10400h。如下指令将该变量的值复制到 AL 寄存器中:
1 |
|
指令会被汇编为下面的机器指令:
1 |
|
这条机器指令的第一个字节是操作代码(即操作码 (opcode) )。剩余部分是 var1 的 32 位十六进制地址。虽然编程时有可能只使用数字地址,但是如同 var1 一样的符号标号会让使用内存更加容易。
直接-偏移量操作数
《汇编语言:基于x86处理器(原书第7版)》 Page 78
在变量名称后加上一个偏移值,可以创建直接偏移 (direct-offset) 操作数,可以通过它来访问没有显示标号的内存地址。我们以一个名为 arrayB 的字节数组开始枚举:
1 |
|
如果是双字节或者双字(四个字节)或者是其他,注意偏移的时候地址分别是 2、4 或者其他等。
数据传送指令
《汇编语言:基于x86处理器(原书第7版)》 Page 75
MOV 指令
MOV指令将源操作数复制到目的操作数。
在它的基本格式中,第一个操作数是目的操作数,第二个操作数是源操作数:
1 |
|
其中,目的操作数的内容会发生改变,而源操作数不会改变。
下面是 MOV 指令的标准格式:
1 |
|
需要特别注意以下的原则:
- 两个操作数必须是同样的大小。
- 两个操作数不能同时为内存操作数。
- 指令指针寄存器 (
IP
、EIP
、RIP
) 不能作为目标寄存器。
内存到内存
单条 MOV 指令不能用于直接将数据从一个内存位置传送到另一个内存位置。相反,在将源操作数的值赋给内存操作数之前,必须先将该数值传送给一个寄存器。在将整型常数复制到一个变量或寄存器时,必须考虑该常量需要的最少字节数。
覆盖值
下述代码示例演示了怎样通过使用不同大小的数据来修改同一个32位寄存器。
当 oneWord 字传送到 AX 时,它就覆盖了 AL 中已有的值。
当 oneDword 传送到 EAX 时,它就覆盖了 AX 的值。
最后,当 0 被传送到 AX 时,它就覆盖了 EAX 的低半部分。
整数的全零/符号扩展
把一个较小的值复制到一个较大的操作数
尽管 MOV 指令不能直接将较小的操作数复制到较大的操作数中,但是程序员可以想办法解决这个问题。
假设要将count (无符号,16位) 传送到ECX (32位),可以先将 ECX 设置为 0,然后将 count 传送到 CX:
1 |
|
如果对一个有符号整数 -16 进行同样的操作会发生什么呢?
1 |
|
ECX中的值 (+65520) 与 -16 完全不同。
但是,如果先将 ECX 设置为 FFFFFFFFh,然后再把 signedVal 复制到 CX,那么最后的值就是完全正确的:
1 |
|
本例的有效结果是用源操作数的最高位(1)来填充目的操作数ECX的高16位,这种技术称为符号扩展( sign extension)。
当然,不能总是假设源操作数的最高位是1。
幸运的是,Intel 的工程师在设计指令集时已经预见到了这个问题,因此,设置了 MOVZX 和 MOVSX 指令来分别处理无符号整数和有符号整数。
MOVZX 指令
MOVZX 指令(进行全零扩展并传送)将源操作数复制到目的操作数,并把目的操作数0扩展到 16 位或 32 位。
这条指令只用于无符号整数,有三种不同的形式:
1 |
|
在三种形式中,第一个操作数(寄存器)是目的操作数,第二个操作数是源操作数。注意,源操作数不能是常数。
MOVSX 指令
MOVSX 指令(进行符号扩展并传送)将源操作数内容复制到目的操作数,并把目的操作数符号扩展到 16 位或 32 位。
这条指令只用于有符号整数,有三种不同的形式:
1 |
|
操作数进行符号扩展时,在目的操作数的全部扩展位上重复(复制)长度较小操作数的最高位。
LAHF 和 SAHF 指令
LAHF(load status flags into AH,加载状态标志位到 AH)指令将 EFLAGS 寄存器的低字节复制到 AH。被复制的标志位包括:符号标志位、零标志位、辅助进位标志位、奇偶标志位和进位标志位。使用这条指令,可以方便地把标志位副本保管在变量中:
1 |
|
SAHF(store AH into status flags,保存 AH 内容到状态标志位)指令将 AH 内容复制到 EFLAGS(或 RFLAGS)寄存器低字节。例如,可以检索之前保存到变量中的标志位数值:
1 |
|
XCHG 指令
XCHG(exchange data,交换数据)指令交换两个操作数内容。该指令有三种形式:
1 |
|
除了 XCHG 指令不使用立即数作操作数之外,XCHG 指令操作数的要求与 MOV 指令操作数要求是一样的。即:
- 两个操作数不能同时都为内存操作数。
- 任何一个操作数都不能为立即数。
- 指令指针寄存器 (
IP
、EIP
、RIP
) 不能作为目标寄存器。 - 两个操作数必须是同样的大小。
以下是一些使用 XCHG 指令的例子:
1 |
|
若要交换两个内存操作数,需要使用一个寄存器作为临时存储容器,并把 MOV 指令和 XCHG 指令结合起来使用:
1 |
|
习题整理
下列语句中,有语法错误的是:(D)
A. add esi, TYPE DWORD
B. pop eax
C. repe cmpsd
D. mov val1, val2
MOV 指令两个操作数不能同时为内存操作数。
程序示例1
《汇编语言:基于x86处理器(原书第7版)》 Page 79
该程序中包含了本文之前介绍的所有指令,包括:MOV、XCHG、MOVSX 和 MOVZX,展示了字节、字和双字是如何受到它们的影响。同时,程序中还包括了一些直接-偏移量操作数。
加法和减法
《汇编语言:基于x86处理器(原书第7版)》 Page 81
INC 和 DEC 指令
INC (increment) 和 DEC (decrement) 指令从操作数中加 1 或减 1,格式是:
1 |
|
根据目标操作数的值,溢出标志位、符号标志位、零标志位、辅助进位标志位、进位标志位和奇偶标志位会发生变化。
INC和 DEC指令不会影响进位标志位。
ADD 指令
ADD 指令将同尺寸的源操作数和目的操作数相加,格式是:ADD dest, source
。
在操作中,源操作数不能改变,相加之和存放在目的操作数中。
该指令可以使用的操作数与 MOV 指令相同。
进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标志位根据存入目标操作数的数值进行变化。
SUB 指令
SUB 指令将源操作数从目的操作数中减掉,操作数格式与 ADD 和 MOV 指令操作数相同。
格式是:SUB dest, source
。
进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标志位根据存入目标操作数的数值进行变化。
有一种执行减法而无需使用额外的数字电路单元的简单方法:对源操作数求补,然后把源操作数和目的操作数相加。
NEG 指令
NEG (negate) 指令通过将数字转换为对应的补码而求得其相反数(将目标操作数按位取反再加1,就可以得到这个数的二进制补码)。
格式是:
1 |
|
进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标志位根据存入目标操作数的数值进行变化。
加减法影响的标志位
《汇编语言:基于x86处理器(原书第7版)》 Page 83
- 进位标志位意味着无符号整数溢出。
比如,如果指令目的操作数为 8 位,而指令产生的结果大于二进制的 1111 1111,那么进位标志位置 1。 - 溢出标志位意味着有符号整数溢出。
比如,指令目的操作数为 16 位,但其产生的负数结果小于十进制的 -32768,那么溢出标志位置 1。 - 零标志位意味着操作结果为 0。
比如,如果两个值相等的操作数相减,则零标志位置 1。 - 符号标志位意味着操作产生的结果为负数。如果目的操作数的最高有效位(MSB)置 1,则符号标志位置 1。
- 奇偶标志位是指,在一条算术或布尔运算指令执行后,立即判断目的操作数最低有效字节中1的个数是否为偶数。
- 辅助进位标志位置1,意味着目的操作数最低有效字节中位 3 有进位。
习题整理
下列语句中,有语法错误的是:(B)
A. mov ax, WORD PTR value
B. inc [esi]
C. movzx cx,bl
D. movsx edx,bl
INC 指令只能对寄存器操作数和内存操作数使用。
和数据相关的操作符和伪指令
《汇编语言:基于x86处理器(原书第7版)》 Page 87
MASM 操作符或伪指令获取数据的地址以及大小等特征信息:
OFFSET 操作符返回一个变量相对于其所在段开始的偏移。
PTR 操作符允许重载变量的默认尺寸。
TYPE 操作符返回数组中每个元素的大小(以字节计算)。
LENGTHOF 操作符返回数组内元素的数目。
SIZEOF 操作符返回数组初始化时占用的字节数。
除此之外,LABEL 伪指令可以用不同的大小类型来重新定义同一个变量。
OFFSET 运算符
OFFSET 操作符返回数据标号的偏移地址。偏移地址代表标号距离数据段开始的距离,单位是以字节计算的。
在下面的例子中,将用到如下三种类型的变量:
1 |
|
假设 bVal 在偏移量为 0040 4000 (十六进制) 的位置,则 OFFSET 运算符返回值如下:
1 |
|
OFFSET 也可以应用于直接-偏移量操作数。
设 myArray 包含 5 个 16 位的字。下面的 MOV 指令首先得到 myArray 的偏移量,然后加 4,再将形成的结果地址直接传送给 ESI。因此,现在可以说 ESI 指向数组中的第 3 个整数。
1 |
|
ALIGN 运算符
ALIGN伪指令将变量的位置按字节、字、双字或段边界对齐,语法是:ALIGN bound。bound 可以取的值有:1、2、4、8、16。指令把地址直接对齐到所指定 bound 的倍数上。
1 |
|
请注意,dVal 的偏移量原本是 0040 4005,但是 ALIGN 4
伪指令使它的偏移量成为 0040 4008。
PTR 操作符
PTR 操作符来重载操作数声明的默认尺寸,这在试图以不同于变量声明时所使用的尺寸属性访问变量的时候非常有用。
例如,假设要讲双字变量 myDouble 的低 16 位传送给 AX 寄存器,由于操作数大小不匹配,编译器将不允许下面的数据传送指令:
1 |
|
但是 WORD PTR
操作符使得将低字(5678)传送给 AX 成为可能:
1 |
|
注意,PTR 必须与一个标准汇编数据类型一起使用,这些类型包括:BYTE、SBYTE、WORD、SWORD、DWORD、SDWORD、FWORD、QWORD 或 TBYTE。
TYPE 运算符
TYPE运算符返回变量单个元素的大小,这个大小是以字节为单位计算的。
比如,TYPE为字节,返回值是1 ;TYPE为字,返回值是2;TYPE为双字,返回值是4;TYPE为四字,返回值是8。
示例如下:
1 |
|
下面是每个 TYPE 表达式的值:
LENGTHOF 运算符
LENGTHOF 运算符计算数组中元素的个数,元素个数是由数组标号同一行出现的数值来定义的。示例如下:
如果数组定义中出现了嵌套的 DUP 运算符,那么 LENGTHOF 返回的是两个数值的乘积。
下表列出了每个 LENGTHOF 表达式返回的数值。
SIZEOF 运算符
SIZEOF 运算符返回值等于 LENGTHOF 与 TYPE 返回值的乘积。
如下例所示,intArray 数组的 TYPE = 2,LENGTHOF = 32,因此,SIZEOF intArray
= 64:
1 |
|
LABEL 伪指令
LABEL 伪指令允许插入一个标号并赋予其尺寸属性而无需任何实际的存储空间。LABEL 伪指令可以使用 BYTE、WORD、DWORD、QWORD 或 TBYTE 等任意的标准尺寸属性。LABEL 伪指令的一种常见的用法是为数据段内其后定义的变量提供一个别名以及一个不同的尺寸属性。
下例中在 val32 前面声明了一个名为 val16 的标号并赋予其 WORD 属性:
1 |
|
val16 是名为 val32 的存储地址的一个别名。LABEL 伪指令本身并不占用实际存储空间。
习题整理
下列语句中,有语法错误的是:(A)
A. mov ax, PTR value
B. inc esi
C. xchg ebx, eax
D. add esi, TYPE DWORD
PTR 必须与一个标准汇编数据类型一起使用,这些类型包括:BYTE、SBYTE、WORD、SWORD、DWORD、SDWORD、FWORD、QWORD 或 TBYTE。
间接寻址
《汇编语言:基于x86处理器(原书第7版)》 Page 91
直接寻址很少用于数组处理,因为,用常数偏移量来寻址多个数组元素时,直接寻址不实用。反之,会用寄存器作为指针(称为间接寻址)并控制该寄存器的值。
如果一个操作数使用的是间接寻址,就称之为间接操作数。
间接操作数
任何一个 32 位通用寄存器(EAX、EBX、ECX、EDX、ESI、EDI、EBP 和 ESP)加上括号就能构成一个间接操作数。寄存器中存放的是数据的地址。
1 |
|
如果目的操作数也是间接操作数,那么新值将存入由寄存器提供地址的内存位置。
在下面的例子中,BL 寄存器的内容复制到 ESI 寻址的内存地址中:
1 |
|
数组
间接操作数是步进遍历数组的理想工具。
下例中,arrayB 有 3 个字节。随着 ESI 不断加 1,它就能顺序指向每一个字节:
1 |
|
如果使用 16 位的整数数组,就需要每次给 ESI 加 2 以便寻址后续的各个数组元素。
变址操作数
变址操作数是指,在寄存器上加上常数产生一个有效地址。每个 32 位通用寄存器都可以用作变址寄存器。
MASM 可以用不同的符号来表示变址操作数(括号是表示符号的一部分):
1 |
|
第一种形式是变量名加上寄存器。变量名由汇编器转换为常数,代表的是该变量的偏移量。
变址寻址的第二种形式是寄存器加上常数偏移量。变址寄存器保存数组或结构的基址,常数标识各个数组元素的偏移量。
两种形式的效果是相同的。
1 |
|
JMP 和 LOOP 指令
《汇编语言:基于x86处理器(原书第7版)》 Page 95
默认情况下,CPU是顺序加载并执行程序。但是,当前指令有可能是有条件的,也就是说,它按照 CPU 状态标志(零标志、符号标志、进位标志等)的值把控制转向程序中的新位置。汇编语言程序使用条件指令来实现如 IF 语句的高级语句与循环。每条条件指令都包含了一个可能的转向不同内存地址的转移(跳转)。控制转移,或分支,是一种改变语句执行顺序的方法,它有两种基本类型:
无条件转移:无论什么情况都会转移到新地址。新地址加载到指令指针寄存器,使得程序在新地址进行执行。JMP 指令实现这种转移。
条件转移:满足某种条件,则程序出现分支。各种条件转移指令还可以组合起来,形成条件逻辑结构。CPU 基于 ECX 和标志寄存器的内容来解释真 / 假条件。
JMP 指令
JMP 指令无条件跳转到目标地址,该地址用代码标号来标识,并被汇编器转换为偏移量。语法是:JMP destination
。
当 CPU 执行一个无条件转移时,目标地址的偏移量被送入指令指针寄存器,从而导致从新地址开始继续执行。
JMP 指令提供了一种简单的方法来创建循环即跳转到循环开始时的标号:
1 |
|
JMP 是无条件的,因此循环会无休止地进行下去,除非找到其他方法退出循环。
LOOP 指令
LOOP指令,正式称为按照 ECX 计数器循环,将程序块重复特定次数。ECX 自动成为计数器,每循环一次计数值减 1。
语法是:LOOP destination
。
循环目标必须距离当前地址计数器 -128 到 +127 字节范围内。
LOOP 指令的执行有两个步骤:第一步,ECX 减 1;第二步,将 ECX 与 0 比较。如果 ECX 不等于 0,则跳转到由目标给出的标号。否则如果 ECX 等于 0,则不发生跳转,并将控制传递到循环后面的指令。
在下例中,每次执行循环时 AX 加 1,当循环结束的时候 AX = 5,ECX = 0:
1 |
|
在循环内创建另一个循环的时候,必须考虑 ECX 中的外层循环计数该如何处理。一个较好的解决方案是把外层循环的技术保存在一个变量中。作为一条一般性的规则,应该尽量避免使用嵌套深度超过两层的循环。否则,管理循环计数将很复杂。
程序示例
《汇编语言:基于x86处理器(原书第7版)》 Page 98
整数数组求和
复制字符串
MOV 指令不能同时有两个内存操作数,所以,每个源字符串字符送入 AL,然后再从 AL 送入目标字符串。