购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.10 x64架构的寻址方式

2.10.1 x64架构下传统模式的寻址方式

有些指令执行时需要再次访问内存以取得实际的操作数。在段部件和页部件之前的工作是生成有效地址,即,处理器根据指令计算有效地址。

x86处理器的寻址方式非常复杂,但并不是没有规律。x86的寻址方式可以追溯到16位处理器的时代,16位处理器从实地址模式开始,然后引入16位保护模式。在实地址模式和16位保护模式下,生成16位有效地址,而且有一套指定和计算有效地址的方法,这就是传统的16位寻址方式。

如图2-29所示,16位寻址方式是用BX或者BP加上SI或者DI,再加上一个8位或者16位的距离得到的。实际上,这三个部分并不需要全都存在。

图2-29 传统的16位寻址方式

如果省略前两个部分,则有效地址来自16位的距离,我们称之为绝对地址,比如

如果省略后两个部分,则有效地址来自BX或者BP,比如

如果省略第三个部分,则有效地址来自第一部分的BX和BP,加上第二部分的SI和DI,比如

如果指令中指定了全部的三个部分,则有效地址来自这三个部分的相加,比如

时间过得很快,转眼到了32位处理器的时代。32位处理器兼容16位处理器,所以也兼容16位处理器的寻址方式。但是,32位处理器的主要特色是引入了32位保护模式。在32位保护模式下,生成32位有效地址,而且也有一套指定和计算有效地址的方法,这就是传统的32位寻址方式。

如图2-30所示,32位寻址方式是用32位的通用寄存器,加上32位通用寄存器乘以倍率,再加上一个8位或者32位的距离,来得到32位的有效地址。实际上,这四个部分并不需要全都存在。

图2-30 32位寻址方式

如果省略前三个部分,则有效地址来自32位的距离,叫作绝对地址,比如

如果省略后三个部分,则有效地址来自8个32位通用寄存器之一,比如

如果省略倍率和距离,则有效地址来自第一部分和第二部分的相加,比如

此时,可以认为倍率是1,即,在这里是ebx乘1;

如果省略了距离,则有效地址来自前三个部分,比如

如果指令中指定了全部的四个部分,则有效地址来自第一部分加第二部分乘以倍率,再加上距离,比如

2.10.2 x64架构下IA-32e模式的寻址方式

对于x64架构的处理器,由于它兼容传统模式,所以它依然支持我们前面所讲的寻址方式,前提是工作在传统模式(实地址模式、16位保护模式或者32位保护模式)下。

x64架构的处理器有自己独立的IA-32e模式,IA-32e模式有两个子模式,分别是兼容模式和64位模式。在IA-32e的兼容模式下运行传统的16位或者32位保护模式程序,并依然采用对应的16位或者32位寻址方式,生成的有效地址是16位或32位的,加0扩展到64位,这是因为IA-32e模式使用64位线性地址;在IA-32e的64位模式下,生成64位有效地址,而且有一套计算有效地址的方法,即,64位寻址方式。

如图2-31所示,64位模式的寻址方式是用64位的通用寄存器,加上64位通用寄存器乘以倍率,再加上一个8位或者32位的距离,来得到64位的有效地址。注意,第二部分不包括RSP,这和32位寻址方式类似。

图2-31 64位寻址方式

和传统的16位、32位寻址方式一样,实际上,这四个部分并不需要全都存在。如果省略前三个部分,则有效地址来自32位的距离,叫作绝对地址,比如

如果省略后三个部分,则有效地址来自64位通用寄存器之一,比如

下面这条指令指定了全部的四个部分,它是用r8加上r15乘以8,再加上66:

2.10.3 64位模式的RIP相对寻址方式

顾名思义,RIP相对寻址是相对于64位的指令指针RIP来寻址的,这种寻址方式只能在64位模式下使用。看过《x86汇编语言:从实模式到保护模式》一书的人都知道并能够体会程序的浮动会影响指令的执行,所以程序加载后还要进行重定位。在平坦模型下,程序不再分段,程序中的任何内容之间都有确定的相对距离。因此,64位模式引入RIP相对寻址方式,以适应这种变化。对于深受程序重定位之苦的人来说,RIP相对寻址是他们梦寐以求的寻址方式。

在分段模型下,程序的重定位很简单,因为指令中的有效地址也是段内偏移量,我们只需要将程序加载的位置确定为段地址就可以了。但是,在64位模式下是不分段的,处理器强制程序采用平坦模型,而且段的基地址都强制为0,在程序加载后,无法再用设定段地址的方式来实现程序重定位,而必须修改程序中的指令,将指令中的有效地址修改为实际的线性地址,这是一件非常麻烦的工作。

来看一个例子,如图2-32所示,这是一个汇编语言程序,在程序中声明了标号data并开辟一个双字,这个双字的初始值为0,不过初始值并不重要。

图2-32 汇编程序的例子

在程序中,指令

用来将寄存器EAX的内容保存到data这里。这条指令的前后还有其他一些指令和数据,但与我们讨论的主题无关,予以省略。

假定标号data距离程序开头的偏移量是0x3f 8,所以标号data代表的数值是0x3f 8。这条mov指令编译后,标号data将被替换为0x3f 8。

事实确实如此。在64位模式下,这条指令编译后,机器码为89 04 25 f 8 03 00 00。最后4字节是采用低端字节序存放的64位有效地址0x000003f8。

