手写操作系统(七)-保护模式入门(一)

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

BIOS 负责从预设的启动设备(通常是硬盘、固态硬盘或光盘)中加载操作系统的引导程序,将控制权交给操作系统。

在IA32下,CPU有两种工作模式:实模式和保护模式。

1.实模式和保护模式

实模式下,CPU 使用 16 位的地址总线和寄存器,可以寻址最多 1MB 的内存空间(从地址 0x00000 到 0xFFFFF)。CPU 访问内存时,使用的是实际的物理地址,没有内存保护机制。内存寻址通过段地址和偏移地址的组合来实现。段地址左移 4 位后加上偏移地址得到物理地址。

实模式有很多不靠谱的地方,逐渐被保护模式淘汰:

  • 实模式下操作系统和用户程序属于同一特权级,操作系统可以做的,普通用户也可以自己做,不安全。
  • 用户程序所引用的地址都是指向真实的物理地址,也就是说逻辑地址等于物理地址。
  • 实模式下用户可以自由修改段基址,访问全部的内存。
  • 访问超过64KB的内存区域要切换段基址,运算频繁,效率低。
  • 一次只能运行一个程序,无法充分利用计算机资源。
  • 只有20条地址线,最大可寻址范围为1MB。

Intel 8086是16位的CPU,它有着16位的寄存器(Register)、16位的数据总线(Data Bus)以及20位的地址总线和1MB的寻址能力。

一个地址是由段和偏移两部分组成的,物理地址遵循这样的计算公式:

物理地址(Physical Address) = 段值(Segment) *  16 + 偏移(Offset)

其中,段值和偏移都是16位的。

 

2.寄存器扩展

80286开始使用保护模式,但其依然是16位的CPU,其通用寄存器还是16位宽。 但其与8086不同的是其地址线由20位变为了24位,即寻址空间变成了2的24次方,等于16MB大小。

从80386开始,Intel家族的CPU进入32位时代。80386有32位地址线,所以寻址空间可以达到4GB。

除段寄存器外,通用寄存器、指令指针寄存器、标志寄存器都由原来的16位扩展到了32位。段寄存器用16位就够用了。

经过extend扩展后的寄存器,统一在名字前加了e表示扩展。

左边已经标注名字的寄存器有通用寄存器组,名字前统一加了字符 E 表示扩展,同样,EFLAGS寄存器和EIP分别在FLAGS和IP基础上扩展而成。 图下边的6个段寄存器,依然是16位。 寄存器中低 16 位的部分是为了兼容实模式,可以单独使用。 高 16 位没办法单独使用,只能在用 32位寄存器时才有机会用到它们。

 

3.运行模式反转

编译器提供了伪指令bits,用它来向编译器传达指令都要编译成多少位的。 比如在实模式下,运行的指令都是16位的,所以编译器要将代码编译成16位的指令。 在实模式下准备好了保护模式所需要的环境后,进入保护模式后的代码就应该是 32 位指令。 也就是,同一段程序要经历两种模式,所以同一段程序中有两种模式的机器码。

bits的指令格式是bits 16 或 bits 32。

bits 16是告诉编译器,下面的代码帮我编译成 16 位的机器码。

bits 32是告诉编译器,下面的代码帮我编译成32位的机器码。

 

4.指令扩展

对于,add和sub指令

  • al,cl支持 8 位操作数
  • ax,сx 支持16位操作数;
  • eax,eсx支持32位操作数

mul指令是无符号数相乘指令,指令格式是mul 寄存器/内存。其中“寄存器/内存”是乘数。

  • 如果乘数是8位,则把寄存器al当作另一个乘数,结果便是16位,存入寄存器ax
  • 如果乘数是16位,则把寄存器ax当作另一个乘数,结果便是32位,存入寄存器eax
  • 如果乘数是32位,则把寄存器eax当作另一个乘数,结果便是64位,存入edx: eax,其中edx是积的高32位,eax是积的低32位。

有符号数相乘指令imul也是一样,不再说明。

对于无符号数除法指令div,其格式是div 寄存器/内存,其中的“寄存器/内存”是除法计算中的除数。

  • 如果除数是8 位,被除数就是16位,位于寄存器ax。 所得的结果,商在寄存器al,余数在寄存器ah
  • 如果除数是16位,被除数就是32位,被除数的高16位则位于寄存器dx,被除数的低16位则位于寄存器ax。 所得的结果,商在寄存器ax,余数在寄存器dx。
  • 如果除数是32位,被除数就是64位,被除数的高32位则位于寄存器edx,被除数的低32位则位于寄存器eax,所得的结果,商在寄存器eax,余数在寄存器 edx。

对于push指令

  • 在实模式环境下:当压入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。

 

5.段描述符

在实模式下,16位的寄存器需要用“段:偏移”这种方法才能达到1MB的寻址能力。

段寄存器 CS、 DS、 ES、 FS、 GS、 SS,在实模式下时,段中存储的是段基地址,即内存段的起始地址。

