DIY操作系统(3):保护模式及kernel loader

保护模式

与实模式比较

模式地址总线宽度最大寻址空间访存地址计算(非段)寄存器宽度段寄存器宽度
实模式20b1M段地址*16+偏移16b16b
保护模式32b4GB基于GDT32b16b

实模式下程序对内存的访问不受限制,很容易破坏操作系统的运行,所以需要引入保护模式。

保护模式下的分段

为什么CPU访问内存要分段:
如果不分段,则编译时就必须确定每条指令在加载到内存时的位置,这是不可能的,同样如果编译出来多个程序使用相同的地址,则无法同时进入内存,会导致覆盖,因此引入了分段机制,即使用段基址+段偏移来确定指令或数据在某段的位置。汇编语言中可以将自己的程序分段(如数据段、代码段),再由编译器进行整合优化,操作系统对各段进行读入和记录信息,并给CPU的段寄存器赋值来进行段的选择。

在这里插入图片描述
CPU中有GDTR寄存器存放GDT的始址和界限,GDT的每一个表项称为一个段描述符,段寄存器中存放选择子,约等于描述符在GDT中的下标,可索引到描述符。全局描述符表是内存中所有进程共用的,一个进程的一个段对应GDT的一个表项,为与选择子未初始化的情况作区分,0号描述符不可用。索引到段描述符后将其整个加载到段描述符缓冲寄存器中。

例如当前段寄存器中选择子为0x8,段内偏移地址为0x9,访存过程为:取选择子的高13位0x1,在GDT中索引到段描述符1,假设段描述符中存储的段基址为0x1234,则将其加上段内偏移地址0x9,进行访存。

全局描述符表GDT

每一表项(即段描述符)占64B,分别描述各内存段的起始地址、大小、权限等信息,存放在内存中,GDT的地址、界限信息存放在GDTR寄存器(48b)中。
请添加图片描述
低16b描述了GDT的大小,最大为2^16B,而一个段描述符为2^3B,故最多可容纳2^13个段或门。

注意0号段描述符不可用,以区分未初始化选择子的情况。

引入保护模式后,段寄存器中保存的不再是段基址,而是该段的段描述符在GDT中的索引,称为选择子(selector)。

段描述符格式如下图。
在这里插入图片描述
字段说明如下:

  • 段界限:共占20b,描述段的大小并限制段内偏移地址。单位可为4KB或1B,由G位指定。
  • 段基址:共占32b,用于存储本段的起始物理地址。
  • S:为0时表示系统段,供硬件使用,如调用门、任务门等各种“门”;为1时表示非系统段,供软件(含OS)使用。
  • TYPE:共占4b,S为1时用于描述代码段和数据段的属性,如下,其中A代表Accessd,标识CPU是否访问;C代表Conforming,标识是否为一致性代码段;E代表Extend,标识段的扩展方向。在这里插入图片描述
  • DPL:占2b,描述符特权级,即常说的ring 0 1 2 3。
  • P:占1b,描述段是否在内存中。

基于GDT的访存地址计算

保护模式下段寄存器的宽度仍是16b,其存放的选择子结构如下:
请添加图片描述

  • RPL(2b):与特权级有关。
  • TI(1b):选择子在GDT中还是LDT(较少使用)中。
  • 索引值(13b):本段的段描述符在GDT中的下标。

CPU用选择子中的索引值取得段描述符,然后将段描述符中的段基址(32b)加上段内偏移地址即可得到物理地址。

进入保护模式的必要条件

必要条件:

  • 打开A20地址线

  • 将CR0寄存器的PE位置1,开启保护模式。(若为0则是实模式)

在loader中加以实现。

何为“保护”

  1. 向段寄存器中加载选择子时的保护:加载时,选择子的索引值必须小于等于GDT中描述符的个数,检查选择子对应的段描述符中的段类型是否与目标段寄存器的用途匹配(如:只有具备可执行属性的代码段才能被加载到CS段寄存器中),检查内存段是否存在等等。
  2. 访问代码段和数据段时的保护:访问任一地址时都需检查段界限,防止越界。
  3. 栈段的保护:与上一点类似。

代码实现

mbr.S

%include "boot.inc"
SECTION MBR vstart=0x7c00
        mov ax,cs
        mov ds,ax
        mov es,ax
        mov ss,ax
        mov fs,ax
        mov sp,0x7c00
        mov ax,0xb800
        mov gs,ax
 
        mov ax,0x600
        mov bx,0x700
        mov cx,0
        mov dx,0x184f
 
        int 0x10
 
        mov byte [gs:0x00],'1'
        mov byte [gs:0x01],0xA4
 
        mov byte [gs:0x02],''
        mov byte [gs:0x03],0xA4
 
        mov byte [gs:0x04],'M'
        mov byte [gs:0x05],0xA4
 
        mov byte [gs:0x06],'B'
        mov byte [gs:0x07],0xA4
 
        mov byte [gs:0x08],'R'
        mov byte [gs:0x09],0xA4
 
        mov eax,LOADER_START_SECTOR
        mov bx,LOADER_BASE_ADDR
        ; 读取4个扇区
        mov cx,4
        ; 加载kernel loader
        call rd_disk_m_16
        ; 跳转执行kernel loader
        jmp LOADER_BASE_ADDR
 ;读取硬盘n个扇区的函数
