进入保护模式

全局描述符

  和一个段有关的信息需要 8 个字节来描述,所以称为段描述符(Segment Descriptor),每个段都需要一个描述符。为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里,所有的描述符都是挨在一起,集中存放的,这就构成一个描述符表,最主要的描述符表是全局描述符表(Global Descriptor Table,GDT)

  为了跟踪全局描述符表,处理器内部有一个 48 位的寄存器,称为全局描述符表寄存器(GDTR)。该寄存器分为两部分,分别是 32 位的线性地址和 16 位的边界。32 位的处理器具有 32 根地址线,可以访问的地址范围是 0x00000000 到0xFFFFFFFF,共 232字节的内存,即 4GB 内存。所以,GDTR的 32 位线性基地址部分保存的是全局描述符表在内存中的起始线性地址,16 位边界部分保存的是全局描述符表的边界(界限),其在数值上等于表的大小(总字节数)减一。
进入保护模式_第1张图片
  因为 GDT 的界限是 16 位的,所以,该表最大是 216 字节,也就是 65536 字节(64KB)。又因为一个描述符占 8 字节,故最多可以定义 8192 个描述符。

  由于在实模式下只能访问 1MB 的内存,故 GDT 通常都定义在 1MB 以下的内存范围中。当然,允许在进入保护模式之后换个位置重新定义 GDT。
进入保护模式_第2张图片

存储器的段描述符

  先是确定 GDT 的起始线性地址,并初始化了一个双字0x00007e00,我们决定从这个地方开始创建全局描述符表(GDT)。在实模式下,主引导程序的加载位置是 0x0000:0x7c00,也就是物理地址 0x07c00。因为现在的地址是 32 位的,所以它现在对应着物理地址 0x00007c00。主引导扇区程序共 512(0x200)字节,所以,我们决定把 GDT 设在主引导程序之后,也就是物理地址 0x00007e00 处。因为 GDT 最大可以为 64KB,所以,理论上,它的尺寸可以扩展到物理地址 0x00017dff 处。

  相应地,因为堆栈指针寄存器 SP 被初始化为0x7c00,和 CS 一样,堆栈段寄存器 SS 被初始化为0x0000,而且堆栈是向下扩展的,所以,从 0x00007c00往下的区域是实际上可用的堆栈区域。只不过,该区域包含了很多 BIOS 数据,包括实模式下的中断向量表。进入保护模式_第3张图片
  每个表述符在GDT中占8个字节,也就是2个双子,或者说是64位。
