《操作系统真象还原》 第四章 保护模式入门

发布于:2025-07-26 ⋅ 阅读:(15) ⋅ 点赞:(0)

1.概述:在前三章的学习中,我们都是在实模式下进行操作,但实模式存在着一些弊端,如用户程序可以自由修改段基址,可以随意访问内存且可用内存只有1MB等问题,为了解决这些问题,处理器厂商开发出了保护模式。

2.保护模式概述:保护模式强调的是“保护”,它是在 Intel 80286 CPU 中首次出现的,这是继 8086 之后,Intel 紧接着推出的一款产品。

2.1 保护模式之寄存器扩展

保护模式是在地址总线和数据总线为32位的环境下运行的,其寻址空间达到了2的32次方,4GB,在内存寻址方法得兼容老方法(段基址:段内偏移量)且要达到4GB的寻址空间,寄存器就得与时俱进,将寄存器宽度提升到32位。因此在保护模式中,除了段寄存器以外,其余寄存器的宽度都提升到了32位。

由于段寄存器中保存的不再是段基址,里面保存的内容叫“选择子”,selector,该选择子其实就是个数,用这个数来索引全局描述符表中的段描述符,把全局描述符表当成数组,选择子就像数组下标 一样。所以段寄存器不需要进行扩展。

2.2 段描述符、全局描述符表、选择子

段描述符:就是用于描述一个内存段的结构,大小为8字节,结构如下图所示

段描述符大小为8字节,在上图中,人为地分成了高32位和低32位,实际中8字节(64位)是连续的。保护模式下地址总线宽度是 32 位,段基址需要用 32 位地址来表示。 段界限表示该段边界的扩展最值,即最大扩展到多少或最小扩展到多少,也可理解为该内存段的大小。其它字段所表示的具体意义可查阅课本p152页。

全局描述符表GDT:一个段描述符只用来定义(描述)一个内存段。代码段要占用一个段描述符、数据段和栈段等,多个 内存段也要各自占用一个段描述符,这些描述符放在哪里呢?答案是放在全局描述符表。全局描述符表 GDT 相当于是描述符的数组,数组中的每个元素都是 8 字节的描述符。可以用选择子(马上会讲到)中提供的下标在 GDT 中索引描述符。全局描述符表位于内存中,需要用专门的寄存器指向它后,CPU 才知道它在哪里。这个专门的寄存器便是 GDTR, 即 GDT Register,专门用来存储 GDT 的内存地址及大小。GDTR 是个 48 位的寄存器(包含GDT起始地址和界限)。对此寄存器需要使用lgdt指令进行初始化。

需要注意的是,GDT表第0个段描述符不可用。因为选择子忘记设置的话,就会是0(就像我们的MBR代码一上来就将段寄存全部初始化为0),就会访问这个段描述符,而如果这个段描述符有内容的话,就会将段基址定位到其他我们并不想要的地方去,所以干脆直接让GDT表第0个段描述符不可用,未设置的选择子访问这个段描述符CPU就会产生异常并阻止。

选择子:在实模式下,段寄存器保存的就是段基址,而保护模式下,段基址已经保存在了段描述符中,段寄存器中再保存段基址是没有意义的,因此段寄存器中保存的是选择子,选择子相当于一个索引值,用此索引值在段描述符表中索引相应的段描述符,这样,便在段描述符 中得到了内存段的起始地址和段界限值等相关信息。选择子结构如下图所示:

RPL位表示请求特权等级 ,TI位表示是在GDT(全局描述符表)还是LDT(局部描述符表)中索引段描述符。选择子的作用主要是确定段描述符,确定描述符的目的,一是为了特权级、界限等安全考虑,最主要的还是要确定段的基地址。

实模式和保护模式的不同:

        (1)实模式的运行环境是16位,保护模式的运行环境是32位。

        (2)实模式下段寄存器存放的就是段基址,保护模式下段寄存器存放的是选择子。

        (3)保护模式下除了段寄存器,其余寄存器都扩展到了32位。

        (4)实模式下共有20条地址线,最大可用内存为1MB,保护模式地址可用内存为4GB。 

3.进入保护模式

首先创建boot.inc配置文件定义段描述符宏以及选择子宏(/include/boot.inc)

                                                    ;-------------	 loader和kernel   ----------
LOADER_BASE_ADDR equ 0x900 
LOADER_START_SECTOR equ 0x2

                                                    ;--------------   模块化的gdt描述符字段宏-------------
