第13章 操作数和有效地址的尺寸
16位操作尺寸的指令和32位操作尺寸的指令可能具有相同的机器码,为了区分这两种不同操作尺寸的指令,32位处理器引入了默认操作尺寸的概念。
代码清单13-1
该章的代码实现的功能和上章一样,都是在页面打印:Protect Mode OK.
只是在进入保护模式后,采用jmp隐式的更新代码段寄存器CS,然后采用32位的操作数尺寸编译后续要执行的源码。
INTEL 80286处理器的16位保护模式
80286也是16位处理器,有24根地址线,可以访问16MB的内存。为了访问1MB以上的内存,同时为了支持多用户和多任务的系统,并加强对内存和程序的保护,80286引入了保护模式。
80286的段寄存器做了扩展,包括一个段选择器,以及一个描述符高速缓存器。在80286的保护模式下,传送到段寄存器里的值段选择子,用来在描述符表中选择一个段描述符。段描述符格式如下:
指令的操作尺寸
指令的操作尺寸(Operation Size):是指令的数据长度及指令在访问内存时的有效地址长度。
16位操作尺寸
指令的操作尺寸是16位的,指令操作数长度是8位或者16位的,有效地址长度是16位的。
例如:
mov al,cl ;8位
mov bx,[0x2e] ;16位
add dx,[bx] ;16位
push dx ;16位
sub dx,[bx+si+0x03] ;16位
32位操作尺寸
指令的操作尺寸是32位的,指令操作数长度是8位或者32位的,可以兼容16位操作尺寸的指令。
例如:
mov al,[0x2e] ;al是8位的,内存有效地址0x0000002e
mov edx,eax ;32位
add al,[eax] ;al是8位的,内存地址32位的,由eax提供
and edx,[eax+esi*8+3] ;32位
push edx ;32位
默认操作尺寸
简化指令的编码:一种可行的方法就是让16位操作尺寸的指令和32位操作尺寸的指令使用相同的编码。
例如:
;以下两条指令的机器码:89 c8
mov ax,cx ;16位指令
mov eax,ecx ;32位指令
指令格式:机器指令分成5大部分:前缀、操作码、寻址方式和操作数类型、立即数、位移。
- 前缀:重复前缀(REP/REPE/REPNE);段超越前缀(ES、DS、CS);总线封锁前缀(LOCK)等。
- 操作码:mov、add、inc等。
- 寻址方式和操作数类型:可选的,1~2字节。例如:eax,[bx+si+0x02]等。
- 立即数:mov eax,0x02 中的0x02就是立即数。
- 位移:8位或32位的位移,例如 mov ecx,[eax+ebx*8+0x02] 中的0x02。
例如:汇编语言指令
mov dx,[bx+si+0x02]
;编码后机器码:8B 50 02
;操作码0x8B;
;之后1字节的寻址方式和操作数类型部分;
; 位7和位6的值是01,表示使用了基地址变址的寻址方式,而且带有8位偏移量;
; 位5~位3的值是010,指示目的操作数为寄存器DX;
; 位2~位0的值是000,表示寻址方式为“BX+SI+8位位移”。
;之后是1字节的位移0x02。
画图表示如下:
再看汇编语言指令:
mov edx,[eax+0x02]
;编码后机器码也是:8B 50 02
;除了机器码第2个字节:位5~位3的值是010,指示目的操作数为寄存器EDX;
;其余都是一样的。
CS描述符高速缓存器中的D位:机器指令8B 50 02到底是什么?16位机器上没有问题,32位机器上取决于CS描述符高速缓存器中的D位,它用来指定处理器当前默认的操作尺寸(Default Operation Size)。
- CS描述符高速缓存器中的D位 = 0,16位操作尺寸。
- CS描述符高速缓存器中的D位 = 1,32位操作尺寸。
CS描述符高速缓存器中的D位变化情况:
- 处理器刚加电时为0;
- 刚进入保护模式未刷新CS时也是0,此时处理器工作在16位保护模式下。
- 后续可以刷新CS,可以设置为1,也可以为0。1就是32位保护模式,0就是16位保护模式。
操作尺寸反转前缀
如果当前默认的操作尺寸是16位的,想执行32位的,或者,如果当前默认的操作尺寸是32位的,想执行16位的。只需要添加反转操作数尺寸的前缀0x66即可。
我把说中说明的整理了一个表格,更容易看一些:
简单理解就是加了 0x66 反转前缀的指令,在16位下会被看成32位的执行,在32位下会被当做16位执行。
反转有效地址尺寸:前缀0x67则用来反转有效地址尺寸。
0x67就意味着将 [eax+0x02] 和 [bx+si+0x02] 进行反转。
编译时的操作尺寸
Bits指令:Bits是编译器提供的伪指令,用于通知编译器编译后面的指令时,默认操作尺寸,它的用法是在关键字 bits 的后面跟数字 16、32、64。
例如:
bits 16 ;16位模式,加方刮号也可以: [bits 16]
mov cx,dx ;89 D1
mov eax,ebx ;66 89 D8
bits 32 ;32位模式
mov cx,dx ;66 89 D1
mov eax,ebx ;89 D8
最后,如果没有指定指令编译时的处理器默认操作尺寸,则默认是“bits 16”的。
清空流水线并串行化处理器
安装代码段描述符:将代码段描述符指向主引导程序所在的区域。
;创建#2描述符,保护模式下的代码段描述符
mov dword [bx+0x10],0x7c0001ff
mov dword [bx+0x14],0x00409800
- 线性基地址为0x00007C00;
- 段界限为0x001EE,粒度为字节(G=0),该段的长度为512字节;
- 属于存储器的段(s=1);
- 默认的操作尺寸是32位的(D=1);
- 该段目前位于内存中(P=1);
- 段的特权级为0(DPL=00);
- 这是一个只能执行的代码段(TYPE=1000)
加载全局描述符表寄存器GDTR:长度较上章加8,因为多了一个代码段描述符。
;初始化描述符表寄存器GDTR
mov word [cs: gdt_size+0x7c00],23 ;描述符表的界限(总字节数减一)
lgdt [cs: gdt_size+0x7c00]
进行远转移:进入保护模式,进行一个远转移。
;以下进入保护模式... ...
jmp 0000000000010_0_00B:flush ;相当于jmp 0x0010:flush
- 描述符索引:2,用来选择2号描述符。
- 表指示器位TI:0,指向GDT。
- 请求特权级RPL:0,最高特权级。
这条远转移指令执行时,处理器加载段选择器CS,从GDT中取出相应的描述符加载到CS描述符高速缓存器,此时描述符高速缓存里的内容: - 基地址为0x00007C00;
- 段界限为0x1FF;
- 长度为0x200;
- 默认操作尺寸为32位。
flush标号表示在相对于段起始处的一段距离,用作段内偏移量(有效地址)。
使用伪指令bits 32:使用jmp后,CS描述符高速缓存器的D位为1,默认操作尺寸为32位,所以后面的指令也需要按照32位操作尺寸编译。
;jmp后增加微指令
bits 32
清空流水线:执行jmp指令时,处理器会清空流水线,重新按指令的自然顺序执行。
显示字符:把数据段选择子0x0008加载到数据段DS选择器,而后通过默认的段选择器可以更加方便显示字符。
CS不能用mov改变:在保护模式下,不允许使用mov指令改变段寄存器CS的内容,比如:mov cs,ax。
有效地址尺寸和内存访问
段界限:每当一条指令访问内存时,处理器会用指令中的有效地址和这个界限值进行比较,如果超出这个界限值,就会被处理器阻止。
例如:
mov ax,0x2000
mov ds,ax ; 数据段寄存器的内容是0x2000
; 在执行了段寄存器DS的传送指令后,
; 用调试命令sreg显示各个段寄存器的内容,可以发现,
; 在实模式下,段寄存器DS的内容是0x2000,
; 描述符高速缓存器里的基地址是0x20000,段界限是默认值0x0000ffff。
mov eax,0xffff ;
mov dl,[eax] ; 只访问1个字节,正常
mov edx,[eax] ; 访问4个字节,越界报错。
cli
hlt
times 510-($-$$) db 0
db 0x55,0xaa
- 在实模式下,处理器将段界限预置成0xFFFF;
- 在保护模式下,段描述符高速缓存器里的段界限不是处理器预置的,而是来自段描述符。
通过Bochs查看ds信息:
实模式下访问全部4GB内存的方法:先从实模式进入保护模式,为段寄存器(比如DS)选择一个基地址部分高于1MB的段描述符,然后从保护模式退回到实模式,就可以利用段寄存器中的残留内容来访问超过1MB以上的内存。
一般指令在32位操作尺寸下的扩展
由于32位的处理器都拥有32位的寄存器和算术逻辑部件,而且同内存芯片之间的数据通路至少是32位的,因此,所有以寄存器或者内存单元为操作数的指令都被扩充,以适应32位的算术逻辑操作。
加法指令add:在32位处理器上,除了允许8位或者16位的操作数,32位的操作数现在也是可用的:
add al,bl
add ax,bx
add eax,ebx ;32位操作数
add dword [ecx],0x0000005f ;32位立即数
单操作数指令:除了双操作数指令,单操作数指令也同样允许32位操作数,比如:
inc al
inc dword [0x2000]
dec dword [eax*2] ;32位操作数
逻辑移动指令:
shl eax,1 ;32位操作数
shl eax,9 ;32位操作数
; 逻辑移动指令的源操作数如果是
寄存器的话,则依然必须使用CL。
; 32位处理器在实际执行时,要先将源操作数(在寄存器CL内)同0x1F做逻辑与。
; 也就是说,仅保留源操作数的低5位,因此,实际移动的次数最大为31。
shl dword [eax*2+0x08],cl
为什么最大的移动次数是31位?
因为 0x1F 二进制表示为 0001_1111B,
cl & 0001_1111B 只会保留最后5位。
最大就只能是:0010_0000B - 1B = 2^5-1 = 31
循环次数:
- 默认操作尺寸是16位的,loop指令的循环次数在寄存器CX中;
- 默认操作尺寸是32位的,则使用的是寄存器ECX;
乘法指令mul:
;16位处理器上
mul r/m8 ;AX <- AL*r/m8,
mul r/m16 ;DX:AX <- AX*r/m16,高位存储在DX,低位存储在AX
;32位处理器上
mul r/m32 ;EDX:EAX <- EAX*r/m32,高位存储在EDX,低位存储在EAX
有符号数乘法指令imul与此相同。
除法指令div:无符号数和有符号数除法也做了32位扩展。
div r/m32
idiv r/m32
- 被除数是64位的,高32位在寄存器EDX;低32位在寄存器EAX。
- 除数是32位的,位于32位的寄存器,或者存放有32位实际操作数的内存地址。
- 指令执行后,32位的商在寄存器EAX,32位的余数在寄存器EDX。
push和pop:允许压入双字(32位)操作数。
push imm8 ;立即压入8位操作数,操作码是6A
push imm16 ;立即压入16位操作数,操作码是68
push imm32 ;立即压入32位操作数,操作码是68
压入字节:用byte修饰。
push byte 0x55 ;16位和32位机器码都是:6A 55
; 16位:压入0x0055,(SP)-2
; 32位:压入0x00000055,(ESP)-4
压入字:用word修饰。
push word 0xfffb ;被看成有符号数。
; 16位:压入0xfffb
; 32位:压入0xfffffffb
压入双字:用dword修饰。
push dword 0xfb ;16位和32位均压入0x000000FB
;栈指针(SP或ESP)都先减去4
操作数位于通用寄存器或内存单元:对于实际操作数位于通用寄存器,或者位于内存单元的情况,只能压入字或者双字。
push r/m16 ;16位寄存器或者内存地址
push r/m32 ;32位寄存器或者内存地址
例如:
;寄存器,则可以使用16位或者32位的通用寄存器。
push ax ;16位寄存器,字
push edx ;32位寄存器,双字
;内存,关键词word或者dword修饰。
push word [0x2000] ;字
push dword [ecx+esi*2+0x02] ;双字
无论是在内存还是寄存器中,
- 在16位模式下,如果压入的是字操作数,那么先将SP的内容减去2;如果压入的是双字,应当先将SP的内容减去4。
- 在32位模式下,如果压入的是字操作数,那么先将ESP的内容减去2;如果压入的是双字,应当先将ESP的内容减去4。
压入段寄存器:压入段寄存器的操作比较特殊,以下是压入段寄存器的push指令格式。
push cs ;机器指令位0E
push ds ;机器指令位1E
push es ;机器指令位06
push fs ;机器指令位0F A0
push gs ;机器指令位0F A8
push ss ;机器指令位16
- 在16位模式下,先将SP的内容减去2,然后直接压入段寄存器的内容;
- 在32位模式下,要先将段寄存器的内容用零扩展到32位,即高16位为全零,然后,将ESP的内容减去4,再压入扩展后的32位值。
本章习题
第1题:
第2题:
如果是按照32位操作数的写法,按照32位编译,那么 mov ebx,16 会多出16位高位部分,按照32位执行就没有问题。
现在是16位操作数尺寸的写法,按照16位进行编译,按照32位执行,所以当执行到 BB 这条指令的时候会把指令当成是32位的,会把BB指令后面32位的数字当做立即数,即1000F7E3,就是将后面F7E3指令当成是立即数的部分了。
书上没有给出答案,我尝试在文心一言问了一下,意思应该是一样的,关键句:隐式的扩展到32位操作。