进入保护模式

Posted chengmf

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进入保护模式相关的知识,希望对你有一定的参考价值。

全局描述符

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

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

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

存储器的段描述符

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

??相应地,因为堆栈指针寄存器 SP 被初始化为0x7c00,和 CS 一样,堆栈段寄存器 SS 被初始化为0x0000,而且堆栈是向下扩展的,所以,从 0x00007c00往下的区域是实际上可用的堆栈区域。只不过,该区域包含了很多 Bios 数据,包括实模式下的中断向量表。技术图片
??每个表述符在GDT中占8个字节,也就是2个双子,或者说是64位。
技术图片
在 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 位,用于指示描述符的子类型,或者说是类别。技术图片技术图片
    • 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) 的索引号。
技术图片
??第一部分(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

以上是关于进入保护模式的主要内容,如果未能解决你的问题,请参考以下文章

从DOS程序进入保护模式

进入保护模式

尝试使用片段保存夜间模式状态

Lab_1:练习3——分析bootloader进入保护模式的过程

Linux 0.11-进入保护模式前最后一次折腾内存-05

进入保护模式