rd_disk_m_16:
        mov esi,eax
        mov di,cx
 
        mov dx,0x1f2
        mov al,cl
        out dx,al
        mov eax,esi
 
        mov dx,0x1f3
        out dx,al
 
        mov cl,8
        shr eax,cl
        mov dx,0x1f4
        out dx,al
 
        shr eax,cl
        mov dx,0x1f5
        out dx,al
 
        shr eax,cl
        and al,0x0f
        out dx,al
 
        shr eax,cl
        mov dx,0x1f5
        out dx,al
 
        shr eax,cl
        and al,0x0f
        or al,0xe0
        mov dx,0x1f6
        out dx,al
 
        mov dx,0x1f7
        mov al,0x20
        out dx,al
 
        not_ready:
                nop
                in al,dx
                and al,0x88
                cmp al,0x08
                jnz not_ready
 
        mov ax,di
        mov dx,256
        mul dx
        mov cx,ax
        mov dx,0x1f0
 
        go_on_read:
                in ax,dx
                mov [bx],ax
                add bx,2
                loop go_on_read
                ; 读取函数结束
                ret
        ; 填充
        times 510-($-$$) db 0
        db 0x55,0xaa

boot.inc

; loader代码被加载到内存中何处
LOADER_BASE_ADDR equ 0x900
; loader在硬盘上的扇区号(LBA方式,0号为起始)
LOADER_START_SECTOR equ 0x2
;--------------   gdt描述符属性  -------------
DESC_G_4K   equ	  1_00000000000000000000000b   ; 段大小单位为4KB
DESC_D_32   equ	   1_0000000000000000000000b
DESC_L	    equ	    0_000000000000000000000b	;  64位代码标记,此处标记为0便可。
DESC_AVL    equ	     0_00000000000000000000b	;  cpu不用此位,暂置为0  
DESC_LIMIT_CODE2  equ 1111_0000000000000000b
DESC_LIMIT_DATA2  equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2  equ 0000_000000000000000b
; 段是否在内存
DESC_P	    equ		  1_000000000000000b
; 特权级
DESC_DPL_0  equ		   00_0000000000000b
DESC_DPL_1  equ		   01_0000000000000b
DESC_DPL_2  equ		   10_0000000000000b
DESC_DPL_3  equ		   11_0000000000000b
; 区分代码段、数据段、系统段
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.

; 构造各种段的高4B直接用
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
RPL1  equ   01b
RPL2  equ   10b
RPL3  equ   11b
TI_GDT	 equ   000b
TI_LDT	 equ   100b

loader.S

   %include "boot.inc"
   section loader vstart=LOADER_BASE_ADDR
   ; 定义loader的栈的栈顶位置
   LOADER_STACK_TOP equ LOADER_BASE_ADDR
   ; 调用函数
   jmp loader_start					
   
; 拼接得到完整的32b的0号描述符、代码段描述符、数据段和栈段描述符、显存段描述符
   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	       ;limit=(0xbffff-0xb8000)/4k=0x7
	       dd    DESC_VIDEO_HIGH4  ; 此时dpl已改为0
   ; GDT大小
   GDT_SIZE   equ   $ - GDT_BASE
   ; GDT段界限
   GDT_LIMIT   equ   GDT_SIZE -	1 
   times 60 dq 0					 ; 此处预留60个描述符的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起始地址
   ; 以后使用lgdt 48b数据,将GDT相关信息写入GDTR寄存器
   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		 ;
   ; 在实模式下打印字符串"2 loader in real"
   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,见下文解释
   ;jmp dword SELECTOR_CODE:p_mode_start	     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
   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
   ; 在保护模式下显示字符P
   mov byte [gs:160], 'P'

   jmp $

用jmp的原因:

  • loader实现了由实模式转保护模式,转换前后,访存地址的计算不一致,因此转换时需要马上刷新段寄存器。
  • 流水线指令译码错误。在76行mov cr0, eax执行后,CPU已进入保护模式,所以之后的指令按道理都应该是32b指令,但根据流水线运行方式,此时78行及之后的一些代码已经在流水线上了,已经按16b指令完成了译码,如果继续执行的话必然错误。[bits 32]之后的指令已经是在保护模式下执行的了,所以可全编译为32b指令。

测试

进入保护模式后查看GDT表,最后一项为显存段的段描述符,选择子0x0018,段基址是0xb8000
在这里插入图片描述
未用选择子初始化的gs段寄存器(进入保护模式前其段基址是0xb800,即输出的第一项信息)
在这里插入图片描述
用显存段的选择子 0x0018初始化gs寄存器
执行完 mov ax, SELECTOR_VIDEOmov gs,ax之后,CPU根据选择子取出段描述符,,初始化gs寄存器完成,输出信息的第一项是其选择子0x0018,dh和dl是段描述符缓冲寄存器的内容。
在这里插入图片描述

往显存中特定位置写入字母p,显示。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值