“pmod, 实模式和保护模式”

实模式和保护模式都是CPU的工作模式,而CPU的工作模式是指CPU的寻址方式、寄存器大小等用来反应CPU在该环境下如何工作的概念。

实模式

实模式出现于早期 8088CPU时期。当时由于CPU的性能有限,一共只有20位地址线 (所以地址空间只有1MB) ,以及8个16位的通用寄存器,以及4个16位的段寄存器。所以为了能够通过这些16位的寄存器去构成20位的主存地址,必须采取一种特殊的方式。当某个指令想要访问某个内存地址时,它通常需要用下面的这种格式来表示:

(段基址: 段偏移量)

其中第一个字段是段基址,它的值是由段寄存器提供的(一般来说,段寄存器有6种,分别为cs,ds,ss,es,fs,gs,这几种段寄存器都有自己的特殊意义,这里不做介绍)。

第二字段是段内偏移量,代表你要访问的这个内存地址距离这个段基址的偏移。它的值就是由通用寄存器来提供的,所以也是16位。那么两个16位的值如何组合成一个20位的地址呢?CPU采用的方式是把段寄存器所提供的段基址先向左移4位。这样就变成了一个20位的值,然后再与段偏移量相加。

即:

物理地址 = 段基址«4 + 段内偏移

所以假设段寄存器中的值是0xff00,段偏移量为0x0110。则这个地址对应的真实物理地址是 0xff00«4 + 0x0110 = 0xff110。

由上面的介绍可见,实模式的"实"更多地体现在其地址是真实的物理地址。

保护模式

随着CPU的发展,CPU的地址线的个数也从原来的20根变为现在的32根,所以可以访问的内存空间也从1MB变为现在4GB,寄存器的位数也变为32位。所以实模式下的内存地址计算方式就已经不再适合了。所以就引入了现在的保护模式,实现更大空间的,更灵活也更安全的内存访问。

在保护模式下,CPU的32条地址线全部有效,可寻址高达4G字节的物理地址空间; 但是我们的内存寻址方式还是得兼容老办法(这也是没办法的,有时候是为了方便,有时候是一种无奈),即(段基址: 段偏移量) 的表示方式。当然此时CPU中的通用寄存器都要换成32位寄存器(除了段寄存器,原因后面再说)来保证寄存器能访问所有的4GB空间。

我们的偏移值和实模式下是一样的,就是变成了32 位而已,而段值仍旧是存放在原来16位的段寄存器中,但是这些段寄存器存放的却不再是段基址了,毕竟之前说过实模式下寻址方式不安全,我们在保护模式下需要加一些限制,而这些限制可不是一个寄存器能够容纳的,于是我们把这些关于内存段的限制信息放在一个叫做全局描述符表(GDT)的结构里。全局描述符表中含有一个个表项,每一个表项称为段描述符。而段寄存器在保护模式下存放的便是相当于一个数组索引的东西,通过这个索引,可以找到对应的表项。段描述符存放了段基址、段界限、内存段类型属性(比如是数据段还是代码段,注意一个段描述符只能用来定义一个内存段)等许多属性,具体信息见下图:

图1 段描述符格式 其中,段界限表示段边界的扩张最值,即最大扩展多少或最小扩展多少,用20位来表示,它的单位可以是字节,也可以是4KB,这是由G位决定的(G为1时表示单位为4KB)。

实际段界限边界值=(描述符中的段界限+1)* (段界限的单位大小(即字节或4KB))-1,如果偏移地址超过了段界限,CPU会抛出异常。

全局描述符表位于内存中,需要用专门的寄存器指向它后, CPU 才知道它在哪里。这个专门的寄存器便是GDTR(一个48位的寄存器),专门用来存储 GDT 的内存地址及大小。

