从零开始写一个操作系统内核 笔记(四) CPU 实模式到 保护模式

什么是保护模式?

原先的CPU 是不需要要保护模式就能访问的,但这样 程序 之间 随便访问,没有任何安全可言,要是一个程序崩溃了或者因为某些BUG跳转到了其他程序的底盘,直接就调用别的程序 的功能多牛逼 哈哈 某些Hack 也是利用这种思路,通过栈溢出直接就跳转到一指定地址执行一些不安全的代码。 而 保护模式在兼容实模式的情况下,CPU 可以通过读取 GDT 表 知道 哪一段范围是放程序,哪一段范围是放操作系统的,同时设置可读 可写等属性,ring0 ~ring3 权限等属性,如果 普通程序 是没法 直接操作 系统内存,直接改写 硬件内容的,这样大大保护了 系统的安全。
并且处理器在 执行代码的时候 也会有 不同的处理等级(Ring0、Ring1、Ring2、 Ring3 从大到小 Ring0 为最高等级),一般操作系统 跑在Ring0 特权级下(特权级低的不能访问特权级高的程序),普通应用程序 一般 跑在Ring3 特权级下。Linux内核目前只使用了 Ring0 和Ring3级。而等级1和等级2 介于内核程序与应用程序之间,它们通常作为系统服务程序来使用。保护模式不仅引入了权限,还引入了分页的功能。我们都知道内存的最小单位是字节,如果要执行一段程序 就要申请 N个字节,但是如果不定大小的向操作系统申请字节,在管理上比较麻烦。所以 就采用分页 规定多少多少字节为一个分页,这样 我要执行程序,只要计算出要多少分页,然后向操作系统申请就好了。

实模式下SS,CS,ES,SP和 保护模式下 的区别?

其实 保护模式下的选择子 和 实模式下 的 段寄存器 是一样的 , 增加了访问内存的能力,和 段之间访问的保护,在实模式下 段:偏移 能访问到计算机内存的全部1M空间 但在保护模式 通过一张表 来限定分配 实现了 不同程序 有单独隔离可访问的空间,原来好比 集体宿舍 大家在里面 打牌的 玩电脑的 xxx的 互相共用,现在 变成了单独隔间,共用一些基础设施。 而GDT 像是一张登记表 记录了 将 单人间 1 给张三 总统套房 给李四 这样 每一个房间 就是一个选择子,我要找张三 先找到张三的房间(选择子) 然后跟宿舍阿姨说(段寄存器) 阿姨就能帮你找到在房间里 XXX的张三了,选择子 还能限定 权限 住在 破烂单间 的 没法访问 住在总统套房的 。

所以GDT(Global Descriptor Table)全局描述符表 相当于一张登记表,上面写了 哪些 地址范围 用来干什么,拥有怎样的权限。

全局描述符表,占 8个字节,同时CPU 内部为了 能直接跟踪GDT ,所以内置了一个48位的寄存器,称为全局描述符表寄存器(GDTR)。

由2部分组成
1. 32位的线性地址
2. 16位边界

  • 32 位线性地址 可以访问到 2 32 2^{32} 232 也就是0x00000000 到0xFFFFFFFF 的 4GB 的内存,也只是GDT存放的其实地址。
  • 16 位保存的是全局描述符表的边界(界限),就是表示的 保存了多少个GDT表 一个G DT 占 用 8个字节 ,16 位的 可以表示的范围 为 2 16 2^{16} 216 字节= 64(KB) 64KB/8字节 =8192 个字节的范围
    上面说的 都是概念 让我们画张图 来便于更好的理解吧。
    在这里插入图片描述
    为什么GDT在 1M 以下空间呢(1M以下未被使用的空间),实际上 GDT 是可以在4G 内存空间中的任意位置的,但是由于 我们在加载GDT表时还处于1M的实模式下,所以被加载时内存在1M 以下空间。
    理论上 你可以在加载后重新定位GDT表。
GDT表栈8个字节 64位