DESC_G_4K   equ	  1_00000000000000000000000b        ;设置段界限的单位为4KB
DESC_D_32   equ	   1_0000000000000000000000b        ;设置代码段/数据段的有效地址(段内偏移地址)及操作数大小为32位,而非16位
DESC_L	    equ	    0_000000000000000000000b	    ;64位代码段标记位,我们现在是在编写32位操作系统,此处标记为0便可。
DESC_AVL    equ	     0_00000000000000000000b	    ;此标志位是为了给操作系统或其他软件设计的一个自定义位,
                                                    ;可以将这个位用于任何自定义的需求。
                                                    ;比如,操作系统可以用这个位来标记这个段是否正在被使用,或者用于其他特定的需求。
                                                    ;这取决于开发者如何使用这个位。但从硬件的角度来看,AVL位没有任何特定的功能或意义,它的使用完全由软件决定。
DESC_LIMIT_CODE2  equ 1111_0000000000000000b        ;定义代码段要用的段描述符高32位中16~19段界限为全1
DESC_LIMIT_DATA2  equ DESC_LIMIT_CODE2              ;定义数据段要用的段描述符高32位中16~19段界限为全1
DESC_LIMIT_VIDEO2  equ 0000_000000000000000b        ;定义我们要操作显存时对应的段描述符的高32位中16~19段界限为全0
DESC_P	    equ		  1_000000000000000b            ;定义了段描述符中的P标志位,表示该段描述符指向的段是否在内存中
DESC_DPL_0  equ		   00_0000000000000b            ;定义DPL为0的字段
DESC_DPL_1  equ		   01_0000000000000b            ;定义DPL为1的字段
DESC_DPL_2  equ		   10_0000000000000b            ;定义DPL为2的字段
DESC_DPL_3  equ		   11_0000000000000b            ;定义DPL为3的字段
DESC_S_CODE equ		     1_000000000000b            ;无论代码段,还是数据段,对于cpu来说都是非系统段,所以将S位置为1,见书p153图
DESC_S_DATA equ	  DESC_S_CODE                       ;无论代码段,还是数据段,对于cpu来说都是非系统段,所以将S位置为1,见书p153图
DESC_S_sys  equ		     0_000000000000b            ;将段描述符的S位置为0,表示系统段
DESC_TYPE_CODE  equ	      1000_00000000b	        ;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.  
DESC_TYPE_DATA  equ	      0010_00000000b	        ;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.


                                                    ;定义代码段,数据段,显存段的高32位
DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b

                                                    ;--------------   模块化的选择子字段宏  ---------------
RPL0  equ   00b                                     ;定义选择字的RPL为0
RPL1  equ   01b                                     ;定义选择子的RPL为1
RPL2  equ   10b                                     ;定义选择字的RPL为2
RPL3  equ   11b                                     ;定义选择子的RPL为3
TI_GDT	 equ   000b                                 ;定义段选择子请求的段描述符是在GDT中
TI_LDT	 equ   100b                                 ;定义段选择子请求的段描述符是在LDT中

修改loader.s文件内容,该文件主要完成初始化GDT后,通过显存的段描述符对显存寻址后实现字符的打印。

主要步骤:

        (1)初始化GDT

           (2)  利用BIOS中断打印字符

           (3)  打开保护模式,加载GDT表的基址进入GDTR寄存器

                打开保护模式:1、打开A20地址线(原因是因为8086存在高端内存,这部分内存只在逻辑中存在,物理中并没有对应。由于8086只有20根地址线,所以访问高端内存会自动丢掉最高位,所以并没有问题,当时很多程序员就利用这个特性偷懒。但是当后续CPU多了地址线后,之前8086偷懒程序员写的程序就会真的访问对于8086来说的高端内存。所以为了兼容他们的程序,就用A20地址线来控制是否能够访问更多的内存。用in与out指令就能与A20交互);2、加载GDT表的首地址进入GDTR;3、将CR0寄存器的pe位置为1,意为打开保护模式(mov指令即可)

         (4)刷新流水线

         (5)加载显存段选择子,在保护模式下操作显存来显示字符

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
    jmp loader_start					                ;loader一进来是一大堆GDT段描述符数据,无法执行,所以要跳过
   
                                                        
GDT_BASE:                                               ;构建gdt及其内部的描述符
    dd 0x00000000 
	dd 0x00000000

CODE_DESC:  
    dd 0x0000FFFF 
	dd DESC_CODE_HIGH4

DATA_STACK_DESC:  
    dd 0x0000FFFF
    dd DESC_DATA_HIGH4

