5 Day:保护模式

前言:

兜兜转转,经历了前几天的磨练,我们拳打显卡,脚踢硬盘,手搓MBR,终于要从野蛮的实模式进入先进发达的保护模式了。今天我们将深入学习保护模式,GDT,以及让我们的操作系统进入保护模式,废话不多说现在开始吧!

参考资料:

《操作系统真象还原》


一,什么是保护模式?

在回答这个问题之前,我们先来看看远方的实模式吧家人们。

实模式缺点:

(1)可用内存只有1MB,对于现代操作系统来说肯定是不够用的。

(2)采用段基址+段偏移地址访问,可以随意访问任何内存并且篡改,十分不安全,设想一下你下载的一个软件,可以随意访问你的任何内存并篡改,这还得了。

(3)用户程序和系统程序平起平坐,应该要有阶级之分。

(4)访问超过64KB就要切换段基址,很累。

看看这些问题确实让人头疼呢,其他的还好,最重要的是内存只有1MB,这也太离谱了,要知道现在开机加载个操作系统都有几百MB内存要占用,但是没关系,保护模式帮我们一一解决了这些


①,保护模式的产生

实际上保护模式是在16位CPU80286上面诞生的,但是由于其还是16位CPU,最终还是被淘汰了。划时代的32位CPU80386完美的接过了他的接力棒,开启了真正的保护模式时代。

当CPU发展到32位时,可访问的内存范围达到了 4GB,运行模式和以前也发生了变化,并且随着硬件发展,高等级的硬件都要向下兼容,于是CPU便有了两种运行模式:实模式和保护模式

实模式:32位CPU16位下运行

保护模式:32位CPU32位下运行


ps:实模式和保护模式是只在32位CPU中才存在的,以前在8086的16位CPU是无实模式这一说的

32位CPU16位: 对于CPU来说32位的CPU可以变为16位模式是完全没问题的,你在16位模式下使用32位立即数都是能被允许的。

在你开机的时候都是由16位实模式转换为32位保护模式的


②,保护模式4大变化

  • 寄存器扩展

1,容量扩展:除段寄存器外,通用寄存器、指令指针寄存器、标志寄存器都由原来的 16 位扩展到了 32 位,也就是说通常的寄存器都可以直接访问4G的内存,再也不用像实模式那样不断地变换段基址了。


2,名称变化:扩展后的寄存器在其前面加上了一个e代表extend,32位寄存器的低16位可以单独使用,高16位是无法单独使用的。


3,段寄存器变化: 段寄存器不在保存段基址了,而是保存选择子(在GDT中的索引,之后会介绍)。


4,缓存技术:更新了一个新的寄存器段描述符缓存寄存器保护模式下负责缓存段描述符,实模式下负责缓存段基址*4的结果

  • 寻址扩展

寻址模式与寄存器改变:

在实模式下有三种寻址方式:基址寻址,变址寻址,基址变址寻址。其基址寄存器只能是bx,bp,变址寄存器只能是si,di。

而在保护模式下,因为已经是32位寄存器,无需再段基址*4来访问内存,更重要的是基址寄存器可以是任何32位通用寄存器,变址寄存器可以是除32位以外的所有通用寄存器,变址寄存器还可以乘以一个比例因子

  • 运行模式反转

编译器模式指明:

在CPU中,32位模式下和16位模式下有着完全不同的机器码格式,所以对于编译器来说将代码编译成16位机器码还是32位机器码就需要人为来指出,编译器提供了一个伪指令bits

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

bits指令范围是从当前bits的标签直到下一个bits的指令标签


反转前缀:

🌟!实模式和保护模式可以互相使用对方的资源,例如16位实模式可以使用32位寄存器!🌟

但是要如此访问的话就必须要使用一个前缀,来使当前模式转换为另一个模式,即模式转换

  • 操作数(0x66):

前缀为0x66,16位实模式使用,操作数变为32位。32位同理

  •  寻址方式反转(0x67):

  • 指令扩展

双操作指令:add,sub可以支持8,16,32

单操作指令:inc,dec支持8,16,32

loop指令:实模式cx,保护模式ecx