而在保护模式运行方式下,段寄存器中存放的不再是寻址段的基地址,而是一个段描述符表中某项的索引值,里面保存的内容也叫“选择子”,selector。索引值指定的段描述符项中含有需要寻址的内存段的基地址、段的最大长度值和段的访问级别等信息。寻址的内存位置是由该段描述符项中指定的段基地址值和偏移值组合而成,段的最大长度也由描述符指定。可见,和实模式下的寻址相比,段寄存器值换成了段描述符索引,但偏移值还是原实模式下的概念。这样,在保护模式下寻址一个内存地址就需要比实模式下多一道手续,也即需要使用段描述符表。

所以在保护模式下,虽然段值仍然由原来16位的cs、ds等寄存器表示,但此时它仅仅变成了一个索引,这个索引指向一个数据结构的一个表项,表项中详细定义了段的起始地址、界限、属性等内容。这个数据结构,就是GDT。GDT中的表项也有一个专门的名字,叫做描述符(Descriptor)。

下图所示的是描述符的结构。

 

保护模式下地址总线宽度是32位,段基址需要用32位地址来表示。

段界限

为了限制程序访问内存的范围,还要对段大小进行约束,所以要有段界限属性。

段界限表示段边界的扩展最值,即最大扩展到多少或最小扩展到多少。扩展方向只有上下两种。对于数据段和代码段,段的扩展方向是向上,即地址越来越高,此时的段界限用来表示段内偏移的最大值。对于栈段,段的扩展方向是向下,即地址越来越低,此时的段界限用来表示段内偏移的最小值。无论是向上扩展,还是向下扩展,段界限的作用如同其名,表示段的边界、大小、范围。段界限用20个二进制位来表示。

只不过此段界限只是个单位量,它的单位要么是字节,要么是4KB,这是由描述符中的G位来指定的。最终段的边界是此段界限值*单位,故段的大小要么是 2 的20 次方等于1MB,要么是 2 的 32 次方(4KB等于2的12次方,12+20=32)等于4GB,上面所说的1MB和4GB只是个范围,并不是具体的边界值。由于段界限只是个偏移量,是从0算起的,

所以实际的段界限边界值=(描述符中段界限+1)* (段界限的粒度大小: 4KB 或者 1) -1。

这个公式很简单,就是表示有多少个4KB或1。由于描述符中的段界限是从0起的,所以左边第1个括号中要加个1,表示4KB或1的实际数量。它与第二个括号中的段粒度大小相乘后得到的乘积是以1为起始的段的实际大小。由于地址是以 0 为起始的,所以公式的最后又减了1。

内存访问需要用到“段基址:段内偏移地址”,段界限其实是用来限制段内偏移地址的,段内偏移地址必须位于段的范围之内,否则CPU会抛异常。根据段的扩展方向,此“段界限*单位”便是段内偏移地址的最大值(向上扩展)或最小值(向下扩展),任何超过此值的偏移地址都被认为是非法访问,CPU会将此错误捕获。

CPU硬件负责检测,但检测到错误后,CPU 会触发相应的异常,就需要写相应的异常处理程序。

20 位的段界限属性会被拆分成两部分。段界限的低 16 位(0~15 位)存放在段描述符的低32位,段界限的高4位(16~19位)存放在段描述符的高32位。

段基址也是, 32位的段基址被分拆成三份存放。

80286是第一款具有保护模式的CPU,当时就已经采用段描述符来描述内存段信息了。只是当时它是16位的保护模式,地址总线是24位,最多访问16MB内存,所以不得不在原有 80286 的段描述符上做扩展,所以才有了今天这样奇形怪状的段描述符。

因为段信息会被CPU缓存到段描述符缓冲寄存器中,此缓冲寄存器中的内容便是段描述符中的内容,它是经过CPU整理后的,段界限和段基址已经被拼合到一起,CPU下次会自动到段描述符缓冲寄存器中取段数据。不会影响CPU获取段信息的效率。

 

G字段

如果G位为0,表示段界限粒度大小为1字节,段界限实际大小就等于描述符中的段界限值。

如果 G 位为 1,表示段界限粒度大小为4KB 字节

举个例子,如果是平坦模型,段界限为0xFFFFF,G位为1,

套用上面公式,段界限边界值=0x100000*0x1000-1=0xFFFFFFFF

 

type字段和S字段

8~11 位是 type 字段,共 4 位,用来指定本描述符的类型。

段描述符,在CPU眼里分为两大类,系统段或数据段,这是由段描述符中的s位决定的,用它指示是否是系统段。在CPU眼里,凡是硬件运行需要用到的东西都可称之为系统,凡是软件(操作系统也属于软件, CPU眼中,它与用户程序无区别)需要的东西都称为数据,无论是代码,还是数据,甚至包括栈,它们都作为硬件的输入,都是给硬件的数据而已,所以代码段在段描述符中也属于数据段(非系统段)。S 为 0 时表示系统段,S 为1 时表示数据段。

type字段是要和S字段配合在一起才能确定段描述符的确切类型,只有 S 字段的值确定后,type字段的值才有具体意义。

