从第五章开始要涉及到一些计算机平台硬件设备的初始化工作了,不过在此之前,我们还需要先了解CPU的几种工作模式。
众所周知,CPU一共有五个模式,分别如下(其实这一段也是从我毕业论文里面复制的... ):
- 实模式:实模式是CPU中最早的运行模式,微软的DOS系统便是运行在实模式下,实模式状态下,应用程序运行时,系统会将CPU所有权限转交给应用程序,这样导致了系统的安全性受到威胁。
- 保护模式:保护模式是32位CPU的主要运行模式,该模式为软件的运行提供了安全保障和大量的功能,在CPU进入64位模式前,需要先切换到保护模式。
- 系统管理模式:CPU在进入该模式以后,会首先保存CPU当前状态,然后进入一个独立的内存地址空间,该模式用于执行电源管理或系统安全方面的指令。
- 虚拟8086模式:该模式允许系统在保护模式下执行8086软件。
- IA-32e模式:本文操作系统会主要使用该模式。处于该模式下的CPU,其部分寄存器会被扩展至64位。CPU进入该模式后,可以进行64位线性地址空间寻址。
下面就来详细介绍一下各个模式的区别、用法及切换方法。
实模式(Real Mode)
早期的8086CPU只有这一个模式,并且现在的CPU在最开始上电的时候,也是处于这个模式。
寻址方式
实模式采用段地址寻址方式,处理器可以直接访问物理地址,而无需进行映射。在实模式下,处理器的位宽一般为16位,所以最高寻址只有1M(0x0000-0xffff)。同样是因为这个原因,段地址不能超过64KB,至于什么是段地址,紧接着我们就来介绍。
实模式采用逻辑地址编址,其寻址是通过段地址+偏移地址的方式,在汇编语言或绝大多数场景下的书写(表示)方式为:段基址:偏移量。其中段基址值保存在段寄存器中,偏移量保存在寄存器中或使用立即数代替。逻辑地址和线性地址的转换方法如下公式(来自《汇编语言》第四版):
线性地址 = 段 << 4 + 偏移
简单来说就是段地址向左偏移四位(对于十六进制表示来说,一般就是在末尾加一个0),然后加上偏移地址即可。举个例子,段地址为0x1000,偏移为0x50,转换成线性地址就是0x10050。这个计算公式在操作系统开发(4)——有引导功能的引导程序中计算loader保存的物理地址时也用到了。
由于上述公式,段地址在进行计算的时候向左移动了4位,所以实际上这种寻址方式使得16位的寄存器可以进行20位的寻址。不过我们还可以用一些手段,利用同样的原理,将寻址扩展到32位,以进行4GB大小的寻址。具体实现方法我们下面会讲到,在开发loader时也会用到这种方法。
实模式的1M寻址限制
上面有讲到过,由于寄存器位宽限制,即使使用段地址寻址方式,最多也只能进行20位寻址。不过就1M内存的话,在我们的操作系统里面其实是很不够用的。并在在实模式的1M内存空间里,还有很多片段是无法使用的,所以我们得想点办法来接触其限制,扩宽其寻址能力,以此来满足我们操作系统loader阶段的需求。
既然有提到1M实模式空间仍有很多片段无法使用,那么就顺便在这里对实模式的1M内存空间进行一下梳理,看看有哪些部分是我们可以使用的。具体内容的话,我在下方的表格中写到了:
位置 | 作用 |
0x00000 - 0x003FF (1K) | 中断向量表 每项4字节,共256项 |
0x00400 - 0x004FF (256Byte) | BIOS数据区 |
0x00500-0x9FFFF | 自由内存区 |
0xA0000 - 0xAFFFF (64K) | EGA/VAG/XGA/XVGA |
0xB0000 - 0xB7FFF (32K) | Mono text video buffer |
0xB8000 - 0xBFFFF (32K) | CGA/EGA+ chroma text video buffer |
0xC0000 - 0xC7FFF (32K) | 显卡BIOS使用 |
0xC8000 - 0xCBFFF (16K) | IDE控制器BIOS使用 |
0xCC000 - 0xEFFFF (143K) | 保留区域 |
0xF0000 - 0xFFFFF (64K) | 系统BIOS使用 |
所以实际上,我们在实模式下能使用的内存区域只有0x00500-0x9FFFF。这些空间在bootloader阶段显然是不够用的,所以我们需要对其进行扩展。下一节我们就来详细讲讲怎么扩展寻址范围。
实模式的4G寻址
A20地址线
在最早期的8088处理器中,地址线一共有20条,这导致其物理寻址能力最多只有1M。而实模式的最大逻辑地址为FFFFh:FFFFh,也就是10FFEFh,所以大于1M的地址会从物理地址0处重新计算。
从80286以后,处理器的地址线扩展到了24条,所以导致80286处理器在寻址时存在一个问题,就是在程序访问100000h到10FFEFh范围内的物理地址空间时,处理器不会从物理地址0处重新计算。为了解决这个问题,IBM向键盘控制器的空闲引脚中追加了一个与门来控制超过1M物理地址的寻址功能,我们一般把这个功能称为“A20地址线”。对于A20地址线,我们还会在后续进行系统loader开发的时候再次进行介绍。
寻址扩展
需要使用1M以上的内存,我们需要先进入一个叫Unreal mode的模式。这种模式运行使用保护模式的段地址寻址方式来进行实模式的寻址。也就是说,该模式的就是4G寻址的实模式。
Unreal mode开启的方法如下:首先需要准备保护模式运行使用的GDT,然后开启A20地址线并进入保护模式。这些流程与实模式切换保护模式的流程相同。进入保护模式后,向目标段寄存器载入段选择子,然后再切换回实模式。如果此后目标段寄存器值不再修改,那么目标段寄存器仍然缓存着这段描述符信息。现在进行内存地址的访问,处理器就会使用段地址访问方式了。在进入Unreal mode后如果重新对目标段寄存器赋值,则会覆盖段寄存器缓存的段描述信息,导致4G寻址失效,只能再次重复上述步骤重新进入Unreal mode。
之所以我们需要在保护模式和实模式之间来回切换,是因为在实模式下我们可以直接使用很多有用的BIOS功能,而保护模式无法使用BIOS功能,如果手动配置的话相对比较繁琐。
保护模式(Protected mode)
在这里主要介绍386架构以后的保护模式,有关286架构的保护模式可以看这里的The 286部分。
在实模式下,CPU只能同时运行一个应用程序,且该应用程序在拿到CPU控制权后可以完全操控内存。所以如果遇到恶意程序,会产生极大的安全风险。还有就是实模式只支持1M内存寻址,在操作系统中显然是不够用的。为了解决以上问题,则有了保护模式(Protected mode)。
在保护模式中,处理器会给程序安排4个执行级别,分别是0级到3级(也叫0环-3环或R0-R3,这些都是一个意思),其中0级权限最高,一般用于内核程序,部分特殊的汇编指令也只能在R0权级下使用;3级权限最低,一般用于用户应用程序。在Linux操作系统和Windows操作系统中都只用到了R0和R3,分别给内核级和用户级程序使用。
其次,保护模式还引入了内存分页管理的功能,这个功能我们后续开发操作系统时也会用到。至于分段式管理和分页式管理的区别以及二者的原理,我后面应该也会单独写一篇文章来介绍。分页式内存管理将物理内存分成固定大小的页,然后由页表映射为虚拟内存地址供应用程序使用,这样能减少不断分配和释放内存产生的大量碎片,减少了空间的浪费。
保护模式的特权级
刚才提到了保护模式引入了R0-R3四个特权级,下面就来详细介绍一下。
与上面所说的相同,从R3到R0的权限是递增的,一般来说我们会将用户应用程序运行在R3特权级,而将操作系统内核和部分驱动程序运行在R0的高特权级。而R1和R2一般用于运行一些系统服务,我们的系统暂时还不会涉及到这两个特权级的使用,不过后面也可能会迭代更新用到这些功能。
除了四个特权级,保护模式还引入了CPL、DPL、RPL三种特权级类型来协助处理器检测执行权限,下面分别进行介绍:
- CPL(Current Privilege Level,当前特权级):CPL描述的是当前程序的执行特权级,其保存于CS或SS段寄存器的第0位和第1位中。一般情况下CPL是正在执行的代码段特权级,当处理器执行不同特权级的代码段时,处理器才会修改CPL。
- DPL(Descriptor Privilege Level,描述符特权级):用于表示段描述符或门描述符的特权级,其保存于段描述符或门描述符的DPL区域内。当处理器访问段描述符或门描述符时,处理器将会对比描述符中的DPL值、段寄存器的CPL值以及段选择子的RPL值。
- RPL(Requested Privilege Level,请求特权级):RPL时段选择子的重载特权级,用于确保程序由祖国的权限去访问受保护的程序。其保存于段选择子的第0位和第1位。RPL与CPL均用于检测目标端的访问权限。也就是说,即使程序拥有足够的权限去访问目标段,但如果RPL值大于CPL值,则RPL会覆盖CPL,反之亦然。
我们在开发Loader和保护模式配置时也会用到这些内容,到时候我还会进一步补充说明。详细的权限级内容挺多的,现在暂时先不全部写出来,后续有空再来慢慢补充。(倒也不是说懒得写,主要是篇幅太大,还是等后面用到再写比较好)
切换到保护模式
有关模式的切换,可以在主线介绍到相关章节时再深入研究,这里先简单了解即可。
在切换到保护模式前,我们需要需要先初始化全局描述符表(GDT),并至少保证包含空描述符、代码段描述符以及数据段描述符三个描述符。然后将GDT所占字节数-1和GDT的物理地址保存到GDTR寄存器,以此向CPU指明GDT在内存中的位置。
有关GDT,我们会在后面使用到时详细介绍,在这里只需要知道先初始化这个东西即可,完成GDT初始化后,需要开启A20地址线。然后通过设置CR0寄存器的PE位进入保护模式,并且在设置完CR0寄存器的PE位后,需要进行远转移以清空PIQ。
系统管理模式(System Management mode)
有关系统管理模式(也叫SMM模式),网络上相关资料较少,所以这里就不详细介绍了。如果有需要,我们在后续开发操作系统的时候用到了再详细介绍。
SMM模式一般由系统估计提供的code来执行,操作系统不会知道CPU在何时进入或退出过SMM模式。由于SMM是由硬件直接控制,所以如果遇到攻击事件,有可能会绕过操作系统的安全防御而直接对CPU进行攻击。
系统管理模式用于处理系统级功能,如电源管理和硬件控制,并且只能通过系统管理中断进入,通过RSM退出。
虚拟8086模式(Virtual 8086 Mode)
由于保护模式和实模式不能兼容,CPU在进入保护模式后就不能再实行实模式程序,所以CPU引入了虚拟8086模式。该模式是在保护模式下创建的一个可以运行实模式应用程序的虚拟环境。
虽然V86模式是由硬件支持的,但要使用这个模式通常还需要操作系统或其他软件的支持。操作系统需要能够管理虚拟环境,包括创建和销毁虚拟环境,以及在必要时在虚拟环境和真实环境之间切换。此外,操作系统还需要处理虚拟环境中的程序试图进行的系统调用和硬件访问,因为在V86模式下,这些操作不能直接由硬件执行。
进入和退出虚拟8086模式需要操作系统的配合,后续不一定会用到这个模式,等后续使用到的时候再回来详细介绍。
IA-32e模式(IA-32e Mode)
IA-32e模式是Intel 64架构的一部分,Intel 64架构包括x86-64、AMD64等。
引用一段维基百科的介绍
苹果公司和RPM包管理员以“x86-64”或“x86_64”称呼此64位架构。甲骨文公司及Microsoft称之为“x64”。BSD家族及其他Linux发行版则使用“amd64”,32位版本则称为“i386”(或 i486/586/686),Arch Linux及其派生发行版用x86_64称呼此64位架构。
IA-32e包含两个子模式,分别为长模式和兼容模式,其中我们主要会用到的就是长模式(所以下面的介绍除非指明,均以长模式为主),也就是64位模式。兼容模式允许64位操作系统运行大多数现有的32位软件,是IA-32e模式下对32位应用的一种兼容。相较于保护模式的32位寻址,只能寻址4G(以前使用32位的xp,只能寻址到3.8G),长模式拥有更大的寻址空间。
IA-32e可寻址4PB物理地址空间或256TB线性地址空间。IA-32e的页管理机制支持4KB、2MB和1GB三种物理页容量,本文系统采用2MB分页容量。