[4.1] 操作系统学习 (保护模式)
本节我们将会学习保护模式
保护模式是怎么来的?
我们都知道实模式下存在很多安全问题, 这主要表现在:
- 操作系统和用户程序的特权级是一样的 (坏处: 用户程序想干嘛就干嘛, 操作系统根本管不了)
- 用户程序所使用的地址都是会指向真正的物理地址, 用户程序能读取任意内存 (坏处: 用户程序能随便读取和修改内存, 造成系统不稳定)
- 一次只能运行一个程序 (坏处: 浪费资源)
- 只有 20 条地址总线, 寻址范围小 (坏处: 只能范围 1MB 内存)
为了解决这个问题工程师发明了 保护模式
其实在保护模式前还有一个模式叫 虚拟 8086 模式 , 这是一个用于过渡的模式, 较少使用, 所以我们不学习
保护模式和实模式的不同
寄存器
CPU 发展到 32 位后, 地址总线和数据总线都发展到 32位, 而寄存器为了适应这一变化也 发展到 32 位.
为了适应以前的实模式, 寄存器是从原来的 16 位扩展到 32 位的
因为现在寄存器是 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, 0x1234 | B83412 |
mov eax, 0x1234 | 66B834120000 |
[bits 32] | |
mov ax, 0x1234 | 66B83412 |
mov eax, 0x1234 | B834120000 |
从中可以看出但我们再使用不同模式下的操作数时, 会在前缀上加上 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], 0x1234 | C7073412 |
mov word [eax], 0x1234 | 67C7003412 |
mov dword [eax], 0x1234 | 6667C70034120000 |
[bits 32] | |
mov dword [eax], 0x1234 | C70034120000 |
mov word [eax], 0x1234 | 66C7003412 |
mov dword [bx], 0x1234 | 67C70734120000 |
从中可以看出但我们再使用不同模式下的寻址方式时, 会在前缀上加上 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 的结构:
我们只要在 PE 位写入1 就可以进入保护模式了(0 进入实模式)
mov eax,cr0
or eax,0x00000001
mov cr0,eax
这次保护模式并没有全部讲完, 还有一个 GDT 没讲, 下次会详细讲