mul指令:

push: 

  • 立即数
对于CPU来说,操作数有一个对其的功能,将8位立即数对其到目前模式的位数再入栈。

#实模式下:
当压入8 位立即数时,由于实模式下默认操作数是 16 位, CPU 会将其扩展为 16 位后再将其入栈, sp-2
当压入 16 位立即数时, CPU 会将其直接入栈, sp-2
当压入 32 位立即数时, CPU 会将其直接入栈, sp-4

#保护模式下:
当压入 8 位立即数时,由于保护模式下默认操作数是 32 位, CPU 将其扩展为 32 位后入栈, esp-4
当压入 16 位立即数时, CPU 直接压入 2 宇节, esp-2
当压入 32 位立即数时, CPU 直接压入 4 字节, esp-4
  • 寄存器
#段寄存器:
无论那种模式下,都是按当前模式的默认操作数大小压入
16位:2字节 sp-2
32位:4字节 esp-4

#通用寄存器
无论那个模式下
压入16位栈指针-2
压入32位栈指针-4
  • 内存

与通用寄存器同理👆


二,全局描述符

全局描述符表( Global Descriptor Table, GDT )是保护模式下内存段的登记表,这是不同于实模式的 显著特征之一。


①,段描述符

段描述符的结构中,顾名思义,该结构专门用来描述一个内存段,该结构是 8 字节大小

 接下来我们来一一介绍段描述符里面的各个位的意义

  • 段基址(32位):描述一个段的位置
  • 段界限(20位):段的界限可能向上扩展或者向下扩展

公式:(段界限+1)*段粒度-1 

  • S(1位):0代表系统段(硬件用到的都是系统段),1代表数据段(软件用到的都是数据段)
  • TYPE(4位):描述TYPE的类型,其中也根据系统段和数据段来区分

我们先关注非系统段

A:accessed,该段被CPU访问过后置为1,未被访问或者新建置为0

C:Conforming,一致性代码段,1为一致性代码,0为非一致性代码段

R:可读段,1可读,0不可读

X:可执行段,1可执行,0不可执行

  • DPL(2位):代表段特权,将计算机权力分为不同的等级

 这两位能表示 4 种特权级,分别是0 1 2 3 级特权,数字越小,特权级越大。特权级是保护模式 下才有的东西, CPU 由实模式进入保护模式后,特权级自动为 0。因为保护模式 的代码已经是操作系统 的一部分啦,所以操作系统应该处于最高的0 特权级。用户程序通常处于3 特权级,权限最小。某些指令 只能在 特权级下执行,从而保证了安全。

  • P(1位):Present,是否存在于内存,存在为1,不存在为0,P字段由CPU检查,如果P字段为0,就需要抛给异常程序去处理,这个异常程序由我们来编写。

通常情况下,段都在内存中,只有少数情况内存不够用,段在磁盘中,需要进行置换。但是如果是平坦模式下4GB是无法置换的,因为大家都用你这4GB内存,而且4GB置换到磁盘中十分麻烦。置换只在开启分页功能时按页(4KB)置换是最好的。

  • AVL(1位):avaliable,可用的,对于用户态来说的。
  • L(1位):1代表是否设为64位代码段,我们写32位的所以先不考虑这些
  • D/B字段(1位):指定操作数和有效地址的大小

对于代码段来说他是D:0是16位,1是32位

对于栈段来说他是B:0是sp寄存器16位,1是esp寄存器32位

  • G(1位):粒度Granularity

0代表段界限的单位是1字节 最大界限是 2^20*1 = 1MB

1代表段界限的单位是4KB   最大界限是 2^20*4KB=4GB


三,GDT,LDT,选择子

讲完表里面的元素,我们就应该来讲表了。

① GDT:全局描述符表

寄存器:GDTR(48位)

存储指令:lgdt 48位内存数据

 存储段描述符数量:2^16/64=8192个段

 GDT起始地址从1开始,0为空描述符


简单来说,段描述符就是一个人的身份证,人们可以通过你的身份证得知你的信息,而GDT就是一个装满身份证的钱包,人们从钱包可以得到各种身份证。