进入保护模式_第4张图片
在 32 位保护模式下,段地址是 32 位的线性地址,如果未开启分页功能,该线性地址就是物理地址。

  • G 位是粒度(Granularity) 位,用于解释段界限的含义。当 G 位是“0”时,段界限以字节为单位。此时,段的扩展范围是从 1 字节到 1 兆字节(1B~1MB),因为描述符中的界限值是 20 位的。相反,如果该位是“1”,那么,段界限是以 4KB 为单位的。这样,段的扩展范围是从 4KB到 4GB。
  • S 位用于指定描述符的类型(Descriptor Type)。当该位是“0”时,表示是一个系统段;为“1”时,表示是一个代码段或者数据段(堆栈段也是特殊的数据段)。
  • DPL 表示描述符的特权级(Descriptor Privilege Level,DPL)。这两位用于指定段的特权级。共有 4 种处理器支持的特权级别,分别是 0、1、2、3,其中 0 是最高特权级别,3 是最低特权级别。刚进入保护模式时执行的代码具有最高特权级 0(可以看成是从处理器那里继承来的),这些代码通常都是操作系统代码,因此它的特权级别最高。
  • P 是段存在位(Segment Present)。P 位用于指示描述符所对应的段是否存在。一般来说,描述符所指示的段都位于内存中。但是,当内存空间紧张时,有可能只是建立了描述符,对应的内存空间并不存在,这时,就应当把描述符的 P 位清零,表示段并不存在。
    • P 位是由处理器负责检查的。每当通过描述符访问内存中的段时,如果 P 位是“0”,处理器就会产生一个异常中断。通常,该中断处理过程是由操作系统提供的,该处理过程的任务是负责将该段从硬盘换回内存,并将 P 位置 1。在多用户、多任务的系统中,这是一种常用的虚拟内存调度策略
  • D/B 位是默认的操作数大小(Default Operation Size) 或者默认的堆栈指针大小(Default Stack Pointer Size),又或者上部边界(Upper Bound) 标志。
    • 在代码段,D=0 表示指令中的偏移地址或者操作数是 16 位的;D=1,指示 32 位的偏移地址或者操作数。
    • 在堆栈段,该位被叫做“B”位,用于在进行隐式的堆栈操作时,是使用 SP 寄存器还是ESP 寄存器。如果该位是“0”,在访问那个段时,使用 SP 寄存器,否则就是使用 ESP 寄存器。同时,B 位的值也决定了堆栈的上部边界。如果 B=0,那么堆栈段的上部边界(也就是 SP 寄存器的最大值)为 0xFFFF;如果 B=1,那么堆栈段的上部边界(也就是 ESP 寄存器的最大值)为 0xFFFFFFFF。
  • L 位是 64 位代码段标志(64-bit Code Segment),保留此位给 64 位处理器使用。
  • TYPE 字段共 4 位,用于指示描述符的子类型,或者说是类别。进入保护模式_第5张图片在这里插入图片描述
    • X 表示是否可以执行(eXecutable)。数据段总是不可执行的,X=0;代码段总是可以执行的。
    • E 位指示段的扩展方向。E=0 是向上扩展的,也就是向高地址方向扩展的,是普通的数据段;E=1 是向下扩展的,也就是向低地址方向扩展的,通常是堆栈段。
    • W 位指示段的读写属性,或者说段是否可写,W=0 的段是不允许写入的,否则会引发处理器异常中断;W=1的段是可以正常写入的。
    • C 位指示段是否为特权级依从的(Conforming)。C=0 表示非依从的代码段,这样的代码段可以从与它特权级相同的代码段调用,或者通过门调用;C=1 表示允许从低特权级的程序转移到该段执行。
    • R 位指示代码段是否允许读出。R=0 表示不能读出,如果企图去读一个 R=0 的代码段,会引发处理器异常中断;如果 R=1,则代码段是可以读出的,即可以把这个段的内容当成 ROM 一样使用。
    • 数据段和代码段的 A 位是已访问(Accessed) 位,用于指示它所指向的段最近是否被访问过。在描述符创建的时候,应该清零。之后,每当该段被访问时,处理器自动将该位置“1”。对该位的清零是由软件(操作系统)负责的,通过定期监视该位的状态,就可以统计出该段的使用频率。当内存空间紧张时,可以把不经常使用的段退避到硬盘上,从而实现虚拟内存管理
    • AVL 是软件可以使用的位(Available),通常由操作系统来用,处理器并不使用它。

