指令的格式及操作尺寸

32位处理器的指令操作尺寸

在32位处理器中,源操作数既可以是32位的,也可以是16位的,还可以是8位的,目的操作数同理,并且在实模式下,也可以用32位寄存器来访问内存,那么是不是说实模式下也可以访问全部4G字节内存呢?通常来讲,是不行的,因为32位处理器的描述符高速缓存中,除了有线性基地址,还有段界限,在实模式下计算机会将其预置为0xFFFF,也就是64KB,如果超过这个界限也会被处理器阻止,这样以保证程序在实模式下和8086的表现相同。比如下面这样:

mov cx, 0x2000
mov ds, cx    ;将逻辑段地址0x2000加载到ds中

mov eax, 0xFFFF
mov bl, [eax]    ;从偏移量为0xFFFF的地方取一个字节
mov bx, [eax]    ;从偏移量为0xFFFF的地方取一个字

如果你在bochs虚拟机中调试上面那段代码,执行倒数第二行指令是没问题的,因为它只在段界限处取一个字节,等于但并未超出界限,而再执行下面一条指令时,虚拟机会报错,因为下面取了两个字节,超出了段界限。
但刚刚之所以说通常,是因为某些情况下,它又是可以访问全部4G字节内存的,32位处理器还有一种Unreal模式,它的描述如下:
Unreal Mode
在这种模式下,处理器以实模式运行,但段大小不再限于64KB。

x86的指令格式

操作码和立即数部分

x86有些指令格式比较简单,只包含操作码和立即数,比如:

mov al, 3
mov cx, 3
mov edx, 3

上述三条指令转换成机器码应该如何表示?我们来参考一下《x86架构软件开发手册卷二》。
在MOV指令参考中,我们找到符合第一条指令的描述:
mov r8, imm8
其中第二列MOV r8, imm8,imm8表示一个8位立即数,r8表示8位寄存器,我们看到操作码部分是B0+rb,其中rb代表一个8位寄存器的编号,如何知道这个编号呢?参考Table2-1的表头:
table2-1
寄存器编号就是倒数第二行,我们找到AL对应的编号是0,B0+0还是B0,所以mov al, 3的机器码是B003

同理,我们再找到符合第二条指令的描述:
B8+rw
第二列,imm16表示16位立即数,r16表示16位寄存器,操作码部分是B8+rw,rw同样是寄存器编号,根据Table2-1的表头,我们得知cx的编号是1,B8+1=B9,再加上立即数0003得到机器码B90300

Intel处理器采用小端序,所以立即数部分是0300,而非0003

再找到第三条指令的描述:
B8+rd
imm32是32位立即数,r32是32位寄存器,操作码部分B8+rd,rd还是寄存器编号,我们找到edx编号为2,B8+2=BA,再加上立即数部分得到BA03000000

ModR/M和偏移量部分

有些处理器指令比较复杂,需要在寄存器与寄存器之间,或者寄存器与内存之间进行操作,此时在操作码后面需要一个寻址方式部分,称之为ModR/M,如图:

ModR/M
ModR/M占一个字节长,其中mod和r/m部分需要组合使用。
e.g. 有如下三条指令:

mov al, cl
mov ax, [bx]
mov cx, [bx+si]

我们分别求三条指令的机器码,先看第一条指令,在《x86架构软件开发手册卷二》中我们找到最符合它的描述:

MOV r/m8,r8
88 /r是它的操作码,其中/r表示指令中的纯寄存器,也就是后面的r8,而前面的 / 表示操作数的扩展,需要将寄存器的编号填充到ModR/M的中间reg/opcode部分,根据前面的经验,cl的编号是001。
如何找到该指令的mod和r/m部分?我们还看Table2-1,找到其中寄存器的部分:


可以看出,mod部分是11,而r/m部分是al所对应的000,所以整个ModR/M部分是11001000,转换为十六进制为C8,再加上前面的操作码部分,整条指令的机器码为88C8

再看第二条指令,找到与之最符合的描述:

/r指代r16,同时/表示需要将寄存器编号填充到ModR/M中间部分,第二条指令的纯寄存器是ax,编号000,再看mod和r/m部分:

[bx]所对应的mod、r/m分别是00、111,所以ModR/M是00000111,对应十六进制07,再加上操作码部分就是8B07