② 选择子

在实模式中,采用的是段基址模式去寻址,而保护模式下采用的是选择子。

选择子位数:16位

0~1位:存储RPL即特权级0,1,2,3
2位:0是GDT,1是LDT
3~15位:代表索引值,刚好是2^13 = 8192

 寻址方式变为:段描述符中的段基址+段内偏移地址(注意无需*4了)


例如:选择子为0x8将选择子加载到ds中,GDT第一个描述符的段基址是0x1234,ds:0x9访问内存段

从选择子可得 RPL为0,访问GDT,且索引为1,于是访问的内存段为 0x1234+0x9=0x123d

③ LDT

支持多任务而创造的表,但是用的比较少,就不重点介绍了,

他的索引是从0 开始

寄存器为ldtr

加载指令为 lldt 16 位寄存器116 位内存


四,进军保护模式

有了以上的了解,我们就要准备进军保护模式了,但是先别急,还不准备写代码,再次之前我们总要知道如何进入保护模式吧,给你一分钟的时间思考如何进入保护模式。哈哈想不出来吧,因为我还有东西没写的😋,那就是打开地址线和设置CR0寄存器。


① 打开A20地址线

至于为什么要打开A20地址线,其实实际上就是做地址回绕,你说你不知道什么是地址回绕?我再Day 0 博客有说哦,快去看看吧。

有的人看完回来就可能会思考,地址回绕是针对20位CPU的呀,我们现在都32位了,如果还是使用实模式,该咋回绕啊。

对咯,所以32位CPU下的A20地址线的作用就是:

A20Gate被打开,访问0x10000~0x10FFFF时,CPU将真正访问这块物理内存

A20Gate被关闭时,访问0x10000~0x10FFFF时,将进行地址回绕

in al,0x92
or al,00000_0010B
out 0x92,al

② 保护模式开关,CR0寄存器的PE位

CRX寄存器即控制寄存器是CPU的窗口,展示CPU的内部状态控制CPU的运行机制

 这次我们将打开控制寄存器的PE位也就是第0位,打开这个就可以启用保护模式,芜湖完事具备只欠写代码了,准备冻手吧!!

mov eax, cr0 
or eax, 0x0000000l 
mov cr0, eax

③ 准备冻手写代码

我们先给boot.inc写一些全局变量(已修复)

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

; GDT 描述符属性
DESC_G_4K         equ 00000000_10000000_00000000_00000000b ; 段界限粒度为 4KB
DESC_D_32         equ 00000000_01000000_00000000_00000000b ; 指令中的有效地址及操作数是 32 位,指令有效地址用 EIP 寄存器
DESC_L            equ 00000000_00000000_00000000_00000000b ; 32 位代码段
DESC_AVL          equ 00000000_00000000_00000000_00000000b ; AVL 位无用途,置为 0
DESC_LIMIT_CODE2  equ 00000000_00001111_00000000_00000000b ; 段界限 19 ~ 16 位
DESC_LIMIT_DATA2  equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 00000000_00000000_00000000_00001011b ; 段界限 19 ~ 16 位
DESC_P            equ 00000000_00000000_10000000_00000000b ; 段存在于内存中
DESC_DPL_0        equ 00000000_00000000_00000000_00000000b ; 特权级 0
DESC_DPL_1        equ 00000000_00000000_00100000_00000000b ; 特权级 1
DESC_DPL_2        equ 00000000_00000000_01000000_00000000b ; 特权级 2
DESC_DPL_3        equ 00000000_00000000_01100000_00000000b ; 特权级 3
DESC_S_CODE       equ 00000000_00000000_00010000_00000000b ; 数据段
DESC_S_DATA       equ DESC_S_CODE                          ; 数据段
DESC_S_SYS        equ 00000000_00000000_00000000_00000000b ; 系统段
DESC_TYPE_CODE    equ 00000000_00000000_00001000_00000000b ; 可执行,非一致性,不可读,已访问位清 0
DESC_TYPE_DATA    equ 00000000_00000000_00000010_00000000b ; 不可执行,向上扩展,可写,已访问位清 0
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 + 0x00

