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

第四章 保护模式入门

本文是对《操作系统真象还原》第四章学习的笔记,欢迎大家一起交流。

知识部分

为什么要有保护模式?

实模式下安全问题:

  • 实模式下操作系统和用户程序属于同一特权级,平起平坐,没有区别对待;
  • 用户程序所引用的地址都是指向真实的物理地址,也就是说逻辑地址等于物理地址,实实在在的指哪打哪;
  • 用户程序可以自由修改段基址,可以不亦乐乎地访问所有内存,没人拦得住;

实模式下使用上的缺陷:

  • 访问超过 64KB 的内存区域是要切换段基址,转来转去容易晕呼;
  • 一次只能运行一个程序,无法充分利用计算机资源;
  • 共 20 条地址线,最大可用内存为 1MB,这即使在 20 年前也不够用。

而保护模式提供了一种保护机制让程序不能随意访问所有内存空间,同时 CPU 的寻址范围也达到了 4GB。

保护模式的特点

保护模式之寄存器扩展

一张图即可,e 代表 extend

注意段寄存器并没有扩展,进入保护模式后,段寄存器中保存的再也不是段基址了,里面保存的内容叫选择子 selector,该选择子其实就是个数,用这个数来索引全局描述符表中的段描述符,把全局描述符表当成数组,选择子就像数组下标一样在,后面讲 GDT 的时候会详细说。

image

保护模式之寻址扩展

image

保护模式之运行模式反转

操作数反转前缀 0x66,寻址方式反转前缀 0x67。

bits 的指令格式是 [bits 16]或 [bits 32]。
[bits 16]是告诉编译器,下面的代码帮我编译成 1 6 位的机器码 。
[bits 32]是告诉编译器,下面的代码帮我编译成 32 位的机器码。

注:
进入保护模式需要三个步骤。
(1) 打开 A20 。(2) 加载 gdt 。(3) 将 cr0 的 pe 位置 1 。

后面代码部分有详细步骤。

全局描述符表(GDT)

实模式与保护模式寻址的不同

实模式下用 段基址:偏移 ​来寻址,即 段基址*16+偏移​,段基址是存在段寄存器中的。

在保护模式下,要访问 4gb 的内存空间,再采取实模式下的方法是远远不够的,所以段寄存器中存储的数据变成了选择子,其格式如下图所示,其中索引值即 gdt 中索引值,TI 用于指示是去 GDT 还是 LDT 中寻找段基址,RPL 是特权级

image

段描述符

GDT 是一个表格,其中每一个表项就对应一个全局描述符,操作系统在寻址市首先根据 gdtr 寄存器得到 GDT 内存起始地址,然后加上选择子的偏移(8 字节为单位),就找到了对应的段描述符,然后在段描述符里可以获取对应段的起始地址和界限,需要注意的是,GDT 表第 0 个段描述符不可用。因为选择子忘记设置的话,就会是 0(就像我们的 MBR 代码一上来就将段寄存全部初始化为 0),就会访问这个段描述符,而如果这个段描述符有内容的话,就会将段基址定位到其他我们并不想要的地方去,所以干脆直接让 GDT 表第 0 个段描述符不可用,未设置的选择子访问这个段描述符 CPU 就会产生异常并阻止。其中 gdtr 寄存器格式如下图所示:

image

段描述符格式如下图所示,可以看到段基址和段界限被区分成了不同部分,十分奇怪,这其实是为了兼容不同的 cpu 所导致的。

image

段描述符的格式有点复杂,下面对重点字段进行说明,详见书 P151:

G 字段:1 代表单位是 4k,0 代表 1 字节,可以看到段界限共 20 位,为 0 时正好是 1MB,为 1 时正好是 4GB

D/B 字段:1 代表有效地址和数据是 32 位,0 代表 16 位

L 字段:1 代表 64 位代码,0 代表 32 位

AVL 字段:看操作系统需求用,暂时不关注

P 字段:存在位

DPL 字段:0-3,4 个特权级

S 字段:是否是系统段(看是否是硬件需要的)

TYPE 字段:需要和 s 字段结合起来看,如下图所示:

image

再说一下段基址和段界限,我们现在的 cpu 都是处于平坦模型下,即是从 0x00000000-0xffffffff ​所以段基址是 0x0,段界限是 0xfffff​,在对应位填上即可