接下来第三条指令的描述与上一条一样,但是/r变成了cx,编号001,mod和r/m部分也变成:


也就是00、000,所以整个ModR/M是00001000,对应十六进制08,整条指令机器码为8B08

为了方便理解,我们做一个表格:

汇编指令操作码modregr/mModR/M机器码
mov al, cl8811001000C888C8
mov ax, [bx]8B00000111078B07
mov cx, [bx+si]8B00001000088B08

根据指令的复杂程度,在ModR/M后面还可以有偏移量部分和立即数部分,比如:

汇编指令操作码modregr/mModR/M偏移量立即数机器码
mov ax, [bx+3]8B0100011147038B4703
mov word [bx+si+3], 0x55AAC701000000400355AAC74003AA55

其中第二条指令是将一个立即数传送到一个内存地址中,描述如下:


操作码中的/0表示将数字0扩展到ModR/M的中间部分,所以第二条指令的reg一栏填000,同时Intel采用小端序,55AA在指令中表示为AA55。

SIB部分

32位处理器有一种特有的寻址方式:基址寄存器 + 索引寄存器 * 倍率 + 偏移量,其中基址寄存器是全部32位寄存器,索引寄存器是除ESP以外的32位寄存器,倍率可以是(1,2,4,8)中的一个,偏移量是8位或者32位。
为了支持这种寻址方式,需要在ModR/M后面再加一个SIB部分,即Scale(倍率),Index(索引寄存器编号),Base(基址寄存器编号),SIB也占一个字节:

SIB
来看一个指令:

add edx, [eax+ebx*2]

在《x86架构软件开发手册卷二》找到符合该条指令的描述:


操作码为03,/r说明要将寄存器编号填充到ModR/M的中间部分,edx的编号为010,那么Mod和R/M部分是哪个呢?


根据下面注释的描述:

[–][–]意味着ModR/M后面跟随SIB字节,所以,Mod为00,R/M为100,SIB部分又该如何确定呢?
我们看表2-3:
在这里插入图片描述
符合该指令的是[EBX*2],SS是01,Index是011,而基址寄存器EAX的编号是000:

确定了以上内容,我们再来画个表格:

所以最终的机器码是031458

再来看一个例子:

add word [eax+ebx*8+0x3c00], 0x55AA

找到符合它的描述:


操作码为81,/0表示ModR/M中间部分是000,Mod和R/M部分是10、100:


再来找SIB部分:

可以看出SS部分是11,Index部分是011,Base是基址寄存器eax的编号000,确定了这些,我们再补充一个表格:

指令前缀部分

数据/地址尺寸反转

除了ModR/M和SIB,根据需要,x86指令格式还可以添加一个指令前缀,比如段超越前缀,数据尺寸反转前缀,地址尺寸反转前缀等,就拿尺寸反转前缀来讲,为了方便说明问题,我们来看三组指令及其对应的机器码:

汇编指令机器码数据/地址尺寸
mov al, dl88 D08位数据
mov ax, dx89 D016位数据
mov eax, edx66 89 D032位数据
mov [bx+di], dh88 318位数据,16位地址
mov [bx+di], si89 3116位数据,16位地址
mov [bx+di], esi66 89 3132位数据,16位地址
mov [ecx], esi66 67 89 3132位数据,32位地址
mov [ecx], si67 89 3116位数据,32位地址
movsbA48位数据
movswA516位数据
movsd66 A532位数据

默认16位操作尺寸

观察表格中第二组指令,你会发现机器码的ModR/M部分都是相同的31,根据Intel手册,表2-1和2-2 ModR/m是31的分别是[BX+DI]和[ECX],分别对应了16位寻址方式和32位寻址方式。
尽管指令的形式和ModR/M都一样,但毕竟是不同的指令,终归得产生不同的机器码,在16位处理器时代,也就是8086和80286时代,Intel为不同的数据尺寸设计了不同的操作码,比如mov [bx+di], dh的操作码是88,mov [bx+di], si的操作码是89。
而到了32位处理器时代,数据尺寸和地址尺寸都得到了扩展,为了不增加指令复杂性,Intel决定,如果ModR/M相同,操作尺寸不同,使用同一个操作码,利用指令前缀来区分尺寸,在上表中看到的66、67就是指令前缀。而且通过观察很容易看出,在默认操作尺寸是16位的情况下,如果操作的数据是32位,则会添加66前缀,如果有效地址是32位,则会添加67前缀,如果数据和地址都是32位,66和67都会被添加。