GDT 表 格式:
在这里插入图片描述
Base:表示段基地址 (16~ 31 bit + 32~39 bit + 56 ~ 63 bit) = 16 + 8 + 8 = 32bit
Limit:表示断界限 (0~ 15 bit + 48 ~51 bit) =16 + 4 = 20bit
Flags:为 0时 段界限每一位表示 单位为字节,范围为 2 20 ∗ 1 2^{20} *1 2201Byte = 1M(表示范围:1B ~1M ),为1 时 段界限 以 4KB为单位 范围为 2 20 ∗ 4 2^{20} *4 2204 4Byte = 4G(此时范围为4KB~4G)。
在这里插入图片描述
AccessByte:由8个Bit 组成了各种权限的组合 S 为0 时 表示 是一个系统段,为1是表示代码段或者数据段。DPL 表示特权级 包含0~3 个特权级,0级为最高等级,刚进入保护模式是特权级为0. P段存在位,P指定所指定的段内存是否存在,如果你GDT指定了4个G的内存,但是实际并没有4G内存是 应该置为0,会触发一个中断异常使操作系统能把该段从硬盘加载到内存。并将P置为1,是虚拟内存使用的机制。
在这里插入图片描述
通过TYPE 可以设置数据段 还是代码段,可以限定 数据是否能被访问等。
上面的GDT表,定义的是不是很奇怪,Base Limit 都划分在不同的字节里了,他们之间不是连续的,之所以这么定义 是Intel处于 兼容 80826 CPU个是 留下历史的包袱。毕竟 你出的CPU要是不能兼容上一代那前面开发的程序就全部不能用了,这样 会丢失一定的市场占有率。

下面提供一个 我写了个脚本 来计算 段描述符 的表示:


a =0x4F9AFFFFFFFFFF # 段描述符 64位


binstr ='{:064b}'.format(a)
pre31bit = binstr[:32]
next31bit = binstr[32:]

_G = {"0":"1Byte","1":"4KB"}
_P = {"0":"当前段不在内存中存在","1":"当前短实际存在内存中"}
_DPL = {"00":"当前段特权级为 0(最高 可以访问其他所有特权级)","01":"当前段特权级为 1(可以访问 1 2 3 特权级)"
        ,"10":"当前段特权级为 2 (可以访问 2 3 特权级)","11":"当前段特权级为 3 只可以访问 3特权级"}

_TYPED = {"E":{"0":"向上拓展","1":"向下拓展(栈)"},"W":{"0":"只读","1":"可读/写"},"A":{"0":"这个段描述符没有被访问过(没有被选择子加载过)","1":"已访问过"}}
_TYPEC = {"C":{"0":"非一致代码段(Ring3 不能调用 Ring0)","1":"一致代码段(Ring3 能调用Ring0)"},"R":{"0":"仅执行","1":"执行/可读"},"A":{"0":"这个段描述符没有被访问过(没有被选择子加载过)","1":"已访问过"}}
_S = {"0":"数据段","1":"代码段"}
_DB = {"0":"16位的代码和数据段","1":"32位的代码和数据段"}
base3 =  pre31bit[0:8]
G = pre31bit[8]
DB = pre31bit[9]
L = pre31bit[10]
AVL = pre31bit[11]
P = pre31bit[16]
DPL = pre31bit[17:19]
S = pre31bit[19]
TYPE = pre31bit[20:24]
base2 = pre31bit[24:]
seglen2 = pre31bit[12:16]
base1 = next31bit[0:16]
segl1= next31bit[16:]


print("段界限:",hex(int(segl1 + seglen2,2)))
print("段基地址: ",hex(int(base3 +base2+base1,2)))
print("G 粒度:",_G[G])
print("L:",L)

print("S:",_S[S])
print("P(标志指出该段当前是否在内存中):",_P[P])
print("DB:",_DB[DB])
print("AVL:",AVL)
print("DPL(该段的特权级):",_DPL[DPL])
if S == "0":
        print("TYPE:", _TYPED["E"][TYPE[0]], _TYPED["W"][TYPE[1]], _TYPED["A"][TYPE[2]])
else:
        print("TYPE:", _TYPEC["C"][TYPE[0]], _TYPEC["R"][TYPE[1]], _TYPEC["A"][TYPE[2]])

假设段描述符 是 0x4F9AFF~FFFFFFFF 通过代入计算得到:
在这里插入图片描述

开启保护模式前的准备

