简介
实模式下用户程序对内存的访问非常自由,没有限制。可以随便修改任何一个内存单元。如果某个内存单元属于操作系统或其他程序,将带来不可预料的后果。
全局描述符表
为了让程序在内存中自由装载,处理器将内存划分为逻辑上的段,并在指令中使用段内偏移地址,保护模式下对内存的访问依然使用段地址和偏移地址,在每个段被访问之前必须先登记。登记的信息包括段起始地址、段的界限和各种访问属性,当访问的偏移地址超出段界限时,处理器将会产生内部异常中断。
一个段的信息需要8个字节来描述,称为段描述符,每个段都需要一个描述符,把所有描述符集中存放就构成了一个描述符表。全局描述符表(Global Descriptor Table,GDT),是为整个软硬件系统服务的,在进入保护模式前必须要定义全局描述符表。
为跟踪全局描述符表,处理器内部有一个48位的寄存器,称为全局描述符表寄存器(GDTR)。该寄存器分为32位线性地址和16位界限。32位处理器有32根地址线,可以访问地址范围0x00000000~0xffffffff,共2的32次方字节,即4G内存。GDTR的32位线性地址保存的是全局描述符表在内存中的起始地址,16位边界保存的是全局描述符表的界限,其数值上等于表的大小减1。
GDT的界限是16位,所以该表最大2的16次方字节,一个描述符占8字节,故最多可以定义8192(2的13次方)个段。
实模式下只能访问1MB的内存,进入保护模式后处理器将按新的内存访问模式工作,所以在进入保护模式之前必须先定义GDT,在进入保护模式后可以换个位置重新定义GDT。
段描述符
段描述符中指定了32位段起始地址,以及20位段界限。32位保护模式下如果未开启分页功能,该线性地址就是物理地址。
20位的段界限用来限制段的扩展范围,访问内存的方法是用段基址加上偏移量,所以对于向上扩展的段,偏移量是从0开始递增,对于向下扩展的段(如栈段),段界限决定了偏移量的最小值。
G 位是粒度(Granularity)位,用于解释段界限的含义。当G为是0时,段界限以字节为单位,此时段的扩展范围是1B到1MB,如果该位是1,段界限以4KB为单位,段的扩展范围是4KB到4G
S 位指定描述符的类型(Descriptor Type),0表示一个系统段;1表示一个代码段或数据段(栈也是特殊的数据段)。
DPL 是描述符的特区级(Descriptor Privilege Level,DPL)。这2位用于指定段的特权级。共有4种特权级,分别为0、1、2、3,其中0是最高特权级,3是最低特权级。刚进入保护模式时执行的代码具有最高特权级0,这些代码通常是操作系统代码,操作系统加载一个程序时通常会指定一个稍低的特权级。不同的特权级程序时相互隔离的(如果这里的数值为2,只有特权级为0、1、2的程序才能访问)。有些处理器特权指令只有0特权级别的程序才能执行。
P 是段存在位(Segment Present)。用于指示所描述的段是否存在,一般来说,描述符描述的段都位于内存中,但是当内存紧张时,用可能只建立了描述符,对应的内存空间并不存在,这时应当把描述符P为清0,表示段不存在,内存空间紧张的情况下,会把很少用到的段换出到硬盘中,腾出空间给当前急需内存的程序使用,这时同样要把描述符P位清0。当再次执行时再装入内存,然后将P位置1。
P 位是处理器负责检查的。每当描述符方位内存中的段时,如果P位是0,处理器会产生一个异常中断,通常该中断处理过程由操作系统提供,该处理过程负责把该段从硬盘换回内存,并将P位置1。这是多用户、多任务系统中常用的虚拟内存调度策略。
D/B 位是“默认的操作数大小”或者默认的栈指针大小,又或者上部边界标志。设立该标志位,主要是为了能在32位处理器上兼容运行16位保护模式的程序。该标志位对不同的段有不同的效果。对于代码段,此位称为“D” 位,用于指示指令中默认的偏移地址和操作数尺寸。D=0表示指令中的偏移地址或操作数是16位的;D=1指示32位的偏移地址或操作数。
例如:代码段描述符的D位是0,那么处理器在这个段上执行时将使用16位的指令指针寄存器IP来取指令,否则使用32位的EIP;对于栈段该位叫做“B” 位,用于在进行隐式的栈操作时,是使用SP寄存器还是ESP 寄存器。隐式的栈操作指令包括push、pop、call等,如果该位是0,在使用时使用SP寄存器,否则使用ESP寄存器。同时B位的值也决定栈的上部边界,如果B=0,那么栈的上部边界(SP寄存器最大值)为0xffff;如果B=1,那么栈的上部边界(ESP的最大值)为0xffffffff。
TYPE 字段共4位,用于指示描述符的子类型,对于数据段这四位分别是:X、E、W、A;对于代码段来说是:X、C、R、A位。
X 表示是否可以执行。数据段总是不可执行的,X=0;代码段总是可以执行的,X=1。
对于数据段来说,E 指示段的扩展方向。E=0是向上扩展的,也就是向高地址方向扩展;E=1 是向下扩展的,也就是向低地址方向扩展,通常是栈段。W位指示读写属性,W=0 的段不允许写入,否则会引发处理器异常中断;W=1的段是可以写入的。
对于代码段来说,C位指示的段是否为特权级依从的。C=0表示非依从的代码段,这样的代码段可以从与它特权级相同的代码段调用;C=1表示运行从低特权级的程序转移到该段执行。 R位指示代码段是否允许读出。代码段总是可以执行的,为防止程序被破坏,它是不能写入的。R=0表示不能读出,如果去读一个R=0的代码段,会引发处理器异常中断;如果R=1,则代码段是可以读出的,即可以把这个段当成ROM一样使用。R属性并非是用来限制处理器,而是用来限制程序和指令的行为。
数据段和代码段的A位是已访问(Accessed)位,用于指示它指向的段最近是否被访问过。在描述符创建的时候,应该清0,每当该段被访问,处理器自动将该位置1。清0是由操作系统负责的,定期监视该位的状态,可以在内存紧张时把不常用的段写回到硬盘上,从而实现虚拟内存管理。
AVL 是软件可以使用的位(Available),通常由操作系统来用,处理器并不使用它。
进入保护模式
CR0是处理器内部的控制寄存器(Control Register,CR),CR0是32位的寄存器,包含一系列用于控制处理器操作模式和运行状态的标志位,它的位0 是保护模式允许位(Protection Enable,PE),是开启保护模式的大门,如果把该位置1,则处理器进入保护模式,按保护模式的规则运行。
保护模式下的中断机制和实模式不同,因此原有的中断向量表不在适用,保护模式下,BIOS中断也不能使用,因为他们是实模式下的代码。在重新设置保护欧式下的中断之前,必须关中断。
32位处理器上,每个段寄存器还包括一个不可见的部分,称为描述符高速缓存器,用来存放段的线性地址、段界限和段属性。实模式下的6个段寄存器cs、ds、es、fs、gs、ss,在保护模式下叫做段选择子。传送到选择子的内容不同实模式下的逻辑段地址,而是段描述符在描述符表中的索引号。
段选择子由3部分组成,第一部分是描述符的索引号,用来在描述符表中选择一个段描述符。TI 是描述符表指示器,TI=0 表示描述符在GDT 中;TI=1 描述符在LDT中。RPL是请求特权级,表示当前选择子的那个程序的特权级别。
故描述符索引号需要乘以8!
当处理器进入保护模式后,寄存器中的内容依然是实模式下的相关内容,在执行某些指令是将出问题,因此需要尽快刷新相关寄存器内容。
清空流水线
处理器内部都采用流水线,进入保护模式前由许多指令已经进入流水线,因为处理器工作在实模式下,所以他们都是按16位操作数和16位地址长度译码,即使有32位保护模式代码。导致指令的执行结果可能不正确,所以必须清空流水线。使用元转移指令jmp 或者远过程调用指令call,会重新加载段选择子,并刷新描述符高速缓存器中的内容。
程序代码
boot.s
org 0x7c00
[bits 16]
xor ax,ax
mov ss,ax
mov sp,0x7c00
;计算GDT所在的逻辑段地址
mov dx,[gdt_base+2]
mov ax,[gdt_base]
mov bx,16
div bx
mov ds,ax ;DS指向该段以进行操作
mov bx,dx ;段内起始偏移地址
;0索引位置的描述符必须是空描述符,这是处理器的要求
mov dword [bx],0x00
mov dword [bx+4],0x00
;1号索引位置描述符 --> 32位代码
;mov dword [bx+0x08],0x7c0001ff
;mov dword [bx+0x08+4],0x00409800
mov eax,code32_end-code32-1
mov [bx+0x08],ax
shr eax,16
or al,0x40
and al,0xff
mov [bx+0x08+6],al
mov byte [bx+0x08+5],0x98
mov eax,code32
mov [bx+0x08+2],ax
shr eax,16
mov [bx+0x08+4],al
mov [bx+0x08+7],ah
;2号索引位置描述符 --> 堆栈
mov dword [bx+0x10],0x00007a00
mov dword [bx+0x10+4],0x00409600
;3号索引位置描述符 --> 数据段描述符
mov dword [bx+0x18],0x8000ffff
mov dword [bx+0x18+4],0x0040920b
;描述符寄存器GDTR界限
mov word [cs:gdt_size],31
lgdt [cs:gdt_size]
in al,0x92 ;南桥芯片内的端口
or al,0x02
out 0x92,al
cli ;保护模式下中断机制还未建立,故关闭中断
mov eax,cr0
or eax,0x01
mov cr0,eax ;设置PE位
;进入保护模式
;jmp dword 8:code32-0x7c00
jmp dword 8:0
[bits 32]
code32:
;加载堆栈选择子
mov ax,0x10
mov ss,ax
mov esp,0x7c00
;加载数据段选择子
mov ax,0x18
mov ds,ax
;以下在屏幕上显示"Protect mode OK."
mov byte [0x00],'P'
mov byte [0x02],'r'
mov byte [0x04],'o'
mov byte [0x06],'t'
mov byte [0x08],'e'
mov byte [0x0a],'c'
mov byte [0x0c],'t'
mov byte [0x0e],' '
mov byte [0x10],'m'
mov byte [0x12],'o'
mov byte [0x14],'d'
mov byte [0x16],'e'
mov byte [0x18],' '
mov byte [0x1a],'O'
mov byte [0x1c],'K'
;32 栈操作检测
mov ebp,esp
push byte 0x11
pop eax
cmp ebp,esp
jne final
mov byte [0x1e],'.'
final:
call io_hlt
jmp $
io_hlt:
hlt
ret
code32_end:
gdt_size:
dw 0
gdt_base:
dd 0x7e00
times 510 - ($-$$) db 0
db 0x55,0xaa
虚拟机加floppy.img 虚拟软盘文件效果如下:
保护模式下使用C语言
进入保护模式后内存访问限制将不再是16位模式下的:bx、bp、si、di、sp,可以使用如eax、ecx等通用寄存器。进入32位后很多功能完全可以使用C语言实现快速与效率的平衡,尤其是C语言的指针对内存访问的使用。
C语言与汇编的结合规则:
1.C语言函数编译成汇编后就是汇编的过程调用
2.函数参数通过栈传递,获取参数 esp+4*参数索引,其中参数索引从左到右从0开始
3.函数中使用eax、ecx、edx 可以不用先保存
4.函数的返回值通过eax 返回