DESC_TYPE_CODE equ 1000_00000000b ;code sector x=1,c=0,r=0,a=0
DESC_TYPE_DATA equ 0010_00000000b ;data sector x=0,c=0,r=1,a=0

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 + 0x00

;选择子属性
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b

;loader和kernel
PAGE_DIR_TABLE_POS equ 0x100000
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b

;kernel
KERNEL_START_SECTOR equ 0x6
KERNEL_BIN_BASE_ADDR equ 0x70000
KERNEL_ENTRY_POINT equ 0xc0001500
PT_NULL equ 0

再在loader.S中加入GDT载入代码和进入保护模式代码

;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	       ;limit=(0xbffff-0xb8000)/4k=0x7
	       			 dd    DESC_VIDEO_HIGH4  ; 此时dpl已改为0

   GDT_SIZE   equ   $ - GDT_BASE  ;GDT的大小
   GDT_LIMIT   equ   GDT_SIZE -	1  ;GDT的界限
   
   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起始地址

   gdt_ptr  dw  GDT_LIMIT 
	    dd  GDT_BASE

loader_start:
   mov byte [gs:0xA0],'G'
   mov byte [gs:0xA1],0xA4
   
   mov byte [gs:0xA2],'e'
   mov byte [gs:0xA3],0xA4
   
   mov byte [gs:0xA4],'n'
   mov byte [gs:0xA5],0xA4
   
   mov byte [gs:0xA6],'i'
   mov byte [gs:0xA7],0xA4
   
   mov byte [gs:0xA8],'u'
   mov byte [gs:0xA9],0xA4
   
   mov byte [gs:0xAA],'s'
   mov byte [gs:0xAB],0xA4

   mov byte [gs:0xA8],'O'
   mov byte [gs:0xA9],0xA4
   
   mov byte [gs:0xAA],'S'
   mov byte [gs:0xAB],0xA4


;准备进入保护模式


   ;-----------------  打开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;刷新流水线, 更新段描述符缓存寄存器

[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:0x140], 'P'

   jmp $

由于loader.S的字节已经超过512KB,所以在mbr中mov cx,1要改为mov cx,4

mov	cx, 4
call rd_disk_m_16
jmp LOADER_BASE_ADDR

然后在我们将loader.bin加载到img中的时候 count=1,要改为count=4,否则会加载不完全

#!/bin/bash
#rm -rf /usr/geniux/img/geniusos.img
nasm -I /usr/geniux/include/ -o mbr.bin mbr.S
dd if=mbr.bin of=/usr/os/bochs/share/bochs/geniusos.img bs=512 count=1 conv=notrunc
echo "disk write success!!"
nasm -I /usr/geniux/include/ -o loader.bin loader.S
dd if=loader.bin of=/usr/os/bochs/share/bochs/geniusos.img bs=512 count=2 seek=2 conv=notrunc
;注 loader的载入 count要改为 2,因为loader的大小已经超过512字节,所以需要变为复制两个扇区

成功运行截图                                     

                                                       🌈  !congratulations! 🌈

 现在可以说,我们已经慢慢探索到操作系统深处的入口了,明天会有什么更加有意思的东西等着我们呢,敬请期待吧!!


bug修复

在4day的时候,我们出现了跳转loader失败的现象,再次我来讲一下如何修复

  • bug 出现的原因:

软盘不规则,导致不支持in out指令,使得磁盘读取失败,在其他虚拟机不存在该问题


  • 解决方法:
  • 采用CHS方式读取硬盘,这是我上一个操作系统采用的读取方式,在此不细讲了,有兴趣的可以根据博客所说,自己实现一下
  • 用bximage 生成一个规则的image

在你的bochs文件夹下使用 bximage 命令

bximage

 然后选择【1】回车,一直往下回车

 

 输入60 (都🆗,我自己是60)回车

 自己给自己的硬盘取个好听的名字,我叫geniusos.img

 取完后就会在你当前bochs的文件夹下生成一个规则的img,然后就通过这个img去写入你的loader和mbr吧,记得改你bash脚本还有配置文件的img路径,改为当前这个img的路径,好的快去试试吧!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值