原先8086处理器能访问1M 的空间,超过1M空间很多程序员为了炫技就充分利用这个特性,但随着CPU出现保护模式能力的提升 使得能访问超过1M以上的空间 ,但为了向下兼容以前的程序,为了解决这个问题。就出现了利用一个当时8042键盘的一个空闲的端口引脚,可以开启或关闭1M以上的空间。如果设置了A20引脚为低电平0,那么能访问的就只有20位(1M)的有效地址。但键盘访问太麻烦后来把这个端口集成到了CPU的A20(快速门)端口 直接控制CPU而不是访问键盘控制器,使用0x92端口 将第二位置 1 就可以开启。
开机时这个引脚是关闭的,

;段描述符 结构定义
;GDT_Descriptor Base,LIMIT,ATTR
;下面的 结构 通过声明 3 个 属性 然后
;CPU 会将 我们定义的 结构 转换成 GDTR寄存器格式
%macro  GDT_Descriptor	3 
	dw %2 & 0xffff		;段界限1 2字节
	dw %1 & 0xffff      ;段基址  2字节
	db (%1 >> 16 ) & 0ff;段基址  1字节
	dw ((%2 >> 8) & 0xf00) | (%3 & 0xf0ff);属性 1 + 段界限2 + 属性2 2字节
	db (%1 >> 24) & 0xff ;段基址 3 1字节
%endmacro

CODE_32 equ 0x4000
CODE_32_G	equ 0x8000
CODE_SEGMENT_32 EQU 0x9A
DATA_SEGMENT_32 EQU 0x92
开启保护模式前的黎明

开启保护模式前 还要对CR0操作,CR0是一个标志位,包含 一系列用于操控CPU运行模式和运行状态的标志位。 他的第一位,是保护模式允许位,要置为0 才能开启保护模式。

总结 开启保护模式 需要 3步:

  1. 定义GDT表 的属性 计算出 GDT表的位置指针 长度及选择子
; 描述符                        基地址        段界限       段属性
LABEL_GDT:      GDT_Descriptor      0,            0,          0             ; 空描述符,必须存在,不然CPU无法识别GDT
LABEL_DESC_CODE:  GDT_Descriptor    0,        0xfffff,    DA_32 | DA_CR | DA_LIMIT_4K ; 0~4G,32位可读代码段,粒度为4KB
LABEL_DESC_DATA:    GDT_Descriptor  0, 		  0xfffff,    DA_32 | DA_DRW | DA_LIMIT_4K; 0~4G,32位可读写数据段,粒度为4KB
LABEL_DESC_VIDEO:   GDT_Descriptor  0xb8000,  0xfffff,    DA_DRW | DA_DPL3            ; 视频段,特权级3(用户特权级)
; GDT全局描述符表 -------------------------------------------------------------
GDTLen              equ $ - LABEL_GDT                           ; GDT的长度
GDTPtr              dw GDTLen - 1                               ; GDT指针.段界限
                    dd LOADER_PHY_ADDR + LABEL_GDT              ; GDT指针.基地址 Loader程序被加载的 地址 0x9000:0000 + LABEL_GDT 开始偏移
; GDT选择子 ------------------------------------------------------------------
SelectorCode        equ LABEL_DESC_CODE - LABEL_GDT             ; 代码段选择子
SelectorData        equ LABEL_DESC_DATA - LABEL_GDT             ; 数据段选择子
SelectorVideo       equ LABEL_DESC_VIDEO - LABEL_GDT | SA_RPL3  ; 视频段选择子,特权级3(用户特权级)
; GDT选择子 ------------------------------------------------------------------

上面 其实 是初始化一张GDT表的 固定格式,上面 定义了 代码段 数据段 和 显卡内存访问的段 除了显卡缓存段 是Ring3 权限其他都是Ring0,并且 我们将代码段 和 数据段 都定义为了 从 0x00000 ~ 0xFFFFF 粒度 是 DA_LIMIT_4K (4KB) ,意味着 内存从0x00000 ~ 0xFFFFF * 4K / 1024 /1024 =3.999 4G 左右的空间。

GDTLen :计算的是你定义的 GDT表 的长度 通过当前地址 - GDT表起始地址 ($ - LABEL_GDT)计算 这是一种固定格式
GDTPtr: 定义了GDT的 长度和 起始地址。
上面代码 基本上是固定格式,除了GDT表定义那边.