最后我们再介绍一下一个新的概念: 段的选择子。段寄存器 CS、 DS、 ES、 FS、 GS、 SS,在实模式下时,段中存储的是段基地址,即内存段的起始地址。 而在保护模式下时,由于段基址已经存入了段描述符中,所以段寄存器中再存放段基址是没有意义的,在段寄存器中存入的是一个叫作选择子的东西。选择子"基本上"是个索引值,虽然它还有其他内容,不过作为初学者暂时忽略也没太大关系。由于段寄存器是 16 位,所以选择子也是 16 位,在其低 2 位即第 0~1 位, 用来存储 RPL,即请求特权级(有兴趣的可以了解一下,不想了解的忽略即可,跟用户态和内核态相关的),可以表示 0、 1、 2、 3 四种特权级。在选择子的第 2 位是 TI 位,即 Table Indicator,用来指示选择子是在 GDT 中,还是 LDT 中索引描述符。 TI 为 0 表示在 GDT 中索引描述符, TI 为 1 表示在 LDT 中索引描述符。选择子的高 13 位,即第 3~15 位是 描述符的索引值,用此值在 GDT 中索引描述符。前面说过 GDT 相当于一个描述符数组,所以此选择子中的索引值就是 GDT 中的下标。选择子结构如下:

图2 选择子结构 此外, 扩充的存储器分段管理机制和可选的存储器分页管理机制,不仅为存储器共享和保护提供了硬件支持,而且为实现虚拟存储器提供了硬件支持; 支持多任务,能够快速地进行任务切换(switch)和保护任务环境(context); 4个特权级和完善的特权检查机制,既能实现资源共享又能保证代码和数据的安全和保密及任务的隔离; 支持虚拟8086方式,便于执行8086程序。

为什么要有保护模式

实模式是有很大弊端的,首先,直接操作物理内存,这样的话每次只能运行一个程序,并且不安全;另外,内存最大使用到1M,限制太大。 保护模式下,程序不能直接访问物理地址,程序的虚拟地址需要被转换为物理地址后再去访问,地址转换是由处理器和操作系统协作完成的,处理器在硬件上提供地址转换部件,操作系统提供转换过程中需要的页表。

实模式到保护模式

二: 操作系统的运行模式 当你打开电脑,轻轻按下开机键,固化在 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 控制寄存器中的标志位打开就好了。除了打开开关,还需要准备好保护模式所需要的一些数据,如上面所说的全局描述符表,然后直接跳往某个构建好的段选择子,就完成了实模式向保护模式的跳跃。

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 中任何一个高,那么就会拒绝访问,从而起到了保护作用

总结

总的来说,运行模式其实是操作系统和处理器之间的一种相辅相成,共同发展的产物,虽然大部分人都不是内核开发人员,但是了解这些运行模式可以更好的帮助我们理解操作系统的底层运行原理,毕竟这是一个程序员的自我修养: )

参考书籍

《操作系统真象还原》

《Orange’S:一个操作系统的实现》

《一个64位操作系统的设计与实现》

顺便推荐几本编译和汇编相关的书:

《汇编语言》

《x86汇编语言: 从实模式到保护模式》

《现代编译原理》

《程序员的自我修养: 链接、装载与库》

全局描述符表

最初的操作系统是无法对内存段做访问限制,有了这样的需求以后,CPU厂商决定采用段描述符来实现相关的功能,在硬件一级上添加GDTR和LDTR来支持全局描述符表和局部描述符表,并由硬件负责周边的安全检测。当初的CPU厂商也并不是凭空制造出了这样一个概念,是与操作系统厂商共同协商后才有了一套硬件方面的支持。 如今x86架构的OS,大都都把分段视为是兼容性的考虑 (类似于对实模式的处理),在系统初始化阶段象征性地初始化GDT,之后的运行就没分段啥事儿了。地址空间的保护模型都来自分页,像ARM体系结构就不支持分段,仅靠MMU进行保护。

https://zhuanlan.zhihu.com/p/42309472
https://zhuanlan.zhihu.com/p/54083337

作者:长安 链接:https://www.zhihu.com/question/50796850/answer/654281605 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。