★PART1:进入保护模式
1. 全局描述符表(Global Descriptor Table,GDT)
32位保护模式下,如果要使用一个段,必须先登记,登记的信息包括段的起始地址,段的界限和各种访问属性,如果偏移地址超过了段的界限,就会引发异常中断。和一个段有关的信息需要8个字节来描述,这被称为段的描述符(Segement Descriptor),每个段都需要一个描述符,为了存放描述符,需要在内存中开辟一段空间。这些描述符集中存放,构成了一个描述符表。
为了跟踪全局描述符表,处理器内部有一个48位的寄存器,称为全局描述符寄存器(GDTR)。这个寄存器分为两个部分,前16位为全局描述表的边界,后32位为全局描述表的线性基地址。GDT的界限是16位的,所以GDT的最大是216字节,又因为一个描述符是8个字节的,所以最多可以定义8192个描述符。
因为进入保护模式之前,是实模式,实模式只能访问最多1MB的内存,所以GDT通常都是定义在1MB以下的内存范围中,允许进入保护模式之后换个地方定义GDT。
因为在进入保护模式之前处理器初始化为实模式,同样的,主引导程序是在0x0000:0x7c00这个地方加载的,而主引导程序最大也就512字节,所以我们把GDT放在主引导程序之后,也就是0x7e00后面,GDT的界限可以到0x17DFF(64KB最大)。
栈段寄存器SS被初始化为0x0000,栈是向下拓展的,我们可以把SP段定义到0x7c00处,然后向下拓展(要注意大小不能太大,因为BIOS数据区和中断向量表都在下面)。
描述符不是由用户程序自己创建的,都是由操作系统创建的,如果用户程序对段的访问超过了操作系统所规定的范围,或者访问了一个不属于它的段,那么这些操作都会被处理器阻止。
2. 32位保护模式下GDT每个位置的定义
每个描述符在GDT占8个字节,每个位置及其定义见下图:
在32位保护模式下,如果未开始页功能,那么段地址就是物理地址,注意描述符段保存器的段界限和基地址是不连续的(其实是为了兼容80286)。另外需要注意的是,段基地址最好是16字节对齐,虽然80386原则上是可以选取任何位置作为段基地址的,但是如果不进行16字节对齐,那么将会对性能产生很大的影响。因为32位CPU的总线是32位的,所以CPU一次会从内存中读取32个字节的数据(而且是从地址4倍数的字节开始读,比如0x00,0x04,0x08….等4倍数地址),然后进行剔除数据不要的部分,然后进行拼合,位移到寄存器里,这个过程是很花费时间的,但这是为了保证传输到寄存器里的内容总是正确的。读取一个字的时候总是可以正确读到,但是读取两个字的时候就可能跨越4倍数的界限,然后CPU必须读取48位数(花费两个时钟周期),才能正确读取数据。(注意CPU的16字节对齐和编程时候的align16是不一样的,编程时的align16是直接在内存上组织数据,和CPU读取数据无关。)
GDT每个位置的意义如下:
G位(Granularity,粒度):
这个位用于解释界限的含义,当G=0,段界限是以字节为单位的,这个时候,段的拓展范围是1B-1MB(段描述符的段界限是20位的);如果G=1,则段界限是以4KB为单位的,范围4KB-4GB。
S位(Descriptor Type,描述符类型):
当S=0,说明这是一个系统段,当S=1,说明这是一个代码段或者是数据段(栈段是特殊的数据段)。
DPL位(Descriptor Privilege Level,DPL特权级)
32位处理器的DPL位有四种,分别是0,1,2,3(就是特权级0123),不同特权级的程序是相互隔离的,其访问是严格限制的,而且有些处理器指令(特权指令)只能由0特权级的程序来执行。在这里,DPL直的是访问该段所必须拥有的最低特权级,如果这里的数值是2,那么特权级0,1,2可以访问这个段,特权级3访问这个段会被处理器阻止。
P位(Segement Present,存在位)
P位用于描述描述符对应的段是否存在,一般来说,描述符所指示的段都是存在于内存中的,但是,当内存空间紧张的时候,有可能只是建立了描述符,对应的内存空间并不存在,这个时候就应该把P位清零。表示段并不 存在,另外,同样是在内从空间紧张的情况下,会把很少用到的段换出到硬盘中,腾出空间给当前急需内存的程序使用(当前正在执行的),这时,同样要把段的描述符P位清零,当再次轮到它执行时,P=1.
P位是处理器负责检查的,每当通过描述符访问内存的段中时,如果P位是0,则处理器会产生一个异常中断,通常,这个中断处理过程是操作系统提供的。该处理过程的任务是负责将该段从硬盘中换回内存,并将P=1。在多用户,多任务的系统中,这是一种常用的虚拟内存调度策略。
D/B位(Default Operation Size,默认的操作数大小)
对于代码段,当D=0表示指令的偏移地址或者操作数是16位的;D=1表示偏移地址或者操作数是32位的.
对于栈段,当D=0,表示使用SP寄存器,栈段上界是0xFFFF;当D=1,表示使用ESP寄存器,栈段上界是0xFFFFFFFF。
L位(64-bit Code Segement)
这个位用于保留给64位处理器使用,当L=0,表示是32位处理器,当L=1,表示是64位处理器。
TYPE位(描述符类别)
X表示是否可执行(eXecutable)。数据段总是不可执行的,X=0,代码段可执行,X=1。
E是对数据段而言,指示段的扩展方向,E=0表示向上拓展,E=1表示向下拓展。
W段指示读写属性,W=0指示不可写入,否则会引发处理器异常中断,W=1表示允许写入。
对代码段而言,C表示是否特权级依从(Conforming),C=0表示非依从的代码段,这样的代码段可以和他特权级相同的代码段调用。或者通过们调用,C=1表示允许从低特权级的程序转移到该段执行。代码段总是可以 执行,但是总是不允许被修改(如果要修改代码段,可以指定一个可以读写的数据段指向这个代码段)。,至于能不能读出,由R位决定,R=0表示不能读出,R=1表示可以读出。(相当于一个ROM)(R位不是指示处理器能 否读取指令的,而是限制程序和指令的行为,比如使用超越前缀CS:访问代码段的内容)。
A位是已访问位(Assessed),指示这个段最近有没有被访问过,在描述符创建后,这个位置应该被置零。之后,每当这个段被访问的时候,处理器都会将这个位变成1,对这个位清零是操作系统做的,通过定期监视该段 的位置,可以统计出该段的使用频率,当内存空间紧张的时候,可以不经常用的段退到硬盘上,从而实现虚拟内存管理。
AVL位(Available)
通常由操作系统用,处理器并不会使用它。
3. 安装存储器的段描述符并加载到GDTR中
处理器规定,全局描述表的第一个表必须是0,相当于是NULL
在这里,lgdt就是load GDT的简称,意图也很明显,就是加载GDT的意思,这个指令的操作数是一个48位的内存单位,低16位是GDT的界限,高32位是GDT的线性基地址,这个指令的操作数在16位模式下是16位的,在32位保护模式下是32位的,这个指令在保护模式和16位模式都可以执行。(注意段界限一定是大小-1,不要错了)。
在初始化状态下,GDTR的基地址被初始化为0x00000000,界限被初始化为0xFFFF,这个指令不会影响任何标志位。
注意:在进入主引导程序的时候,段寄存器和GDTR的内容和处理器刚加电的时候不再相同。因为BIOS加电自检程序在执行的时候要进入保护模式,进行相应的测试,这会改变相关段的内容。
4. 关于A20(第21个地址线)开启问题
8086只有20根地址线,只能访问1MB的内存,到了80386有32根地址线,这里就会出现一个问题,在8086时代,很多程序都会利用20位地址回绕特性(当物理地址超过0xFFFFF就会回绕到0x00000),而到了80286以后,由于地址线加多了,这个进位不会被丢弃,所以就会引发很多问题。
Intel想了一个方法,他们在80286和80386在A20处使用一个与门控制,并且把这个与门的控制阀门放在键盘上,端口号是0x60,向这个端口写入数据的时候,如果这个第一位是1,那么键盘控制器通向与门的输出就是1,与门的输出决定于A20是0还是1(在实模式下,只要强制与门的输出为0,那么实模式的回绕特性将会被保留)。
我们先来看这种老式方法的操作,代码From: http://hengch.blog.163.com/blog/static/107800672009013104623747/
这种方法非常麻烦,后来到了80486,这个问题被得到简化。在80486以后,处理器本身就有了A20M#引脚(A20 Mask,A20屏蔽),这个引脚低电平有效。在ICH上,有一个用于兼容老式设备的端口0x92,第7-2位保留,第0位叫做INIT_NOW,用于初始化处理器,当它从0到1过渡的,ICH会使INIT#引脚电平变为低电平有效,并保持至少16个PCI时钟周期,也就是说,如果向0x92写入1,那么就会让处理器复位,导致计算机强制重启。
当INIT_NOW从0到1,ALT_A20_GATE将会被置为1,这就是说,计算机启动的时候,第21个根引线是自动启用的(但是A20#M是仅用于单处理器系统,多核系统一般是不用的)。现在基本都是USB设备了。
快速打开A20的方法,非常简单。
5. 32位保护模式下的内存访问
要开启保护模式,除了加载GDT,打开A20还不够,我们必须还要对CR0开关进行操作,CR0也是一个处理器内部的控制寄存器(Control Register,RD)。这样的控制器还有CR1,CR2,CR3等。CR0是一个32位的寄存器,他的第一位(0位)是保护模式允许位(Protection Enable,PE),如果把这个位置为1,那么处理器将会进入保护模式,按保护模式的规则开始运行。在保护模式下,实模式下的中断向量表不再适用,且我们不能再使用BIOS中断,这就是为什么我们之前要把中断关掉的原因。
在8086下,执行到第三行时,处理器先将ds的内容左4位,然后加上偏移地址0xc0,然后再把al的内容写入,实际写入的内容的位置是0x200c0。
在32位处理器下的实模式下,首先如果处理器要引用一个段(也就是执行将段地址传到段寄存器的指令),处理器会自动将段地址左移4位,然后传到描述符高速缓存器,这以后,就一直使用描述符高速缓存器的内容作为段地址。只要不改变段寄存器DS的内容,以后每次访问内存都直接使用DS描述符高速缓存器中的内容,在实模式下段寄存器只能传送16位的逻辑地址。(这个时候处理器不会把他看成是段的选择子),处理器也只能访问1MB的内存。
PS:这里书上有句话是错的,作者说高速缓存器是32位的,显然是错的,用bochs一看就知道是64位的,而且即使在实模式下,描述符高速缓存器的各个位置的定义都是一样的,并不存在像书上说的那样会把高位填充为0,看图。)
(dl和dh分别是描述符高速缓存器的低32位和高32位)
而在32位处理器下的保护模式下,传入段寄存器的内容不再是逻辑地址,而是段的选择子,所谓段的选择子,其实就是段描述符在描述符表(GDT,LDT)的索引号。
第一部分(0~1)是RPL特权级,表示给出当前选择该选择子的那个程序的特权级,第二部分是TI(2)(Table Indicator),当TI=0,表示描述表在GDT中;当TI=1,表示描述符在LDT中。第三部分(3~15)是描述符索引号,这个部分是只有13位的,正好和213=8192个描述符对应。
比如,我们要加载第一个在GDT中的段,可以这么写:
这表示我们想加载在GDT的第一个段,特权级是0
GDT的线性基地址在GDTR中,每个描述符占用8个字节,党处理器在执行改变段选择器的指令的时候,就将指令中的索引号乘以8得到偏移地址,和GDTR中的线性地址相加,以此访问GDT,处理器会根据GDT的界限以及特权级检查,如果没有问题,那么处理器就会将在对应描述符的内容的一部分(线性基地址,段界限和段的访问属性)加载到高速缓存中。此后,每当有访问内存的指令,就不会再访问GDT的描述符,而是直接用当前段寄存器的高速缓存的内容提供线性基地址。,访问代码段遗失一样如此访问的(EIP+高速缓存中的线性基地址)。
6. 清空流水线并且串行化处理器
在进入保护模式之前的最后一个步骤,就是要清空流水线,因为在实模式下,高速缓存器也被用来直接访问内存,但是这些内容在保护模式下是无效的;并且,在进入保护模式之前,已经有很多指令进入流水线了,在实模式下他们都是按照16位操作数或者16位地址长度编译的,即使用bits32编译的指令,进入保护模式之后,因为CS的描述符高速缓存中还有实模式残留的内容,可能会导致指令执行结果不正确,并且乱序执行得到的中间结果也是无效的,所以我们必须在进入保护模式之前把CS,SS,DS,ES,FS和GS的内容,包括段选择器和描述符高速缓存器的内容清除。
建议的做法就是在设置了CR0的PE位后,立马使用直接远转移指令jmp,当处理器遇到jmp时,一般会清空流水线,并且串行化执行。不仅如此,CS还会被重新加载,描述符高速缓存器的内容会被刷新。
当然也可以使用dword来描述偏移地址,这样的话flush对应标号有所不同(因为偏移地址和段的选择子的长度变了,变成32位,不加dword这两个长度都是16位),但是不影响执行。
需要注意的是,在保护模式下,不允许直接用mov指令改变段寄存器CS的内容,企图这样操作会引发无效操作码的异常中断。
注意:在跳转指令之前,处理器虽然进入了保护模式,但是,这个时候描述符高速缓存器的内容没有被刷新,但是处理器任然是可以继续执行下去的,因为检查描述符是否有效,通常是在加载段寄存器(选择器),并刷新描述符高速缓存器的时候进行的,比如jmp 0x0008:flush这条指令,而对于数据段来说,是加载段选择子的时候,比如mov ds,cx,但是现在因为是刚进入保护模式,描述符的很多位,是在实模式下都是无效的。
如图,在执行跳转指令之前,CS是个数据段,这显然是错的,描述符里面的数据只是实模式遗留下来的而已。只有跳转指令执行后,CS的描述符高速缓存器的内容才会被刷新。
★PART2:进入保护模式例程
;---------------------保护模式主引导扇区程序--------------------- mov ax,0x00 mov ss,ax mov sp,0x7c00 mov ax,[cs:gdt_base+0x7c00] mov dx,[cs:gdt_base+0x7c00+0x02] mov bx,0x10 div bx mov ds,ax ;得到base基地址,让ds指向这个地址 mov bx,dx ;得到偏移地址 ;---------------------安装描述符--------------------- ;描述符0 mov dword [ebx+0x00],0x00 ;第一个描述符必须是0 mov dword [ebx+0x04],0x00 ;描述符1 mov dword [ebx+0x08],0x7c0001FF mov dword [ebx+0x0c],0x00409800 ;基地址0x00007c00,段界限0x001FF,粒度是字节, ;长度是512字节,在内存中的32位段,特权级为0,只能执行的代码段 ;描述符2 mov dword [ebx+0x10],0x8000FFFF mov dword [ebx+0x14],0x0040920B ;基地址0x000B8000,段界限0x0FFFF,粒度是字节, ;长度是64KB,在内存中的32位段,特权级为0,可以读写的向上拓展的数据段 ;描述符3 mov dword [ebx+0x18],0x00007A00 mov dword [ebx+0x2c],0x00409600 ;基地址0x00000000,段界限0x07A00,粒度是字节, ;在内存中的32位段,特权级为0,可以读写的向下拓展的栈段 mov word [cs:gdt_size+0x7c00],31;写入GDT段界限,4个描述符是32个字节,所以界限就是31 lgdt [cs:gdt_size+0x7c00] ;load gdt mov dx,0x92 ;南桥ICH芯片内的端口0x92 in al,dx or al,0x02 out dx,al ;打开A20 cli ;关闭中断 mov eax,cr0 or eax,0x01 mov cr0,eax ;设置PE位,处理器进入保护模式 ;保护模式 jmp 0x0008:flush ;现在是在16位保护模式下,0x0008依然是段的选择子,而flush则是偏移地址 [bits 32] flush: mov cx,0x0010 mov ds,cx ;以下在屏幕上显示"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'
hlt
;------------------------------------------------------------------------------- gdt_size dw 0 gdt_base dd 0x00007e00 ;GDT的物理地址,主引导扇区是512个字节,这个地址刚好在主引导扇区之后 times 510-($-$$) db 0 db 0x55,0xaa