CPU 加载 GDT 通过 lgdt 这条命令

	lgdt	[GDTPtr];获取GDT表的位置 并加载
  1. 开启A20快速门
    这里通过 0x92 端口,将 开启CPU的 第快速门,实现 开启保护模式的其中一步。
	;开启A20 快速门
	in      al,0x92
  	or 		al, 00000010b
  	out 	92h, al
  1. 设置CR0 第0 为 1
	mov 	eax,cr0
	or 		eax,0x1
	mov		cr0,eax

通过上面 3步 就可以 开启了 保护模式
其实还差一步:

jmp	代码段选择子:(要跳转到的程序 + 程序加载其实地址)

选择子 就是 我们 前面定义的 LABEL_DESC_DATA 然后 跳转到那段代码 如下 就是PM_Start

[SECTION .code32]
[BITS 32]
PM_Start:
  mov   ax, SelectorData
  mov   ds, ax
  mov   es, ax
  xxxxx....省略

如果 你的 Loader 程序 被加载 到了 0x1000:0x0000 那么 就加上 0x1000<< 4 + 0x0000
最后 就是 这样 :jmp SelectorCode:(PM_Start: + 0x10000)

在跳转前 还需要关闭 磁盘驱动让他停止读取,如果不关闭就一直会发出声音,虽然在我们的BOCHS下 关不关 是没什么区别的:

;关闭驱动器
KillMotor:
	push	dx
	mov		dx,03F2h
	mov		al,0
	out		dx,al
	pop		dx
	ret	

以上步骤 就是 开启保护模式 必须要干的事了啦。

loder_begin:
	mov		ax,cs
	mov 	es,ax
	mov 	ds,ax
	mov 	ss,ax
	mov 	sp,BaseOfStack
	;加载GDT
	lgdt	[GDTPtr]
	;关闭外部中断
	cli
	;开启A20 快速门
	in      al,0x92
  or 		al, 00000010b
  out 	92h, al
	;设置cr0 开启保护模式
	mov 	eax,cr0
	or 		eax,0x1
	mov		cr0,eax
  jmp 	dword SelectorCode:LOADER_PHY_ADDR + segment32
  jmp 	$
;关闭驱动器
KillMotor:
	push	dx
	mov		dx,03F2h
	mov		al,0
	out		dx,al
	pop		dx
	ret	
LoaderESAddress dw 0x7000
Sectoroffset   dw 00
Fatcluster dd 0x00
NoLoaderMessage:	db	"Can't find you kernal!"
SectorNo db 0x00
LoaderFileName: db	"KERNEL  BIN",0;寻找的 文件名 loader.bin
     msgProgress db ".", 0x00
     StartBootMessage:	db	"Start Loading Kernel.bin Please Waiting"
     absoluteSector db 0x00;S
     absoluteHead   db 0x00;H
     absoluteTrack  db 0x00;C
     datasector  dw 0x0000	

[SECTION .code32]
[BITS 32]
align 32
segment32:
  mov   ax, SelectorData
  mov   ds, ax
  mov   es, ax
  mov   fs, ax
  mov   ss, ax              ; ds = es = fs = ss = 数据段
  mov   esp, TopOfStack     ; 设置栈顶
  mov   ax, SelectorVideo
  mov   gs, ax              ; gs = 视频段
  mov   ebx,0x10;列偏移多少个字符
  mov   ecx,2;一个字符 2个字节 所以乘以2
  mov   esi,LoadMseeage
  call  showMSG
  jmp   $
showMSG:
  mov   edi,(80*11);屏幕中间位置
  add   edi,ebx;自带符偏移
  mov   eax,edi
  mul   ecx ;每个字符 2个字节 所以乘以2
  mov   edi,eax
  mov   ah,0xc
  mov   al,[esi];位置
  cmp   al,0
  je    fin
  inc   ebx
  inc   si
  mov   [gs:edi],ax ;写入屏幕
  jmp   showMSG
fin:
  HLT
  jmp   fin
[SECTION .data32]
[BITS 32]
align 32
DATA32:
_LoadMseeage: DB "Welcome To 32Bits Protect Model!(#^.^#)",0
BottomOfStack times 0x100 db 0
TopOfStack equ $ + LOADER_PHY_ADDR
;--------------------------------------
LoadMseeage  equ LOADER_PHY_ADDR + _LoadMseeage


打印 几个字符来表示能成功加载成功:
在这里插入图片描述
好了 到这里 我们 已经 进入了32 为的保护模式啦 。_
完整代码,请见GITHUB。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值