3.2.3 指令语句、操作数和寻址
指令(Instructions)是CPU执行的操作,通常也称做操作码(Opcode)。操作数(Operand)是指令操作的对象。而地址(Address)是指定数据在内存中的位置。指令语句是程序运行时刻执行的一条语句,它通常可包含4个部分:
标号(可选)。
操作码(指令助记符)。
操作数(由具体指令指定)。
注释。
一条指令语句可以含有0个或最多3个用逗号分开的操作数。对于具有两个操作数的指令语句,第1个是源操作数,第2个是目的操作数,即指令操作结果保存在第2个操作数中。
操作数可以是立即数(即值是常数值的表达式)、寄存器(值在CPU的寄存器中)或内存(值在内存中)。一个间接操作数(Indirect Operand)含有实际操作数值的地址值。AT&T语法通过在操作数前加一个"*"字符来指定一个间接操作数。只有调转/调用指令才能使用间接操作数。见下面对跳转指令的说明。
立即操作数前需要加一个"$"字符前缀。
寄存器名前需要加一个"%"字符前缀。
内存操作数由变量名或者含有变量地址的一个寄存器指定。变量名隐含指出了变量的地址,并指示CPU引用该地址处内存的内容。
1.指令操作码的命名
AT&T语法中指令操作码名称(即指令助记符)最后一个字符用来指明操作数的宽度。字符b、w和l分别指定byte、word和long类型的操作数。如果指令名称没有带这样的字符后缀,并且指令语句中不含内存操作数,那么as就会根据目的寄存器操作数来尝试确定操作数宽度。例如,指令语句"mov %ax, %bx"等同于"movw %ax, %bx"。同样,语句"mov $1, %bx"等同于"movw $1, %bx"。
AT&T与Intel语法中几乎所有指令操作码的名称都相同,但仍有几个例外。符号扩展和零扩展指令都需要2个宽度来指明,即需要为源和目的操作数指明宽度,AT&T语法中通过使用两个操作码后缀来做到。AT&T语法中符号扩展和零扩展的基本操作码名称分别是movs...和movz...,Intel中分别是movsx和movzx。两个后缀就附在操作码基本名上。例如,"使用符号扩展从%al移动到%edx"的AT&T语句是"movsbl %al, %edx",即从byte到long是bl,从byte到word是bw、从word到long是wl。AT&T语法与Intel语法中转换指令的对应关系见表3-2。
表3-2 AT&T语法与Intel语法中转换指令的对应关系
|
AT&T< xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> |
Intel |
说明 |
|
cbtw |
cbw |
把%al中的字节值符号扩展到%ax中 |
|
cwtl |
cwde |
把%ax符号扩展到%eax中 |
|
cwtd |
cwd |
把%ax符号扩展到%dx:%ax中 |
|
cltd |
cdq |
把%eax符号扩展到%edx:%eax中 |
2.指令操作码前缀
操作码前缀用于修饰随后的操作码。它们用于重复字符串指令、提供区覆盖、执行总线锁定操作或指定操作数和地址宽度。通常操作码前缀可作为一条没有操作数的指令独占一行并且必须直接位于所影响指令之前,但是最好与它修饰的指令放在同一行上。例如,串扫描指令scas使用前缀执行重复操作:
repne scas %es:(%edi), %al |
部分操作码前缀见表3-3。
表3-3 操作码前缀列表
|
操作码前缀 |
说明 |
|
cs, ds, ss, es, fs, gs |
区覆盖操作码前缀。通过指定使用 区:内存操作数 内存引用形式会自动添加这种前缀 |
|
data16, addr16 |
操作数/地址宽度前缀。这两个前缀会把32位操作数/地址改变成16位的操作数/地址。但请注意,as并不支持16位寻址方式 |
|
lock |
总线锁存前缀。用于在指令执行期间禁止中断(仅对某些指令有效,请参见80x86手册) |
|
wait |
协处理器指令前缀。等待协处理器完成当前指令的执行。对于80386/80387组合用不着这个前缀 |
|
rep, repe, repne |
串指令操作前缀,使串指令重复执行%ecx中指定的次数 |
3.内存引用
Intel语法的间接内存引用形式:
section:[base + index*scale + disp]
|
对应于如下AT&T语法形式:
section:disp(base, index, scale)
|
其中base和index是可选的32位基寄存器和索引寄存器;disp是可选的偏移值;scale是比例因子,取值范围是1、2、4和8,scale乘以索引index用来计算操作数地址。如果没有指定scale,则scale取默认值1。section为内存操作数指定可选的段寄存器,并且会覆盖操作数使用的当前默认段寄存器。请注意,如果指定的段覆盖寄存器与默认操作的段寄存器相同,则as就不会为汇编的指令再输出相同的段前缀。以下是几个AT&T和Intel语法形式的内存引用例子:
movl var, %eax # 把内存地址var处的内容放入寄存器%eax中。 movl %cs:var, %eax # 把代码段中内存地址var处的内容放入%eax中。 movb $0x0a,%es:(%ebx) # 把字节值0x0a保存到es段的%ebx指定的偏移处。 movl $var, %eax # 把var的地址放入%eax中。 movl array(%esi), %eax # 把array+%esi确定的内存地址处的内容放入%eax中。 movl (%ebx, %esi, 4), %eax # 把%ebx+%esi*4 确定的内存地址处的内容放入%eax中。 movl array(%ebx, %esi, 4), %eax # 把array + %ebx+%esi*4 确定的内存地址处的内容放入%eax中。 movl -4(%ebp), %eax # 把 %ebp -4 内存地址处的内容放入%eax中,使用默认段%ss。 movl foo(,%eax,4), %eax # 把内存地址 foo + eax * 4 处内容放入%eax中,使用默认段%ds。 |
4.跳转指令
跳转指令用于把执行点转移到程序另一个位置处继续执行下去。这些跳转的目的位置通常使用一个标号来表示。在生成目标代码文件时,汇编器会确定所有带有标号的指令的地址,并且把跳转到的指令的地址编码到跳转指令中。跳转指令可分为无条件跳转和条件跳转两大类。条件跳转指令将依赖于执行指令时标志寄存器中某个相关标志的状态来确定是否进行跳转,而无条件跳转则不依赖于这些标志。
JMP是无条件跳转指令,并可分为直接(direct)跳转和间接(indirect)跳转两类,而条件跳转指令只有直接跳转的形式。对于直接跳转指令,跳转到的目标指令的地址是作为跳转指令的一部分直接编码进跳转指令中;对于间接跳转指令,跳转的目的位置取自某个寄存器或某个内存位置中。直接跳转语句的写法是给出跳转目标处的标号;间接跳转语句的写法是必须使用一个星字符"*"作为操作指示符的前缀字符,并且该操作指示符使用与movl指令相同的语法。下面是直接和间接跳转的几个例子。
jmp NewLoc # 直接跳转。无条件直接跳转到标号NewLoc处继续执行。 jmp *%eax # 间接跳转。寄存器%eax的值是跳转的目标位置。 jmp *(%eax) # 间接跳转。从%eax指明的地址处读取跳转的目标位置。
|
同样,与指令计数器PC 无关的间接调用的操作数也必须有一个"*"作为前缀字符。若没有使用"*"字符,那么as汇编器就会选择与指令计数PC相关的跳转标号。还有,其他任何具有内存操作数的指令都必须使用操作码后缀(b、w或l)指明操作数的大小(byte、word或long)。