安装存储器的段描述符并加载GDTR

  处理器规定,GDT 中的第一个描述符必须是空描述符,或者叫哑描述符或 NULL 描述符。在初始状态下(计算机启动之后),GDTR 的基地址被初始化为0x00000000;界限值为 0xFFFF。

 ;创建0#描述符,它是空描述符,这是处理器的要求
         mov dword [bx+0x00],0x00
         mov dword [bx+0x04],0x00  
         
         ;创建#1描述符,保护模式下的代码段描述符
         ;线性基地址为 0x00007C00。
		 段界限为 0x001FF,粒度为字节(G=0)。该段的长度为 512 字节。
		 属于存储器的段(S=1)。
		 这是一个 32 位的段(D=1)。
		 该段目前位于内存中(P=1)。
		 段的特权级为 0(DPL=00)。
		 这是一个只能执行的代码段(TYPE=1000)。
		 
         mov dword [bx+0x08],0x7c0001ff     
         mov dword [bx+0x0c],0x00409800     

         ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区)
         线性基地址为 0x000B8000。
		 段界限为 0x0FFFF,粒度为字节(G=0)。即,该段的长度为 64KB。
		 属于存储器的段(S=1)。
		 这是一个 32 位的段(D=1)。
		 该段目前位于内存中(P=1)。
		 段的特权级为 0(DPL=00)。
		 这是一个可读可写、向上扩展的数据段(TYPE=0010)。
		 
         mov dword [bx+0x10],0x8000ffff     
         mov dword [bx+0x14],0x0040920b     

         ;创建#3描述符,保护模式下的堆栈段描述符
         线性基地址为 0x00000000。
		 段界限为 0x07A00,粒度为字节(G=0)。
		 属于存储器的段(S=1)。
		 这是一个 32 位的段(D=1)。
		 该段目前位于内存中(P=1)。
		 段的特权级为 0(DPL=00)。
		 这是一个可读可写、向下扩展的数据段,即堆栈段(TYPE=0010)。
		 
         mov dword [bx+0x18],0x00007a00
         mov dword [bx+0x1c],0x00409600

         ;初始化描述符表寄存器GDTR
         mov word [cs: gdt_size+0x7c00],31  ;描述符表的界限(总字节数减一)   
                                             
         lgdt [cs: gdt_size+0x7c00] ;指向一个包含了 48 位(6 字节)数据的内存区域。
         ;16 位模式下,该地址是 16 位的;在 32 位模式下,该地址是 32 位的。该指令在实模式和保护模
式下都可以执行。

关于第21条地址线A20的问题

  8086只有20根地址线,只能访问1MB的内存,到了80386有32根地址线,这里就会出现一个问题,在8086时代,很多程序都会利用20位地址回绕特性(当物理地址超过0xFFFFF就会回绕到0x00000),而到了80286以后,由于地址线加多了,这个进位不会被丢弃,所以就会引发很多问题。

  Intel想了一个方法,他们在80286和80386在A20处使用一个与门控制,并且把这个与门的控制阀门放在键盘上,端口号是0x60,向这个端口写入数据的时候,如果这个第一位是1,那么键盘控制器通向与门的输出就是1,与门的输出决定于A20是0还是1(在实模式下,只要强制与门的输出为0,那么实模式的回绕特性将会被保留)。
  
  这种方法非常麻烦,后来到了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设备了。

保护模式下的内存访问

  要开启保护模式,除了加载GDT,打开A20还不够,我们必须还要对CR0开关进行操作,CR0也是一个处理器内部的控制寄存器(Control Register,RD)。这样的控制器还有CR1,CR2,CR3等。CR0是一个32位的寄存器,他的第一位(0位)是保护模式允许位(Protection Enable,PE),如果把这个位置为1,那么处理器将会进入保护模式,按保护模式的规则开始运行。在保护模式下,实模式下的中断向量表不再适用,且我们不能再使用BIOS中断,这就是为什么我们之前要把中断关掉的原因。

  在32位处理器下的实模式下,首先如果处理器要引用一个段(也就是执行将段地址传到段寄存器的指令),处理器会自动将段地址左移4位,然后传到描述符高速缓存器,这以后,就一直使用描述符高速缓存器的内容作为段地址。只要不改变段寄存器DS的内容,以后每次访问内存都直接使用DS描述符高速缓存器中的内容,在实模式下段寄存器只能传送16位的逻辑地址。(这个时候处理器不会把他看成是段的选择子),处理器也只能访问1MB的内存。

  在32位处理器下的保护模式下,传入段寄存器的内容不再是逻辑地址,而是段的选择子,所谓段的选择子,其实就是段描述符描述符表(GDT,LDT) 的索引号。
