从零开始写一个操作系统内核 笔记(四) 番外篇 8086CPU 之保护模式

在这里插入图片描述

在这里插入图片描述

操作系统内核加载过程

在这里插入图片描述上图是 linux 0.1.2 应到程序加载操作系统的步骤。

在实模式下的1M 空间下,BIOS启动后经过一系列自检(CPU、内存、硬盘等)后,将控制权 交给主引导记录MBR(Main Boot Record)位于硬盘的第0盘0道1扇区(LBA=0)的512个字节,BIOS事先规定将代码执行权移交到了0x7c00处地址,所以需要将MBR读写到0x7c00处,由于MBR只有 512个字节如果你想一步上天是不可能的 512 个字节能做的事情是非常有限的,所以 MBR能做的 就只是从磁盘上 读取次 下一个代替MBR完成任务的能递交接力棒的程序称为OBR(OS Boot Record)操作系统引导记录,如果这个程序占 7个字节 那么MBR就要读取 14个扇区 然后将其加载到指定内存储并且jmp跳转,将代码执行权交给它,在Linux 0.12中 这个OS加载程序 被加载到了 0x9000:0000处这块内存,从上图可以看到0x90000没有被其他程序占用,引导程序主要功能是 加载kernel内核程序,检查内存 显卡等一系列硬件参数存放起来,以便后面操作系统内核使用。设GBTR表进入保护模式,最后跳转到 内核起始处。

段描述符缓冲寄存器

在这里插入图片描述
在80286 之后 ,保护模式下 当 段寄存器 被赋予 16位的选择子
mov ds,selector
段描述符缓冲器就会 从 IGDR中取到 对应 选择子的 base limit 等 加载到段描述缓冲器,直到下一次段描述符缓冲器,再次被赋值。

段描述符缓冲器实际包含 96 位 但是可见能操作的只有16位,
其中32 位Lmit表示段的限长.
32位 Base表示段的起始地址
16 位 Selector表示段的选择子 (可见部分)

可以看到 在80286 处理器下 base 和Limit 都是 0~23 可以访问 2的23次方 也就是 16M的内存,但是 由于 寻址方式还是通过 段基址 << 4 + 偏移 能访问到的还是20 位,所以在80286 下 一个段 你还是只能访问 64 KB 的空间,然后你要 换下一个段 就该改变段寄存器,不能只通过一个段寄存器 就能访问全部16M的空间,感觉有点不爽,所以在 以后的 80386 机器上 将地址总线改成32根 在32位下, 一个寄存器 拥有了 32位 2^32次方 能表示 完整的4G空间。
在这里插入图片描述

保护模式后下的段选择子

在这里插入图片描述
RPL :请求特权级别(00 01 10 11)四个取值 对应Ring 0 ~ Ring3
TI: 为0 查GDT表 为1 查LDT表
Index: 处理器会乘以8 在IDT或者GDT中查找相应的段描述符。
这里 我们 举个例子 :
45 转到2进制 01000 1 01
得到:
RPL:1(权限 Ring33为最低 Ring30 能访问Ring3 1、2、3 Ring3只能访问Ring33)
TI:1
Index:8

进入32位保护模式,段寄存器 CS 、DS、 ES、 FS、 GS、 SS ,它们还是16位的,但是不再存着所谓的段地址了,而是成为段选择器,里面存着段选择子,段选择子里面有描述符索引,根据这个索引去位于内存的GDT(Global Descriptor Table 全局描述符表)里找真正的段基地址(线性地址);
GDT有很多描述符:代码段描述符、数据段描述符、栈段描述符;
在GDT里,每个描述符,占用8个字节单元;
索引号 乘以 8 得到要找的描述符在GDT表内的偏移地址;
在这里插入图片描述

总结:在保护模式下,每个段寄存器 包含96位 ,16位为选择子,通过选择子的索引CPU会获取GDT表或者IDT表的段描述信息。

段描述符格式

段描述符,是由 64Bit 也就是 8个字节 构成的,分为 前 32位 和 后 32位。
一般情况下,规定用途的属性一般在后32位 所以我们不需要去看 前32位。
在这里插入图片描述

某描述符是64位的0x4F9AFFFFFFFFFF,请问,段基地址是多少?段界限是多少?G、D、L、AVL、P、DPL、S和TYPE各是多少?

首先 分成前 32 位和 后32位,0x4F9AFF 0xFFFFFFFF

  1. D/B位(如果该段是代码段我们称为B 如果该段是数据段我们称为D):用来表示 段以 16位 或者 32位 操作,如果D/B 为 0 则段的limit 寻址方式为 16位,limit最大FFFFH 64KB大小,当指定为 1是 最大FFFFFFFF = 4GB的寻址空间。
  2. 数据段 TYPE 的 E位 可以设置向上向下扩展 向上扩展 就是从内存高位到内存低位,0~0xFFFFFFFF(如果DB位设置为 0 则只能寻址64K) 计算方式为 :Base ~Limit (base > limit) ,而向下 扩展 是从 高地址到低地址, base < limit 此时 limit 作为起始地址,以下的部分 为可用部分B为 0或1 做限长度, base > limit 用 limit + base 做起始地址,B为 0或1 做限长度。
  3. P位表示当前段 是否在内存中。
  4. G 表示代码段粒度,1为4K 0为 1字节。同样 0xFFFFF 地址 1字节 就能表示 1M 空间,4K 就能表示4G。
  5. A 表示 当前段描述符 是不是被加载过了。

下面提供我写了个 提取段描述符信息的脚本:


a =0x00cff70000000000 # 段描述符 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:]


limit = hex(int(segl1 + seglen2,2))
base =hex(int(base3 +base2+base1,2))
print("段界限:",limit)
print("段基地址: ",base)
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]])
段权限检查

CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。
     RPL说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限,有点像函数参数。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。
    DPL存储在段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。
当进程访问一个段时,需要进程特权级检查,一般要求DPL <= max {CPL, RPL}
    下面打一个比方,中国官员分为6级国家主席1、总理2、省长3、市长4、县长5、乡长6,假设我是当前进程,级别总理(CPL=2),我去聊城市(DPL=4)考察(呵呵),我用省长的级别(RPL=3 这样也能吓死他们:-))去访问,可以吧,如果我用县长的级别,人家就不理咱了(你看看电视上的微服私访,呵呵),明白了吧!为什么采用RPL,是考虑到安全的问题,就好像你明明对一个文件用有写权限,为什么用只读打开它呢,还不是为了安全!
举个栗子:
mov ax,0x20 此时选择子 是 0x20 转成二进制 100 0 00
RPL:0 TI:0(0为查GDT表) GDTindex = 100
mov ds,ax 如果要正确执行 那么当前CS代码段 的权限 要 大于等于 RPL的权限
也就是说 CPL >= RPL ,如果不满足CPU是无法通过运行的。
如果 ds = ax,如果 ds 已经等于 ax了 不做任何操作,
上面检测通过 CPL >= RPL 此时通过选择子在 GDT表内 查找 并且此时 要求 GDT的DPL <= RPL 才能正确被加载,写入缓存描述符表的96位中。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值