3.6 80x86的寻址模式
“寻址模式(addressing mode)”是为了访问指令操作数而设置的特定于某种硬件的机制。80x86家族提供3种不同类别的操作数:寄存器操作数、立即操作数及内存操作数。下面几节将讨论这些寻址模式。
3.6.1 80x86的寄存器寻址
80x86指令大都能够操作80x86的通用寄存器。可以将寄存器的名字作为指令操作数来访问该寄存器。
我们用80x86的mov指令来看一些示例,了解汇编器是如何实现这种策略的。
3.6.1.1 HLA中的寄存器访问
HLA的mov指令看上去是这样的:
mov( source, destination );
该指令将操作数source中的数据复制到操作数destination。8位、16位和32位寄存器都可以作为该指令的有效操作数。对两个操作数的唯一限制就是要求其位数相同。
我们来看一些实际的80x86 mov指令:
mov( bx, ax ); // 将BX的内容传送到AX
mov( al, dl ); // 将AL的内容传送到DL
mov( edx, esi ); // 将EDX的内容传送到ESI
3.6.1.2 Gas中的寄存器访问
Gas中的每个寄存器名要冠以百分号“%”,例如:
%al, %ah, %bl, %bh, %cl, %ch, %dl, %dh
%ax, %bx, %cx, %dx, %si, %di, %bp, %sp
%eax, %ebx, %ecx, %edx, %esi, %edi, %ebp, %esp
Gas的mov指令语法与HLA雷同,只是无需括号和分号,并且要求其汇编语句在源代码中不能跨行。例如:
mov %bx, %ax // 将BX的内容传送到AX
mov %al, %dl // 将AL的内容传送到DL
mov %edx, %esi // 将EDX的内容传送到ESI
3.6.1.3 MASM和TASM中的寄存器访问
MASM和TASM汇编器使用的寄存器名与HLA相同,基本语法则类似于Gas,但源和目标操作数须互换位置。也就是说,典型的mov指令是这样:
mov destination, source
这里有一些MASM和TASM语法的mov指令示例:
mov ax, bx ; 将BX的内容传送到AX
mov dl, al ; 将AL的内容传送到DL
mov esi, edx ; 将EDX的内容传送到ESI
3.6.2 立即寻址
将寄存器和内存作为操作数的指令多数也能用立即数,即以常量为操作数。例如HLA 的下列mov指令将数值送入相应的目标寄存器:
mov( 0, al );
mov( 12345, bx );
mov( 123_456_789, ecx );
采用立即寻址模式时,大部分汇编器都允许指定各种各样的文字常量类型。例如,可以提供十六进制、十进制和二进制形式的数;也可以提供字符常量作为操作数。原则是常量必须在目标操作数中放得下。下面是一些HLA、Gas和MASM/TASM的例子:
mov( 'a', ch ); // HLA
mov 'a, %ch // Gas
mov ch, 'a' ;MASM/TASM
mov( $1234, ax ); // HLA
mov 0x1234, %ax // Gas
mov ax, 1234h ;MASM/TASM
mov( 4_012_345_678, eax ); // HLA
mov 4012345678, %eax // Gas
mov eax, 4012345678 ;MASM/TASM
几乎每个汇编器都允许创建符号常量名,并将这些常量名当作操作数。例如,HLA预定义了两个布尔常量true和false,我们可以将其作为mov指令的操作数:
mov( true, al );
mov( false, ah );
某些汇编器甚至可用指针常量等抽象数据类型的常量,请参看对应汇编器的手册来了解用法细节。
3.6.3 位移寻址
最常见且最易理解的寻址模式是位移寻址,即直接寻址,其中通过32位常量指定内存单元的位置。内存单元可以是源操作数,也可以是目标操作数。
举个例子,假设变量J是位于地址$8088的字节变量,HLA指令“mov (J, al);”则指的是将位于内存地址$8088中的内容传送入寄存器AL中。类似地,如果字节变量K在内存中位于$1234,指令“mov (dl, K);”则指的是把寄存器DL中的内容传送到地址单元$1234中,请参看图3-4。
位移寻址模式非常合适于访问简单的标量变量。高级语言程序中通常使用这种寻址模式访问静态变量或全局变量。
注意:Intel将这种寻址模式称作“位移寻址”,是因为操作码mov后面跟着32位常量的内存地址(即位移值)。在80x86处理器上,位移值是相对于内存起始位置(即地址0)的偏移量。
图3-4 位移寻址模式(直接寻址)
本章中的示例通常访问内存中的字节型数据。但也要知道,在80x86处理器上通过指定第一个字节的地址,同样可以访问字和双字,请参看图3-5。
图3-5 使用直接寻址模式访问字或双字型数据
MASM、TASM和Gas对于位移寻址模式所采用的语法与HLA相同,即只要指定欲访问的变量名作为操作数即可。有的MASM、TASM程序员爱用方括号括住变量名,尽管在这些汇编器中并无必要这么做。
下面是一些使用HLA、Gas和MASM/TASM语法的示例:
mov( byteVar, ch ); // HLA
movb byteVar, %ch // Gas
mov ch, byteVar ;MASM/TASM
mov( wordVar, ax ); // HLA
movw wordVar, %ax // Gas
mov ax, wordVar ; MASM/TASM
mov( dwordVar, eax ); // HLA
movl dwordVar, %eax // Gas
mov eax, dwordVar ; MASM/TASM
3.6.4 寄存器间接寻址
80x86 CPU能够使用寄存器间接寻址模式,即通过寄存器间接访问内存。我们称这种模式为“间接”,是因为操作数本身并非地址,操作数的值才是要用的内存地址。在寄存器间接寻址模式中,寄存器的值指向要访问的地址。例如,HLA指令“mov (eax, [ebx]);”告诉CPU,将EAX中的内容保存到以EBX值为地址的内存单元中。
3.6.4.1 HLA的寄存器间接寻址
80x86的这种寻址模式有8个形式,使用HLA时是这样的:
mov( [eax], al );
mov( [ebx], al );
mov( [ecx], al );
mov( [edx], al );
mov( [edi], al );
mov( [esi], al );
mov( [ebp], al );
mov( [esp], al );
这8个寻址形式以方括号内寄存器(分别为EAX、EBX、ECX、EDX、EDI、ESI、EBP和ESP)的内容作为偏移量,寻址到相应的内存单元。
注意:寄存器间接寻址模式要求通过32位寄存器指定内存地址,这种寻址模式下不能用8位或16位寄存器指定。
3.6.4.2 MASM和TASM的寄存器间接寻址
MASM和TASM在寄存器间接寻址模式的语法与HLA基本一样——用一对方括号括住寄存器名——只是操作数的顺序在mov指令中正好颠倒。
前面的HLA指令与MASM/TASM等效为:
mov al, [eax]
mov al, [ebx]
mov al, [ecx]
mov al, [edx]
mov al, [edi]
mov al, [esi]
mov al, [ebp]
mov al, [esp]
3.6.4.3 Gas的寄存器间接寻址
Gas则以圆括号括住寄存器名。前面HLA的mov指令其Gas形式如下:
movb (%eax), %al
movb (%ebx), %al
movb (%ecx), %al
movb (%edx), %al
movb (%edi), %al
movb (%esi), %al
movb (%ebp), %al
movb (%esp), %al
3.6.5 变址寻址
在所有地址运算完成后,指令终将访问的内存地址称为“有效地址(effective address)”。变址寻址模式通过将变量地址——也称为“位移(displacement)”或“偏移(offset)”——与32位寄存器值相加,计算出有效地址,这才是指令准备访问的内存地址。所以如果VarName位于地址$1100,而EBX内容为8,则HLA的“mov (VarName[ebx], al);”将会把地址单元$1108的内容放入寄存器AL,请参看图3-6。
图3-6 变址寻址模式
3.6.5.1 HLA的变址寻址
HLA中采用下列变址寻址语法,其中VarName为程序中某个静态变量的名字。
mov( VarName[ eax ], al );
mov( VarName[ ebx ], al );
mov( VarName[ ecx ], al );
mov( VarName[ edx ], al );
mov( VarName[ edi ], al );
mov( VarName[ esi ], al );
mov( VarName[ ebp ], al );
mov( VarName[ esp ], al );
3.6.5.2 MASM和TASM的变址寻址
MASM、TASM的语法与HLA相同,它们还允许多种语法变形。下面是先前变址寻址指令操作数的等效形式,并给出了MASM和TASM支持的变形。
varName[reg32]
[reg32+varName]
[varName][reg32]
[varName+reg32]
[reg32][varName]
varName[reg32+const]
[reg32+varName+const]
[varName][reg32][const]
varName[const+reg32]
[const+reg32+varName]
[const][reg32][varName]
varName[reg32-const]
[reg32+varName-const]
[varName][reg32][-const]
MASM和TASM还允许其他很多组合。这些汇编器将方括号内的并列项当作它们被用加号分隔一样。由于加法满足交换率,组合的种类很多。
这里给出的MASM/TASM语句等效于前节中的HLA示例:
mov al, VarName[ eax ]
mov al, VarName[ ebx ]
mov al, VarName[ ecx ]
mov al, VarName[ edx ]
mov al, VarName[ edi ]
mov al, VarName[ esi ]
mov al, VarName[ ebp ]
mov al, VarName[ esp ]
3.6.5.3 Gas的变址寻址
和寄存器间接寻址相同,Gas在变址寻址模式中仍然用圆括号而非方括号。下面是Gas允许的变址寻址语法:
varName(%reg32)
const(%reg32)
varName+const(%reg32)
前面的HLA指令在Gas中等效为:
movb VarName( %eax ), al
movb VarName( %ebx ), al
movb VarName( %ecx ), al
movb VarName( %edx ), al
movb VarName( %edi ), al
movb VarName( %esi ), al
movb VarName( %ebp ), al
movb VarName( %esp ), al
3.6.6 比例变址寻址
比例变址寻址模式与变址寻址类似,区别只有两点。比例变址寻址还允许:
l 将两个寄存器值相加,外加一个位移量
l 可将变址寄存器的值乘以比例因子1、2、4或8
怎么做呢?请看下面的HLA例子:
mov( eax, VarName[ ebx + esi*4 ] );
比例变址寻址模式与变址寻址的主要差异在于它们有无“esi*4”。该示例通过将EBX加上4倍的ESI值来求得有效地址,图3-7给出了比例变址寻址模式的地址计算过程,其中scale指比例因子,本例中scale等于4。
图3-7 比例变址寻址模式
3.6.6.1 HLA的比例变址寻址
HLA语法提供若干方式来指定使用比例变址寻址模式。这里有一些语法形式:
VarName[ IndexReg32 * scale ]
VarName[ IndexReg32 * scale + displacement ]
VarName[ IndexReg32 * scale - displacement ]
[ BaseReg32 + IndexReg32 * scale ]
[ BaseReg32 + IndexReg32 * scale + displacement ]
[ BaseReg32 + IndexReg32 * scale - displacement ]
VarName[ BaseReg32 + IndexReg32 * scale ]
VarName[ BaseReg32 + IndexReg32 * scale + displacement ]
VarName[ BaseReg32 + IndexReg32 * scale - displacement ]
例子中的BaseReg32表示任意32位通用寄存器,IndexReg32则表示除ESP外的任意32位通用寄存器,scale必须是1、2、4、8中的某个常数,而VarName表示静态变量名。
3.6.6.2 MASM和TASM的比例变址寻址
MASM、TASM支持的语法与HLA相同,但另有一些形式与变址寻址中给出的语法对应。后者只是基于加法交换率的语法变形。
3.6.6.3 Gas的比例变址寻址
Gas还是一如既往使用圆括号而非方括号来括住比例变址操作数。Gas也使用3个操作数的语法来指定基址寄存器、变址寄存器和比例值,不像其他汇编器用算术表达式给出。Gas比例变址寻址表达式的一般语法是:
expression( baseReg32, indexReg32, scaleFactor )
具体有以下形式:
VarName( ,IndexReg32, scale )
VarName + displacement( ,IndexReg32, scale )
VarName - displacement( ,IndexReg32, scale )
( BaseReg32, IndexReg32, scale )
displacement( BaseReg32, IndexReg32, scale )
VarName( BaseReg32, IndexReg32, scale )
VarName + displacement( BaseReg32, IndexReg32, scale )
VarName - displacement( BaseReg32, IndexReg32, scale )
3.8 在汇编语言中指定操作数尺寸
80x86上的汇编器通过两种机制指定操作数尺寸:
l 操作数使用类型检查来说明尺寸——多数汇编器都这么做
l 指令本身指定了尺寸——Gas是这么做的
举例来说,我们考虑下列3条HLA的mov指令:
mov( 0, al );
mov( 0, ax );
mov( 0, eax );
每个情况里都由寄存器操作数说明了mov指令传送到那个寄存器的数据尺寸。MASM/TASM采用类似的语法,只是操作数要互相调换位置:
mov al, 0 ;8位数据传送
mov ax, 0 ;16位数据传送
mov eax, 0 ;32位数据传送
一定要注意的是,这6条语句的指令助记符都是一样的,均为mov。正是操作数而非指令助记符指明了要传送数据的尺寸。
3.8.1 HLA的类型强制
不过,这种方法存在一个问题。请考虑下列的HLA示例:
mov( 0, [ebx] ); //将0复制到地址为[ebx]的内存单元
该指令是含糊不清的:EBX指向的内存位置可以是字节、字或双字。指令并未把操作数的尺寸告诉给汇编器。要是遇到这样的指令,汇编器就会报错,我们必须明确指出内存操作数的尺寸。如果是在HLA中,这可以通过类型强制运算符实现:
mov( 0, (type word [ebx]) ); //16位数据传送
通常用下列HLA语法能将任何内存操作数强制成适当尺寸:
(type new_type memory)
其中new_type表示byte、word或dword等数据类型,memory为要强制类型的内存地址。
3.8.2 MASM和TASM的类型强制
MASM和TASM同样存在这一问题。应当这么使用强制运算符来指定内存单元:
mov word ptr [ebx], 0 ;16位数据传送
当然,将例子中的word替换成byte或dword,就能将内存单元强制为字节或双字尺寸。
3.8.3 Gas的类型强制
Gas汇编器无需强制运算符,因为它采用了截然不同的技术来指定操作数的尺寸——由指令助记符明确说明尺寸。Gas的mov指令不只有一个助记符,而是使用三种指令助记符,由mov和一个字符后缀组成,具体有:
movb 传送8位数据(字节)
movw 传送16位数据(字)
movl 传送32位数据(双字)
采用这些指令助记符时,即使操作数尺寸没有明确,也永远不会产生歧义。例如:
movb 0, (%ebx) //8位数据传送
movw 0, (%ebx) //16位数据传送
movl 0, (%ebx) //32位数据传送