数据很多时候都保存在主存储器中。尽管可以事先将它们取到寄存器中再进行处理,但指令也需要能够直接寻址存储单元进行数据处理。寻址主存中存储的操作数就称为存储器寻址方式,也称为主存寻址方式。编程时,存储器地址使用包含段选择器和偏移地址的逻辑地址。
段寄存器(段选择器)有默认的使用规则,如表2-8所示。寻址存储器操作数时,段寄存器不用显式说明,数据就在默认的段中,一般是DS段寄存器指向的数据段;如果采用EBP(BP)或ESP(SP)作为基地址指针,则默认使用SS段寄存器指向堆栈段。
表2-8 段寄存器的使用规定
如果不使用默认的段选择器,则需要书写段超越指令前缀显式说明。段超越指令前缀是一种只能跟随在具有存储器操作数的指令之前的指令,其助记符是段寄存器名后跟英文冒号,即CS:、SS:、ES:、FS:或GS:。
因为段基地址由默认的或指定的段寄存器指明,所以指令中只有偏移地址即可。存储器操作数寻址使用的偏移地址常称为有效地址(Effective Address,EA)。但是,指令代码中通常只表达形式地址,由形式地址结合规则,经过计算得到有效地址。最后,处理器将有效地址转换为物理地址访问存储单元。
为了方便各种数据结构的存取,IA-32处理器设计了多种主存寻址方式,但可以统一表达如下(如图2-11所示):
32位有效地址=基址寄存器+变址寄存器×比例+位移量
其中的4个组成部分是:
● 基址寄存器——任何8个32位通用寄存器之一。
● 变址寄存器——除ESP之外的任何32位通用寄存器之一。
● 比例——可以是1、2、4或8(因为操作数的长度可以是1、2、4或8字节)。
● 位移量——可以是8或32位有符号值。
图2-11 IA-32处理器的存储器寻址
需要注意的是,编写运行在32位Windows环境下的应用程序时,必须接受操作系统的管理,不能违反其保护规则。例如,一般不能修改段寄存器的内容,进行数据寻址的地址必须在规定的数据段区域内。如果随意设置访问的逻辑地址,将可能导致非法访问。尽管汇编和连接过程中没有语法错误,但运行时将会提示运行错误。
有效地址只有位移量部分,且直接包含在指令代码中,就是存储器的直接寻址方式。直接寻址使用直接给出的存储器地址访问存储器数据,常用于存取变量。
例如,将变量DVAR的内容(变量值)传送给ECX的指令是:
汇编语言使用地址操作符(中括号)括起变量名(或偏移地址)表示直接寻址,即是在其偏移地址(有效地址)的存储单元读写操作数。假设操作系统为变量DVAR分配的有效地址是00405000H,则该指令的机器代码是8B 0D 00504000,反汇编的指令形式为“MOV ECX,DS:[405000H]”,其源操作数采用直接寻址方式。在代码段中,MASM汇编程序还可以省略变量名加的中括号,同样表示访问变量值,即直接寻址。
图2-12演示了该指令的执行过程:该指令代码中数据的有效地址(图中第①步)与数据段寄存器DS指定的段基地址一起构成操作数所在存储单元的线性地址(图中第②步)。该指令的执行结果是将逻辑地址“DS:00405000H”单元的内容传送至ECX寄存器(图中第③步)。图中假设程序工作在32位保护方式,平展存储模型中DS指向的段基地址等于0。
图2-12 存储器直接寻址
[例2-10]存储器直接寻址程序
本示例程序的每条指令都采用了直接寻址,或为源操作数或为目的操作数,从列表文件可以看到指令代码中包含其相对地址(后缀R是列表文件的一个标志,表示这是一个相对地址,连接时还需要形成真正的偏移地址)。最后一条指令要实现的功能是将DVAR的第一个数据传送到DVAR+4位置,但提示出错,错误信息是“无效的指令操作数”。这是因为绝大多数指令并不支持两个操作数都是存储单元。虽然用高级语言将一个变量赋值给另一个变量很常见,但使用直接寻址访问变量,需要在指令代码中编码有效地址。如果编码两个地址,将导致指令代码过长;而指令执行时至少要访问两次主存,使得指令功能较复杂,硬件实现也较困难。所以,处理器设计人员没有实现这种双操作数都是存储单元的指令。当然,可以先将一个变量转存到某个通用寄存器,然后再传送给另一个变量。也就是说,无法用一条处理器指令实现高级语言中两个变量直接赋值的语句,但使用两条指令就可以了。
有效地址存放在寄存器中,就是采用寄存器间接寻址存储器操作数(如图2-13a所示)。MASM汇编程序使用英文中括号括起寄存器名表示寄存器间接寻址。IA-32处理器的8个32位通用寄存器都可以作为间接寻址的寄存器,但建议主要使用EBX、ESI、EDI,访问堆栈数据时使用EBP。
图2-13 寄存器间接寻址和相对寻址
例如,下面前两条指令的源操作数、后两条指令的目的操作数都是寄存器间接寻址方式:
在寄存器间接寻址中,寄存器的内容是偏移地址,相当于一个地址指针。指令“MOV EDX,[EBX]”执行时,如果EBX=405000H,则该指令等同于“MOV EDX,DS:[405000H]”。
利用寄存器间接寻址,可以方便地对数组中的元素或字符串中的字符进行操作。也就是说,将数组或字符串首地址(或末地址)赋值给通用寄存器,利用寄存器间接寻址就可以访问到数组或字符串头一个(或最后一个)元素或字符,再加减数组元素所占的字节数(对ASCII码字符串来说,每个字符占一个字节)就可以访问到其他元素或字符。
[例2-11]寄存器间接寻址程序
本示例程序实现将源字符串传送给目的字符串DSTMSG(没有给出列表文件内容,请读者重点关注程序)。程序首先利用OFFSET获得源字符串SRCMSG的首地址并将其传送给ESI,用ESI寄存器间接寻址访问源字符串的每个字符。同样,目的字符串DSTMSG采用EDI指向,EDI寄存器间接寻址访问每个字符。再从源字符串取一个字符,通过AL传送给目的字符串(MOV指令不支持两个存储单元直接传送)。接着ESI和EDI都加1(ADD是加法指令)指向下一个字符,重复传送。循环指令LOOP(详见4.3节)利用ECX控制计数:首先将ECX减1,然后判断ECX是否为0,不为0则继续循环执行,为0则结束。所以,程序开始设置ECX等于字符串的长度(字符个数)。
程序最后显示目的字符串内容,用于判断是否传送正确。
寄存器相对寻址的有效地址是寄存器内容与位移量之和(如图2-13b所示)。例如:
在这条指令中,源操作数的有效地址由EBX寄存器内容加位移量4得到,默认与EBX寄存器配合的是DS指向的数据段。再如:
在该指令中,源操作数的有效地址等于EBP-8,与之配合的默认段寄存器为SS。
偏移量还可以采用其他常量形式,也可以使用变量所在的地址作为偏移量。例如:
这里用变量名COUNT(也可以加中括号形式“[COUNT]”)表示其偏移地址用作相对寻址的偏移量,由其地址与寄存器ESI相加的数据作为有效地址访问存储单元。
像寄存器间接寻址一样,利用寄存器相对寻址也可以方便地对数组的元素或字符串的字符进行操作。方法是:用数组或字符串首地址作为位移量,赋值寄存器等于数组元素或字符所在的位置量。
[例2-12]寄存器相对寻址程序
本示例程序采用寄存器相对寻址,变量所在的偏移地址与寄存器EBX相加得到有效地址访问字符串。采用寄存器相对寻址方式的程序更简洁一些,但每个相对地址都需要指令进行加法运算(增加了指令的复杂性)。
要观察程序的动态执行情况,需要利用调试程序。附录A介绍了WinDbg调试程序,大家可以参考,并尝试执行指令和程序,以便获得指令执行的直观感受。
使用变址寄存器寻址操作数称为变址寻址。在变址寄存器不带比例(或者认为比例为1)的情况下,需要配合使用一个基址寄存器(称为基址变址寻址方式),还可以再包含一个位移量(称为相对基址变址寻址方式),存储器操作数的有效地址由一个基址寄存器的内容加上变址寄存器的内容或再加上位移量构成。这种寻址方式适用于二维数组等数据结构。例如:
MASM允许两个寄存器都用中括号,但位移量要书写在中括号前,例如:
偏移量可以使用其他常量形式,也可以使用变量地址,但数字开头不能使用正号或负号。
对应使用变址寄存器的存储器寻址,IA-32处理器支持变址寄存器内容乘以比例1(可以省略)、2、4或8的带比例存储器寻址方式。例如:
主存以字节为可寻址单位,所以地址的加减是以字节单元为单位,比例1、2、4和8分别对应8、16、32和64位数据的字节个数,从而方便以数组元素为单位寻址相应数据。