# 保护模式

  • 保护模式是在硬件层面提供的 CPU 运行机制,是现代操作系统的根本。
  • 没有保护模式,操作系统是没有安全性可言的。

# 何为保护

  • 指令是存放在内存中的

  • 假定你的程序试图对操作系统的关键代码进行破坏:

    • mov byte ptr ds:[kernel], 0x90
  • 操作系统应该如何制止?

  • 也许这个时候有同学发炎了:

    • “也许操作系统它能监控呢?”
  • 答案是,监控你程序的并不是操作系统,这种工作必须在硬件层完成

    • 很简单的道理,实际上运行指令的是 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 看作一段,分给程序 A
    • 0x80 ~ 0xff 看作一段,分给程序 B

实模式下的分段机制大抵如此。

# 段寄存器

  • 想来各位读者对寄存器都有所了解,既然我们需要一个容器来存放段的基地址,寄存器自然是不错的选择。

  • 但是通用寄存器本身数量也不够多,再想腾出来存放段基址,也是心有余而力不足了。

  • 因此,理所当然的,就有了以为名的段寄存器

# ds 寄存器

  • ds,即 data segment,意为数据段
  • 咱们一看就知道,这个东西就是和数据 相关的。
  • 它也十分简单,16 位的宽度,作用就是存放数据段的基址

注解

实际在实模式下,物理地址的转换公式略微复杂一些
(ds << 4) + address = 最终的物理地址

# 结合先前的示例

地址 程序 基地址
0x00 ~ 0x7f 提供给程序 A 0x00
0x80 ~ 0xff 提供给程序 B 0x80
  • 我们选择让 B0x40 这个位置存放一些数据
  • 而现在我们有了 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 段寄存器,我们有何不可呢?

# 权限的建立

  • 首先,咱们既然要保证安全,那自然就要划分哪些是应用能做的,哪些是应用不能做的。

  • 其次,总归要有程序去管理软硬件的,操作系统的不受限制也理所应当了。

# 权限划分

  • 既然有了不能做能做之分,并且还有了 "区别对待",权限自然也就建立起来了。

  • 至此,咱们初步确定了权限的划分。

    • 常规应用,拥有部分权限
    • 操作系统,拥有所有权限

# 段机制的延展

  • 咱们最先想到的,最不能让应用去乱搞的是什么呢?

  • 首先就是不能让应用去破坏咱们的操作系统,咱们首先要把自己保护起来。

    • 如果应用能随意修改操作系统的指令序列,那么所谓的保护将毫无意义

# 初步构想

  • 我们可以沿用实模式下存在的段机制,为段设立权限。

    • 应用的指令序列处在一个权限受限的段中,不允许访问除自己段内的任何内存。
  • 而操作系统就处在拥有最高特权的段中,掌有生杀大权