相反,如果默认操作尺寸是32位的呢?

汇编指令机器码数据/地址尺寸
mov al, dl88 D08位数据
mov ax, dx66 89 D016位数据
mov eax, edx89 D032位数据
mov [bx+di], dh67 88 318位数据,16位地址
mov [bx+di], si66 67 89 3116位数据,16位地址
mov [bx+di], esi67 89 3132位数据,16位地址
mov [ecx], esi89 3132位数据,32位地址
mov [ecx], si66 89 3116位数据,32位地址
movsbA48位数据
movsw66 A516位数据
movsdA532位数据

可以看出,32位尺寸下指令前缀刚好和16位相反,如果使用了16位数据则会添加66前缀,如果使用了16位地址则会使用67前缀。

描述符中的D位

处理器在执行指令时,如何判断当前代码该用16位还是32位呢?在段存储器描述符中,有一个D/B位,如果S=1,X=1,表示该段是代码段,此时D/B是D位。如果D=0,处理器使用16位尺寸处理数据及寻址,如果D=1,处理器使用32位尺寸处理数据和寻址。

到此为止,x86的所有指令格式就全部涵盖了,这里有一张Intel手册中的指令格式截图:

切换32位操作尺寸

虽然在保护模式中,使用16位操作尺寸也可以,但32位处理器还是使用32位尺寸更加得心应手,在代码中,我们可以添加一行bits指令:

[bits 32]

只要有bits 32一条声明,之后所有的指令都会按照32位尺寸来进行编译,但是通常在实模式下执行16位代码时,声明为32bits的很多指令已经进入了流水线,显然按照16位尺寸去看待提前进入流水线的32位指令,是错误的,所以这时我们需要清空流水线。
在x86中,只要执行了jmp指令,必然会清空流水线,不过,保护模式下的jmp和实模式下的jmp有些不同,实模式下jmp指令是jmp 逻辑段地址:偏移地址,而保护模式需要你指定一个段选择子,像这样jmp 描述符选择子:段内偏移量,一旦执行了jmp指令,处理器会从GDT中取出指定的描述符并缓存到cs的描述符高速缓存器中,然后用缓存中的线性基地址+段内偏移地址计算出目标指令位置,并跳转到该处执行,此时流水线中错误的指令已经清空。
e.g.

         jmp dword 0000000000001_0_00B:flush    ;16位的描述符选择子:32位偏移
                                                ;清流水线并串行化处理器 
         [bits 32] 

    flush:
         mov cx,0000000000010_0_00B         ;加载数据段选择子(0x10)
         mov ds,cx

话说mov ds, eax

在32位处理器中,往段寄存器中传送数据可以这样写:

mov ds, ax
mov ds, eax
mov ds, edx

mov cs, eax
mov ss, eax
mov gs, eax
mov fs, eax

上述指令中,第一条没问题,但是后面的所有指令你会感觉很奇怪,怎么能用32位通用寄存器向16位段寄存器中传输数据呢?然而这些指令编译都可以通过,在Intel的手册第二卷中,有这么一段描述:



翻译过来大概意思是:

在32位操作尺寸下,向段寄存器和通用寄存器之间传送数据,IA-32处理器不要求必须使用0x66尺寸反转前缀,但是大部分编译器会遵照标准格式来插入这个前缀,(例如mov ds, ax)。处理器当然能够正确执行这条指令,但是将会额外消耗一个时钟周期。在大部分汇编器中,使用mov ds, eax可以避免不必要的0x66前缀。当处理器用32位通用寄存器作为源操作数或者目的操作数时,仅低16位有效。如果寄存器是目的操作数,高位2字节结果依赖处理器实现,例如奔腾4,Intel至强,P6家用处理器,高位2字节填充0;其它32位处理器没有定义。

也就是说,之所以出现这种奇怪的写法,是为了避免不必要的0x66数据尺寸反转前缀,不过你要是使用NASM编译器的话,是没有这种问题的,NASM无论哪种写法,都不会添加0x66前缀。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值