各种称为“门”的结构便是系统段,也就是硬件系统需要的结构,非软件使用的,如调用门、任务门。

这里主要是关注S为1时,非系统段的type子类型。

type字段共4位,用于表示内存段或门的子类型。

 

目前主要关注非系统段

A位表示Accessed位,这是由CPU来设置的,每当该段被CPU访问过后,CPU就将此位置设置1,所以,创建一个新段描述符时,应该将此位置 0。 我们在调试时,根据此位便能判断该描述符是否可用。

C表示一致性代码段,也称为依从代码段, Conforming。 一致性代码段是指如果自己是转移的目标段,并且自己是一致性代码段,自己的特权级一定要高于当前特权级,转移后的特权级不与自己的DPL为主,而是与转移前的低特权级一致,也就是听从、依从转移前的低特权级。 C为1时则表示该段是一致性代码段,C为0时则表示该段为非一致性代码段。

R 表示可读,R为1 表示可读,R为 0 表示不可读。 这个属性一般用来限制代码段的访问。 如果指令执行过程中,CPU发现某些指令对R为0的段进行访问,如使用段超越前缀CS来访问代码段,CPU将抛出异常。 不过不可读的代码段只是来限制代码指令的,并不是连CPU也不能看。

X 表示该段是否可执行,EXecutable。 我们所说的指令和数据,在 CPU 眼中是没有任何区别的,都是010101这样类似的二进制。所以要用type中的X位来标识出是否是可执行的代码。代码段是可执行的,即X为1。而数据段是不可执行的,即X为0.

E是用来标识段的扩展方向,Extend。E为0表示向上扩展,即地址越来越高,通常用于代码段和数据段。E为1表示向下扩展,,地址越来越低,通常用于栈段。

w是指段是否可写,Writable。W为1表示可写,通常用于数据段。W为0表示不可写入,通常用于代码段。对于W为0的段有写入行为,同样会引发CPU抛出异常。

 

DPL字段

段描述符的第13~14位是DPL字段, Descriptor Privilege Level,即描述符特权级,这是保护模式提供的安全解决方案,将计算机世界按权力划分成不同等级,每一种等级称为一种特权级。由于段描述符用来描述一个内存段或一段代码的情况(若描述符类型为“门”),所以描述符中的DPL是指所代表的内存段的特权级。

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

 

P字段

段描述符的第15位是P字段,Present,即段是否存在。如果段存在于内存中,P为1,否则P为0。

P字段是由CPU来检查的,如果为0, CPU将抛出异常,转到相应的异常处理程序,在异常处理程序处理完成后要将P置1。也就是说,对于P字段,CPU只负责检查,我们负责赋值。不过在通常情况下,段都是在内存中的。

当初CPU的设计是当内存不足时,可以将段描述符中对应的内存段换出,也就是可以把不常用的段直接换出到硬盘,待使用时再加载进来。但现在即使内存不足时,也没有将整个段都换出去的,现在基本都是平坦模型,一般情况下,段都要4GB大小,换到硬盘也是很占空间。

所以这些是未开启分页时的解决方案,保护模式下有分页功能,可以按页(4KB)的单位来将内存换入换出。

 

AVL字段

段描述符的第20位为AVL字段,从名字上看它是AVaiLable,可用的。不过这“可用的”是对用户来说的,也就是操作系统可以随意用此位。对硬件来说,它没有专门的用途。

 

L字段

段描述符的第21位为L字段,用来设置是否是64位代码段。L为1表示64位代码段,否则表示32位代码段。这目前属于保留位,在我们 32 位 CPU下编程,将其置为 0 便可。

 

D/B字段

段描述符的第22位是D/B字段,用来指示有效地址(段内偏移地址)及操作数的大小。也就是为了兼容 286 的保护模式,286的保护模式下的操作数是16位。

既然是指定“操作数”的大小,也就是对“指令”来说的,与指令相关的内存段是代码段和栈段,所以此字段是D或B。

对于代码段来说,此位是 D 位;若 D 为 0,表示指令中的有效地址和操作数是 16 位,指令有效地址用IP寄存器。若D为1,表示指令中的有效地址及操作数是32位,指令有效地址用EIP寄存器。

对于栈段来说,此位是B位,用来指定操作数大小,此操作数涉及到栈指针寄存器的选择及栈的地址上限。若 B 为 0,使用的是 sp 寄存器,也就是栈的起始地址是 16 位寄存器的最大寻址范围0xFFFF,若B为1,使用的是 esp 寄存器,也就是栈的起始地址是 32位寄存器的最大寻址范围0xFFFFFFFF。

 

G字段

段描述符的第23位是G字段,Granularity,粒度,用来指定段界限的单位大小。所以此位是用来配合段界限的,它与段界限一起来决定段的大小。

若G为0,表示段界限的单位是1字节,这样段最大是2的20次方*1字节,即1MB。

若G为1,表示段界限的单位是4KB,这样段最大是2的20次方*4KB字节,即4GB。

 

 

6.参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

CPU的保护模式与实模式的区别

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