一: 前言
操作系统的学习是件枯燥的事情, 枯燥在你要去看汇编, 看 Intel 手册, 看...... 但是也很有趣, 最起码让我知道了熬夜掉头发写的代码是如何在物理机上运行的.
从去年到现在, 陆陆续续看了好几本操作系统相关的书, 由于不是专业搞内核的(当然也没那个能力 ), 每次看过后都很容易忘记, 俗话说好记性不如烂笔头, 倒不如自己梳理记录下, 权当作笔记了, 如有错误, 欢迎指正.
二: 操作系统的运行模式
当你打开电脑, 轻轻按下开机键, 固化在 ROM 中 BIOS 程序便开始悄悄运行起来, 当它完成加电自检等操作后, 就会从磁盘中寻找 Boot 引导程序去加载内核. 怎么找? 没那么神秘, 其实就是一个写死的规定, BIOS 会检测磁盘的第 0 磁头第 0 磁道第 1 扇区的内容是否以 0x55aa 结尾, 如果是, 那么就认为第 1 扇区中存放的即为 Boot 引导程序, 顺便把它复制到物理内存 0x7c00 处, 接着跳到此处开始执行, 所以我们只要将编译好的引导程序放在上面所说的磁盘扇区, 让 BIOS 来寻找就好了. 等到 CPU 的控制权限成功转交给我们的 Boot 引导程序了, 我们就进入了下面要说的实模式.
2.1 实模式
早期其实是没有实模式这个概念的, 因为那时的程序员还没有想到后面会冒出好几种模式, 实模式是为了为了和后来出现的几种模式做区分, 才被大家这样叫的.
要讲实模式, 就不得不讲讲内存寻址了, 先来复习下大学时的汇编课程.
我们的代码分为代码段, 数据段等, 所有的内存寻址都是根据 段基址: 段内偏移 来访问的, 这样的地址形式称为逻辑地址, 为什么搞这么复杂呢? 因为早期的 8086 处理器寄存器宽度只有 16 位, 16 位的寄存器只能进行 64 KB 的寻址, 而 8086 有 20 根地址线, 按照地址线来计算可以进行 1 MB 的寻址, 所以 16 位宽度的寄存器是显然不能满足需求的, 为了解决这个问题, 聪明的程序员想出了用 段基址: 段内偏移的方式来扩展寻址空间.
物理地址 = 段基址 << 4 + 段内偏移
CPU 在访问内存前, 会先经过段部件, 按照上面的换算公式, 计算出物理地址, 这样两个 16 位的寄存器合在一起, 宽度便成了 20 位.
在实模式下, 段寄存器直接存放的就是段基址, 比如 CPU 中用来存放当前指令地址的 CS:IP 寄存器, CS 中存放的便是代码段的基址.
那么实模式有什么缺陷呢? 首先就是不安全, 程序可以随意访问任何物理地址, 就像逛菜市场一样, 无拘无束. 为了不让某些非法分子到处瞎转悠, 保护模式孕育而生!
2.2 保护模式
时代在发展, CPU 也在进步, 处理器厂商为了满足不断增长的内存需求, 研发出了 32 位 的寄存器, 还顺带着搞出了一个保护模式.
在保护模式下, 很重要的一点就是段寄存器不是直接存放段基址了, 而是存放着段选择子.
段选择子??? 简单点, 说话的方式简单点!
好吧, 抛开特权级等信息, 你大可把它当作一个索引, so... 索引什么? 索引段描述符.
段描述符, 顾名思义, 用来描述一个段的信息. 长度为 64 位, 其中有 32 位用来存放段基址, 剩下 32 位存放着段界限等信息.
那么段描述符存放在哪里呢? GDT, 全局描述符表, 全局描述符表会存放着所有的段描述符, 当然还有 LDT, 这里先不提了.
好吧, 那我把索引 (段选择子) 告诉 CPU 了, 他怎么知道上哪找 GDT 呢? 嗯嗯, 当然你得提前告诉 CPU GDT 的地址啊:
lgdt [GDT 地址]
只要执行了上面这个指令, CPU 便会记录下 GDT 的地址了, 将其存到 GDTR 寄存器中.
了解了上面这些术语后, 现在来梳理下保护模式下的寻址方式:
段寄存器存放段选择子;
CPU 根据段选择子从 GDT 中找到对应段描述符;
从段描述符中取出段基址.
根据之前的公式, 结合段基址和段内偏移, 计算出物理地址.
未开启分页时, 保护模式寻址方式
上面所说的是分页机制未开启的情况下寻址过程, 如果开启了分页机制, 第 3 步计算出来的就是线性地址, 需要经过页部件才能转换成物理地址.
如何开启保护模式呢? 当然也没那么神秘, 就是将 CR0 控制寄存器中的标志位打开就好了. 除了打开开关, 还需要准备好保护模式所需要的一些数据, 如上面所说的全局描述符表, 然后直接跳往某个构建好的段选择子, 就完成了实模式向保护模式的跳跃.
2.3 IA-32e 模式
32 位的寄存器已经可以进行 4GB 的内存寻址了, 但是似乎还不够, 所以后来又发展出了 64 位寄存器, 其中 48 位用来寻址, 这下好了, 至少目前感觉够用了.
伴随着 64 位处理器又出现了一种新的模式: IA-32e.
IA-32e 是基于保护模式的, 也就是说也是通过段选择子, 段描述符等来寻址的, 但是和 32 位保护模式不同的一点是, 对于 IA-32e 来说, 所有段描述符中的段基址都是 0, 段长度都是可寻址的最大长度, 这样在分页情况下, 段内偏移量直接就等于线性地址了, 无需再经过公式计算.
三: 特权级
前面说到保护模式比实模式安全, 但是上面好像没有体现出来这个特点啊, 其实除了寻址方式的变化, 保护模式还增加了一个新名词: 特权级.
特权级共四层, 0 为最高特权级, 为内核代码所运行级别, 3 为最低特权级, 为用户程序所运行级别.
段描述符中会记录访问当前段所需特权级, 程序在访问一个段时需要先构建段选择子, 段选择子中中有两位专门负责表示当前程序请求访问目标段的时的特权级, 即为 RPL. 一般来说, RPL = CPL,CPL 即为当前程序所在代码段的特权级, 存在 CS 寄存器中的后两位(因为 CS 寄存器存放的就是当前代码段的段选择子).
目标段的特权级被称为 DPL, 当程序访问目标段的时候, 如果 DPL 特权比 CPL 和 RPL 中任何一个高, 那么就会拒绝访问, 从而起到了保护作用
四: 总结
总的来说, 运行模式其实是操作系统和处理器之间的一种相辅相成, 共同发展的产物, 虽然大部分人都不是内核开发人员, 但是了解这些运行模式可以更好的帮助我们理解操作系统的底层运行原理, 毕竟这是一个程序员的自我修养:)
五: 参考书籍:
《操作系统真象还原》 https://book.douban.com/subject/26745156/
《Orange'S: 一个操作系统的实现》 https://book.douban.com/subject/3735649/
《一个 64 位操作系统的设计与实现》 https://book.douban.com/subject/30222325/
顺便推荐几本编译和汇编相关的书:
《汇编语言》 https://book.douban.com/subject/1215178/
《x86 汇编语言: 从实模式到保护模式》 https://book.douban.com/subject/20492528/
《现代编译原理》 https://book.douban.com/subject/30191414/
《程序员的自我修养: 链接, 装载与库》 https://book.douban.com/subject/3652388/
来源: https://juejin.im/entry/5c3023dd6fb9a049fd0ffdec