[4.1] 操作系统学习 (保护模式)

·

2 min read

本节我们将会学习保护模式

保护模式是怎么来的?

我们都知道实模式下存在很多安全问题, 这主要表现在:

  • 操作系统和用户程序的特权级是一样的 (坏处: 用户程序想干嘛就干嘛, 操作系统根本管不了)
  • 用户程序所使用的地址都是会指向真正的物理地址, 用户程序能读取任意内存 (坏处: 用户程序能随便读取和修改内存, 造成系统不稳定)
  • 一次只能运行一个程序 (坏处: 浪费资源)
  • 只有 20 条地址总线, 寻址范围小 (坏处: 只能范围 1MB 内存)

为了解决这个问题工程师发明了 保护模式

其实在保护模式前还有一个模式叫 虚拟 8086 模式 , 这是一个用于过渡的模式, 较少使用, 所以我们不学习

保护模式和实模式的不同

寄存器

CPU 发展到 32 位后, 地址总线和数据总线都发展到 32位, 而寄存器为了适应这一变化也 发展到 32 位.

为了适应以前的实模式, 寄存器是从原来的 16 位扩展到 32 位的

1.png

2.png

因为现在寄存器是 32 位的了, 所以它的寻址范围达到了 4GB, 我们再也不用*16再访问了

现在就算我们的段寄存器是 0, 也可以访问到全部的 4GB 内存.

不过现在我们解决了寻址问题, 还没有解决安全问题呢.

其实我们在保护模式中, 段寄存器里面储存的并不是段基址, 而是一个叫 选择子(selector), 这其实是全局描述符表(GDT)里面的东西.

我们这里先不讲 GDT, 不过我们还是要和大家说一下什么是选择子

选择子其实就是个数, 用这个数来索引GDT中的段描述符. 如果把GDT当成数组, 选择子就像数组下标一样

不过一个事实是 GDT 的访问是很耗费时间的, 所以为了提高CPU 获取段信息的效率, 工程师用了一个新技术--缓存, CPU 把每次获取的段信息储存在一个叫段描述符缓冲寄存器(DCR)里, 以后每次访问相同的段时, 就可以直接读取 DCR, 且对于 DCR 的失效时间, 只要每引用一个段, DCR 就会被刷新

还有一点, 我们再实模式下, 如果运行这代码mov ax,[dx],是会报错的, 因为实模式下的寄存器的用途都是固定的, 而保护模式下, 我们想要把寄存器用到哪里, 都是允许的

寻址

进入保护模式, 我们的寻址范围就可以达到 4GB 之大

寻址方式自然也会发生改变

实模式下内存寻址:

{ 基址寄存器(BX,BP) }+{ 变址寄存器(SI,DI) }+{ 16位的偏移量(立即数) }

保护模式下内存寻址:

{ 基址寄存器(all) }+{ 变址寄存器(all) }*{ 比例因子(1,2,4,8) }+{ 32位的偏移量(立即数) }

值得注意的是比例因子是可有可无的, 而只能为1,2,4,8

具体使用:

mov eax,[ecx*4+0x1234]
mov eax,[eax+edx*2+0x8]
mov eax,[eax+edx*8+0x12345678]

运行模式反转

以前的工程师为了适应实模式和保护模式, 指令格式总是要一样

指令格式前缀 | 操作码 | 寻址方式,操作数类型 | 立即数 | 偏移量

一些东西在 16 位 CPU 和 32位 CPU 中是截然不同的东西, 假设在实模式下 dx 是 010, 而在保护模式下 edx 也是 010

这就要有一个东西来区分这些东西, nasm 为我们提供了 bits 指令(格式[bits 16]或[bits 32]或[bits xx]

  • [bits 16]是告诉编译器, 下面的代码帮我编译成 16 位的机器码
  • [bits 32]是告诉编译器, 下面的代码帮我编译成 32 位的机器码

如果我们的 CPU 是 32 位的, 那在实模式下要怎么使用 eax 等寄存器呢, 我们可以使用 0x66(操作数反转前缀)

例如:

[bits 16)
mov ax, 0x1234 
mov eax, 0x1234
[bits 32]
mov ax, 0x1234 
mov eax, 0x1234

编译后:

指令机器码
[bits 16]
mov ax, 0x1234B83412
mov eax, 0x123466B834120000
[bits 32]
mov ax, 0x123466B83412
mov eax, 0x1234B834120000

从中可以看出但我们再使用不同模式下的操作数时, 会在前缀上加上 0x66

除了操作数,那寻址方式呢? 我们可以使用 0x67

[bits 16]
mov word [bx], 0x1234
mov word [eax], 0x1234
mov dword [eax], 0x1234
[bits 32]
mov dword [eax], 0x1234
mov word [eax], 0x1234
mov dword [bx], 0x1234

编译后:

指令机器码
[bits 16]
mov word [bx], 0x1234C7073412
mov word [eax], 0x123467C7003412
mov dword [eax], 0x12346667C70034120000
[bits 32]
mov dword [eax], 0x1234C70034120000
mov word [eax], 0x123466C7003412
mov dword [bx], 0x123467C70734120000

从中可以看出但我们再使用不同模式下的寻址方式时, 会在前缀上加上 0x67

进入保护模式

进入保护模式是有固定的方式的

  • 打开 A20 地址线
  • 打开 CR0 寄存器里面的 Protection Enable

值得注意的是这些操作的顺序是可以颠倒的

打开 A20 地址线

我们再 0 章时讲过 实模式下的地址回绕 , 再实模式下我们使用'段基址:偏移'的方式可以访问到0x10ffef, 这就超过了A20 地址线的寻址访问, CPU 会自动绕回 0地址

后来的 32 位 CPU 都有了 A24 地址线, 这意味着寻址范围达到了 4GB, 由于实模式下会地址回绕, 但是保护模式不会, 这样如果是 32 位 CPU 运行实模式, 就会出现一些奇奇怪怪的问题, IBM 为了解决这个问题在键盘控制器上的一些输出线来控制第 21 根地址线( A20)的有效性, 故被称为 A20Gate

  • A20Gate 打开:访问超出 1MB 的地址时, CPU 会真正访问
  • A20Gate 关闭:访问超出 1MB 的地址时, CPU 会采用地址回绕

打开 A20Gate 的方法也是十分简单,只要在 0x92 端口第 1 位写入 1 即可(0 关闭)

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

寄存器 CR0 的 PE 位

CR系列寄存器是控制寄存器, 控制寄存器是 CPU 的窗口, 既可以用来展示 CPU 的内部状态, 也可用于控制 CPU 的运行机制.

我们先看一下 CR0 的结构:

3.png

4.png

我们只要在 PE 位写入1 就可以进入保护模式了(0 进入实模式)

mov eax,cr0
or eax,0x00000001
mov cr0,eax

这次保护模式并没有全部讲完, 还有一个 GDT 没讲, 下次会详细讲