【保护模式】1.初探保护模式与分段机制
# 保护模式
- 保护模式是在硬件层面提供的 CPU 运行机制,是现代操作系统的根本。
- 没有保护模式,操作系统是没有安全性可言的。
# 何为保护
-
指令是存放在内存中的
-
假定你的程序试图对操作系统的关键代码进行破坏:
mov byte ptr ds:[kernel], 0x90
-
操作系统应该如何制止?
-
也许这个时候有同学发炎了:
- “也许操作系统它能监控呢?”
-
答案是,监控你程序的并不是操作系统,这种工作必须在硬件层完成
- 很简单的道理,实际上运行指令的是 CPU,只有 CPU 知道当前 CPU 正在做什么事,操作系统是不可能知道的
- 操作系统也是由 CPU 运行的指令序列。
- 除非 CPU 提供了一种回调机制,运行任何指令都先运行操作系统的指令,但这是不现实的,对性能影响过于严重
- 实现一套虚拟机也可以保证安全性,但是依旧存在性能问题,也不在本文讨论范围之内。
- 很简单的道理,实际上运行指令的是 CPU,只有 CPU 知道当前 CPU 正在做什么事,操作系统是不可能知道的
# 模式之分
-
我们得到了结论,保证 操作系统的安全性 这种机制,必须是硬件提供的。
- 在早期,CPU 并未提供这种保护模式,为此才划分出了 “实模式” 与 “保护模式”
- 在保护模式出现时,为了兼容,因此也诞生了 “虚拟 8086 模式”,但已经不重要了,也不在本文讨论范围内。
-
在学习保护模式时也需要牢记,保护模式是硬件层的东西,切勿与操作系统混淆。
# 物理内存
- 我们都知道,我们可见的物理内存,其实就是连续的、对每一个单元进行了地址编号的很大的存储器。
地址 | 数据 |
---|---|
0x00 | 0xff |
0x01 | 0x00 |
… | … |
表格仅为举例,与真实物理内存布局无关
-
程序必须是存放在内存中,才能被 CPU 取指执行。
-
假定你是操作系统 (给你管理硬件资源),那么多个程序又如何存放比较好呢?
# 多个程序的安置
- 可能有同学回答了:
- 我顺序加载嘛,第一个程序从哪里占用到哪里,第二个程序从哪里占用到哪里
地址 | 程序 |
---|---|
0x00 ~ 0x7f | 提供给程序 A |
0x80 ~ 0xff | 提供给程序 B |
… | … |
-
如表格所述,我们成功将两个程序分别放到了不同的位置。
-
这样子他们就互不干扰了,我们真是个小天才。
# 问题仍在
-
很遗憾,这样子的程序,运行依旧存在困难
- 在编写程序的时候,无法预知程序在运行时究竟会被加载到内存的哪个位置。
- 假设 B 选择
0x40
这个地址存放一些数据,那不是正好破坏了 A 的指令序列?
-
为此,内存分段诞生了
# 内存分段
-
如果我们在编写程序的时候,任何使用内存的指令,填写的内存地址,都是一个偏移值,让 CPU 替我们去与基地址相加,最终得到真正的物理地址。
-
那么不管我们的程序被加载到内存的哪个位置,只要提供一个正确的基地址,就可以让多个程序互不干扰了!
# 示例
地址 | 程序 | 基地址 |
---|---|---|
0x00 ~ 0x7f | 提供给程序 A | 0x00 |
0x80 ~ 0xff | 提供给程序 B | 0x80 |
… | … | … |
-
假设 B 选择
0x40
这个地址存放一些数据,我们还会破坏 A 的指令序列吗? -
这样看上去,是不是像给内存分段了一样?
0x00 ~ 0x7f
看作一段,分给程序 A0x80 ~ 0xff
看作一段,分给程序 B
实模式下的分段机制大抵如此。
# 段寄存器
-
想来各位读者对寄存器都有所了解,既然我们需要一个容器来存放段的基地址,寄存器自然是不错的选择。
-
但是通用寄存器本身数量也不够多,再想腾出来存放段基址,也是心有余而力不足了。
-
因此,理所当然的,就有了以段为名的段寄存器。
# ds 寄存器
- ds,即 data segment,意为数据段
- 咱们一看就知道,这个东西就是和数据、段 相关的。
- 它也十分简单,16 位的宽度,作用就是存放数据段的基址
注解
实际在实模式下,物理地址的转换公式略微复杂一些
(ds << 4) + address = 最终的物理地址
# 结合先前的示例
地址 | 程序 | 基地址 |
---|---|---|
0x00 ~ 0x7f | 提供给程序 A | 0x00 |
0x80 ~ 0xff | 提供给程序 B | 0x80 |
… | … | … |
- 我们选择让 B 在
0x40
这个位置存放一些数据 - 而现在我们有了 ds 寄存器,只要在程序运行前初始化 ds 寄存器,就可以做到不破坏 A 的程序了。
# 根据段的用途进行划分
- 但是这样子明显我们还需要小心翼翼,毕竟虽然不会破坏其他程序了,但不代表不会破坏自己的指令序列啊!
- 代码和数据都放在一个段里,是不是不便管理?
- 如果我们对代码和数据再做进一步的划分,是不是更好?
地址 | 程序 | 基地址 |
---|---|---|
0x00 ~ 0x3f | 提供给程序 A 存放数据 | 0x00 |
0x40 ~ 0x7f | 提供给程序 A 存放代码 | 0x40 |
0x80 ~ 0xcf | 提供给程序 B 存放数据 | 0x80 |
0xd0 ~ 0xff | 提供给程序 B 存放代码 | 0xd0 |
… | … | … |
-
为此,自然是可以存在更多的段寄存器
- cs(code segment)
- ss(stack segment)
- ds(data segment)
- …
-
还是先前的问题,程序 B 试图在
0x40
这个地址存放数据,我们只需要让 cpu 知道,数据段基址是0x80
0x80 + 0x40 = 0xc0
-
我们只需要划分好各个段,就可以很好的让程序工作了!
# CPU 对内存的访问
- 为此,cpu 也被设计为,在访问任何汇编指令中显式书写的内存地址时,都会先根据用途选择段寄存器,得到段基地址,指令中的地址视作偏移 (逻辑地址),运算后得到真正的物理地址,再进行访问。
注解
读写内存,可以划分为对数据段的访问
执行指令,可以划分为对代码段的访问
…
# 初探基于段的保护
-
在实模式下,对于任何存在于内存中的指令序列,cpu 是一视同仁的,ip 指哪它跑哪,埋头苦干。
-
操作系统将我们的程序加载到内存,使得 cs:ip 指向我们程序的入口点之后,我们想干什么,就不是操作系统能说了算了。
- 当然我们也可以反手来一波背刺,捅死操作系统。
# 尝试分段
-
你可能想到了,啊,我们先前不是才讲过,分段不就好了吗?
-
其实所谓的分段,也是建立在大家都规规矩矩,和睦相处的情况下,才能最大程度上避免 "不小心" 出现的问题。
- 简单地说,全看编写程序的人自觉不自觉。
- 既然操作系统能修改 cs、ds 段寄存器,我们有何不可呢?
# 权限的建立
-
首先,咱们既然要保证安全,那自然就要划分哪些是应用能做的,哪些是应用不能做的。
-
其次,总归要有程序去管理软硬件的,操作系统的不受限制也理所应当了。
# 权限划分
-
既然有了不能做和能做之分,并且还有了 "区别对待",权限自然也就建立起来了。
-
至此,咱们初步确定了权限的划分。
- 常规应用,拥有部分权限;
- 操作系统,拥有所有权限。
# 段机制的延展
-
咱们最先想到的,最不能让应用去乱搞的是什么呢?
-
首先就是不能让应用去破坏咱们的操作系统,咱们首先要把自己保护起来。
- 如果应用能随意修改操作系统的指令序列,那么所谓的保护将毫无意义。
# 初步构想
-
我们可以沿用实模式下存在的段机制,为段设立权限。
- 应用的指令序列处在一个权限受限的段中,不允许访问除自己段内的任何内存。
-
而操作系统就处在拥有最高特权的段中,掌有生杀大权。