但是也有的段比较特殊,比如显存段,我们前面说过,显存段的范围是 0xB8000-0xBFFFF​,所以段基址是 0xB8000​,段界限是 (0xBFFFF-0xB8000+1)/4k-1=7

代码部分

首先 mbr 文件和 loader 编译命令要变一下,因为我们 loader 变大了,所以一个扇区不太够,我们直接改成 4 个,一劳永逸。

image

image

下面是 boot.inc 代码,我们新增了很多 GDT 的描述

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


 ;--------------   gdt描述符属性  -------------
DESC_G_4K   equ	  1_00000000000000000000000b    ;  4k粒度
DESC_D_32   equ	   1_0000000000000000000000b    ;  有效地址和数据是32位   
DESC_L	    equ	    0_000000000000000000000b	;  64位代码标记,此处标记为0便可。
DESC_AVL    equ	     0_00000000000000000000b	;  cpu不用此位,暂置为0  
DESC_LIMIT_CODE2  equ 1111_0000000000000000b    ;  代码段段界限高四位 全1
DESC_LIMIT_DATA2  equ DESC_LIMIT_CODE2          ;  数据段段界限 同 代码段
DESC_LIMIT_VIDEO2  equ 0000_000000000000000b    ;  显存段段界限大小为BFFFF-B8000 = 7FFF,在20位段界限下高4位全0
DESC_P	    equ		  1_000000000000000b        ;  存在位
DESC_DPL_0  equ		   00_0000000000000b        ;  r0
DESC_DPL_1  equ		   01_0000000000000b        ;  r1
DESC_DPL_2  equ		   10_0000000000000b        ;  r2
DESC_DPL_3  equ		   11_0000000000000b        ;  r3
DESC_S_CODE equ		     1_000000000000b        ;  表示代码段非系统段
DESC_S_DATA equ	  DESC_S_CODE                   ;  数据段同代码段
DESC_S_sys  equ		     0_000000000000b        ;  系统段
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   ;最后加的是段基址BFFFF的23-16位

;--------------   选择子属性  ---------------
                                                    ;RPL代表特权级
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 的内容

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp  near loader_start 				

;构建gdt及其内部的描述符
    GDT_BASE:   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     ;段基址:0xB8000到0xBFFFF为文字模式显示内存,此处取后四位
                                    ;limit:0x0007 (bFFFF-b8000+1)/4k = 0x8 由于从0开始,所以再减一
            dd    DESC_VIDEO_HIGH4

    GDT_SIZE   equ   $ - GDT_BASE    ;得到gdt大小
    GDT_LIMIT   equ   GDT_SIZE -	1   ;大小减1即为gdt界限
    times 50 dq 0           ;此处预留50个描述符的slot
    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的指针,前2字节是gdt界限,后4字节是gdt起始地址
   gdt_ptr  dw  GDT_LIMIT 
	    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
                  
;1 打开A20
    in al, 0x92				
    or al, 0000_0010B
    out 0x92, al

;2 加载gdt
    lgdt [gdt_ptr]

;3 将cr0的pe位置1
    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax

   jmp  dword 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'               ; 显示在第二行
                                        ; 默认的文本显示模式是80*25,即每行是 80 个字符(0~79),每个字符占 2 字节,故传入偏移地址是 80*2=160。

   jmp $

上面代码的主要作用还是打印字符,一个是利用 BIOS 字段一个是直接操纵显存打印不同字符,注释很详细了,下面对关键点进行说明

6-15 行:构建 GDT 表,包含代码段描述符,数据/栈段描述符,显存段描述符

17-18 行:得到 GDT 表起始地址和大小,后面填到 gdtr 寄存器

19 行:填充了 50 个空白的段描述符,以便后面使用

20-22 行:得到三个段的选择子

50-56 行:利用 BIOS 字段打印字符

63-74 行:进入保护模式

76 行:无条件跳转,用于刷新流水线,因为在执行下面 32 位代码之前,已经先把代码送上了流水线,进行取指、译码等操作,在这两步 32 位和 16 位又有很大的不同,段寄存器的使用不同,16 位编译 32 位指令会加上 0x66/0x67 等反转符,所以我们要清空流水线,以便顺利进入 32 位

80-90 行:进入到 32 位模式,打印字符 P

最终效果如下:

image

posted @   fdx_xdf  阅读(71)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示