进入保护模式_第6张图片
  第一部分(0 ~ 1)是RPL特权级,表示给出当前选择该选择子的那个程序的特权级,第二部分是TI(2)(Table Indicator),当TI=0,表示描述表在GDT中;当TI=1,表示描述符在LDT中。第三部分(3~15)是描述符索引号,这个部分是只有13位的,正好和213=8192个描述符对应。

  GDT的线性基地址在GDTR中,每个描述符占用8个字节,党处理器在执行改变段选择器的指令的时候,就将指令中的索引号乘以8得到偏移地址,和GDTR中的线性地址相加,以此访问GDT,处理器会根据GDT的界限以及特权级检查,如果没有问题,那么处理器就会将在对应描述符的内容的一部分(线性基地址,段界限和段的访问属性)加载到高速缓存中。此后,每当有访问内存的指令,就不会再访问GDT的描述符,而是直接用当前段寄存器的高速缓存的内容提供线性基地址。,访问代码段遗失一样如此访问的(EIP+高速缓存中的线性基地址)。

清空流水线并且串行化处理器

  在进入保护模式之前的最后一个步骤,就是要清空流水线,因为在实模式下,高速缓存器也被用来直接访问内存,但是这些内容在保护模式下是无效的;并且,在进入保护模式之前,已经有很多指令进入流水线了,在实模式下他们都是按照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,但是现在因为是刚进入保护模式,描述符的很多位,是在实模式下都是无效的。

进入保护模式例程

         ;文件说明:硬盘主引导扇区代码 

         ;设置堆栈段和栈指针 
         mov ax,cs      
         mov ss,ax
         mov sp,0x7c00
      
         ;计算GDT所在的逻辑段地址 
         mov ax,[cs:gdt_base+0x7c00]        ;16位 
         mov dx,[cs:gdt_base+0x7c00+0x02]   ;16位 
         mov bx,16        
         div bx            
         mov ds,ax                          ;令DS指向该段以进行操作
         mov bx,dx                          ;段内起始偏移地址 
      
         ;创建0#描述符,它是空描述符,这是处理器的要求
         mov dword [bx+0x00],0x00
         mov dword [bx+0x04],0x00  

         ;创建#1描述符,保护模式下的代码段描述符
         mov dword [bx+0x08],0x7c0001ff     
         mov dword [bx+0x0c],0x00409800     

         ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 
         mov dword [bx+0x10],0x8000ffff     
         mov dword [bx+0x14],0x0040920b     

         ;创建#3描述符,保护模式下的堆栈段描述符
         mov dword [bx+0x18],0x00007a00
         mov dword [bx+0x1c],0x00409600

         ;初始化描述符表寄存器GDTR
         mov word [cs: gdt_size+0x7c00],31  ;描述符表的界限(总字节数减一)   
                                             
         lgdt [cs: gdt_size+0x7c00]
      
         in al,0x92                         ;南桥芯片内的端口 
         or al,0000_0010B
         out 0x92,al                        ;打开A20

         cli                                ;保护模式下中断机制尚未建立,应 
                                            ;禁止中断 
         mov eax,cr0
         or eax,1
         mov cr0,eax                        ;设置PE位
      
         ;以下进入保护模式... ...
         jmp dword 0x0008:flush             ;16位的描述符选择子:32位偏移
                                            ;清流水线并串行化处理器 
         [bits 32] 

    flush:
         mov cx,00000000000_10_000B         ;加载数据段选择子(0x10)
         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'

         ;以下用简单的示例来帮助阐述32位保护模式下的堆栈操作 
         mov cx,00000000000_11_000B         ;加载堆栈段选择子
         mov ss,cx
         mov esp,0x7c00

         mov ebp,esp                        ;保存堆栈指针 
         push byte '.'                      ;压入立即数(字节)
         
         sub ebp,4
         cmp ebp,esp                        ;判断压入立即数时,ESP是否减4 
         jnz ghalt                          
         pop eax
         mov [0x1e],al                      ;显示句点 
      
  ghalt:     
         hlt                                ;已经禁止中断,将不会被唤醒 

;-------------------------------------------------------------------------------
     
         gdt_size         dw 0
         gdt_base         dd 0x00007e00     ;GDT的物理地址 
                             
         times 510-($-$$) db 0
                          db 0x55,0xaa

你可能感兴趣的:(《从实模式到保护模式》)