VIDEO_DESC: 
    dd 0x80000007	                                    ;limit=(0xbffff-0xb8000)/4k=0x7
    dd DESC_VIDEO_HIGH4                                 ; 此时dpl已改为0

    GDT_SIZE equ $ - GDT_BASE
    GDT_LIMIT equ GDT_SIZE - 1 
    times 60 dq 0					                    ; 此处预留60个描述符的空间
    SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0       ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
    SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	    ; 同上
    SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	    ; 同上 
gdt_ptr dw GDT_LIMIT                                    ;定义加载进入GDTR的数据,前2字节是gdt界限,后4字节是gdt起始地址,
	    dd  GDT_BASE
loadermsg db '2 loader in real.'

loader_start:

                                                        ;------------------------------------------------------------
                                                        ;INT 0x10    功能号:0x13    功能描述:打印字符串
                                                        ;------------------------------------------------------------
                                                        ;输入:
                                                        ;AH 子功能号=13H
                                                        ;BH = 页码
                                                        ;BL = 属性(若AL=00H或01H)
                                                        ;CX=字符串长度
                                                        ;(DH、DL)=坐标(行、列)
                                                        ;ES:BP=字符串地址 
                                                        ;AL=显示输出方式
                                                        ;   0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
                                                        ;   1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
                                                        ;   2——字符串中含显示字符和显示属性。显示后,光标位置不变
                                                        ;   3——字符串中含显示字符和显示属性。显示后,光标位置改变
                                                        ;无返回值
    mov sp,LOADER_BASE_ADDR
    mov	bp,loadermsg                                    ; ES:BP = 字符串地址
    mov	cx,17			                                ; CX = 字符串长度
    mov	ax,0x1301		                                ; AH = 13,  AL = 01h
    mov	bx,0x001f		                                ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
    mov	dx,0x1800		                                ;
    int	0x10                                            ; 10h 号中断

                                                        ;-----------------   准备进入保护模式   ------------------------------------------
                                                        ;1 打开A20
                                                        ;2 加载gdt
                                                        ;3 将cr0的pe位置1


                                                        ;-----------------  打开A20  ----------------
    in al, 0x92
    or al, 0000_0010B
    out 0x92,al

                                                        ;-----------------  加载GDT  ----------------
    lgdt [gdt_ptr]


                                                        ;-----------------  cr0第0位置1  ----------------
    mov eax,cr0
    or eax,0x00000001
    mov cr0,eax

                                                        ;jmp dword SELECTOR_CODE:p_mode_start	    
    jmp  SELECTOR_CODE:p_mode_start	                    ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
					                                    ; 这将导致之前做的预测失效,从而起到了刷新的作用。

[bits 32]
p_mode_start:
    mov ax,SELECTOR_DATA
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov esp,LOADER_STACK_TOP
    mov ax,SELECTOR_VIDEO
    mov gs,ax

    mov byte [gs:160], 'P'

    jmp $

其中times 60 dq 0 表示 预留60个段描述符的位置以便以后使用 dq 表示定义8字节数据(一个段描述符大小),times 60 表示循环执行60次,lgdt [gdt_ptr]表示为GDTR寄存器赋值加载GDT,[bits 32]代表下来的指令编译成32位,mov byte [gs:160], 'P' gs寄存器中存放着显存段描述符的选择子,CPU会根据选择子的索引值在GDT中的到显存的段基址。段内偏移量为160(文本模式显示窗口大小默认为80*25,一个字符占两个字节,所以偏移量160表示第二行第一个字符)。

修改mbr.s中的内容(此处的mbr.s文件与上一章的一样),因为将loader.s编译后超出了在MBR中所设置的512B,所以我们需要修改MBR中读入磁盘扇区的个数(修改为4)

    mov cx,4			            ; 待读入的扇区数

使用以下命令分别编译mbr.s和loader.s文件并启动bochs虚拟机就会得到下图效果。

mbr.s:

nasm -o /home/yyx/Desktop/chapter4/mbr /home/yyx/Desktop/chapter4/mbr.s -I /home/yyx/Desktop/chapter4/include

dd if=/home/yyx/Desktop/chapter4/mbr of=/home/yyx/Desktop/bochs/hd60M.img bs=512 count=1 conv=notrunc

loader.s:

nasm -o /home/yyx/Desktop/chapter4/loader /home/yyx/Desktop/chapter4/loader.s -I /home/yyx/Desktop/chapter4/include

dd if=/home/yyx/Desktop/chapter4/loader of=/home/yyx/Desktop/bochs/hd60M.img bs=512 count=4 conv=notrunc seek=2


注意:由于我们在mbr.s文件中修改了读入磁盘扇区个数,因此在使用dd命令将loader.s写入磁盘时count选项也因修改为4。 


网站公告

今日签到

点亮在社区的每一天
去签到