要执行编译后的程序,必须先将其加载到内存里。如图2-33所示,假定程序是从线性地址0x3000000开始加载的,那么标号data定义的双字则位于线性地址0x30003F8,上述汇编语言指令所对应的机器指令89 04 25 F8 03 00 00位于哪个线性地址并不重要,所以没有显示。

我们知道,程序在加载和执行前,需要重定位。在分段内存管理的时代,需要先确定段地址。在这里,段地址可以确定为0x3000000。那么,上述mov指令执行时,是用段地址0x3000000加上指令中的有效地址0x3F8,得到线性地址0x30003F8,并将EAX的内容写入这个地址。

但是,在64位模式下是不分段的,处理器强制程序采用平坦模型,而且段地址被视为0。既然是不分段的,我们就不能利用段地址来实现程序的浮动和重定位,而只能通过修改指令的机器码89 04 25 F8 03 00 00,将它的有效地址部分从F8 03 00 00修改为实际的有效地址F8 03 00 03。显然,在平坦模型下,程序的重定位更困难、更麻烦。

图2-33 汇编程序的数据位置

但是,请考虑一下,如果我们在指令中不使用标号data的绝对汇编地址,而是使用当前这条mov指令和标号data的相对距离,会怎样呢?

很好,因为这个相对距离在程序编译时是固定的,在程序加载后也是固定的,所以再也不用考虑程序重定位的问题,不管程序加载到什么位置,都不需要修改这条指令,处理器根据这个相对值就能找到要访问的目标位置,这就是RIP相对寻址的原理。使用RIP相对寻址方式,上述mov指令就可以写成

注意,为了在指令中使用相对距离,我们使用了关键字rel,它是相对的意思。

使用RIP相对寻址,有效地址是用RIP的当前内容加上指令中指定的距离得到的。请注意,在任何时候,RIP的内容实际上是下一条指令的地址,因为在指令执行的同时,处理器自动增加RIP的值使其指向下一条指令。

在使用RIP寻址方式的指令中,指定的距离只有32位,而且是有符号数。所以,使用RIP相对寻址方式,可以访问的地址范围是以下一条指令为中心的前后2GB。有关RIP相对寻址方式的具体使用方法,后面将结合程序代码做详细的说明和演示。

2.10.4 64位模式下的指令变化情况

在64位模式下,如果指令的目的操作数部分是通用寄存器,而且执行的是32位的操作,那么,32位的操作结果将零扩展到整个64位的目的寄存器。例如:

这条指令很简单,是将立即数0x12345678传送到寄存器EAX。此操作结束后,寄存器EAX的值是0x12345678。但是,与此同时,RAX的高32位被清零,所以64位的RAX是0x0000000012345678。

注意,以上规则不影响8位和16位操作,8位和16位操作像往常一样保留未写入的高位部分。

在64位模式下,支持使用64位偏移量的间接绝对近转移。这里有两个例子。

在第一条指令中,标号dest指示的内存位置处保存着目标位置的64位偏移量。指令执行时,从dest处取得这个偏移量,并修改指令指针RIP即可完成转移。在第二条指令中,目标位置的偏移量来自寄存器RSI。指令执行时,用RSI的内容来改变指令指针RIP即可完成转移。

在64位模式下,支持使用32位或者64位偏移量的间接绝对远转移。这里有两个例子。

在第一条指令中,标号dest1指示的内存位置处保存着目标位置的16位选择子和64位偏移量,因为内存操作数是用qword修饰的。指令执行时,如果选择子是门描述符的选择子,则64位偏移量从门描述符中取出,并用来修改指令指针RIP。如果选择子是一般的代码段选择子,则指令指针要用从dest1处取得64位偏移量来修改,完成转移。

在第二条指令中,标号dest2指示的内存位置里保存着目标位置的16位选择子和32位偏移量,因为内存操作数是用dword修饰的。指令执行时,如果选择子是门描述符的选择子,则64位偏移量从门描述符中取出,并用来修改指令指针RIP。如果选择子是一般的代码段选择子,则从dest2处取得的32位偏移量被零扩展到64位,再用来修改指令指针RIP,完成转移。

在64位模式下,不再支持直接绝对远转移。比如下面这条指令,在64位模式下是非法的。不过我们可以用间接绝对远转移来完成相同的操作。

在64位模式下,使用64位的栈指针,不受栈段描述符B位的影响(再说栈段寄存器SS已经不用),也不受指令前缀的影响。这意味着,在64位模式下,始终使用64位的栈指针寄存器RSP。

在64位模式下,栈操作的尺寸只能是16位或者64位的,比如push rax和push ax;相反需要注意的是,32位的栈操作是非法的,比如push eax。

在64位模式下,可以用push指令压入字节、字、双字和四字长度的立即数,比如push byte 0x08,不足64的立即数会在压栈前符号扩展到64位。

在64位模式下,除了FS和GS,其他段寄存器的入栈和出栈操作都是非法的,比如push cs和push es。对于FS和GS,段寄存器的内容先零扩展到64位再压入栈中。

在64位模式下,有些指令是无效的,比如daa、das、into、lds、les、pop ds、pop es、pop ss、popa、popad、push cs、push ds、push es、push ss、pusha、pushad等。 FSaHpwe7w6lJBk4kcUZoY5h7zCZDJ9dMvEMaY63U2jOKsHWcXv73nb15wOPdafYd

点击中间区域
呼出菜单
上一章
目录
下一章
×