今天我们就系统的讲下实模式和保护模式。我觉得能很形象的说明保护模式存在的意义。先看下面这段代码。
int main()
{
int* addr = (int*)0;
cli(); //关中断
while(1)
{
*addr = 0;
addr++;
}
return 0;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
这段代码如果能顺利执行,其实是很可怕的。你会发现他直接的获取到了内存的 0 号位置,并且能顺序的向下遍历,然后还能直接的对内存写入数据,对了,这段代码还把中断关了。想象一下,如果你和几个用户一起用一台服务器,一旦有个用户执行完这段代码后,那么所有人还在内存中的数据将被删的干干净净,并且在删的过程中还没有任何办法去中断这段代码对应的进程。
这段代码的工作方式就是实模式的工作方式。实模式的特点主要在于实模式的寻址方式,实模式的寻找方式进而决定了实模式在寻址范围以及保护性上都不如保护模式好。目前实模式的存在主要是为了兼容之前的系统,在操作系统读入bootsect.s 以及执行 bootsect.s 进行 setup 和 system 读入阶段是在实模式下执行,之后都是在保护模式下执行了。我们接下来分别看实模式和保护模式的工作方式。
Part1实模式
今天详细讲一下载入过程。正好我们来感受下实模式是如何工作的。
首先说实模式应该如何表示指令在内存的位置,实模式下通过 ”CS“ 和 ”IP“ 两个寄存器来表示位置,CS 寄存器存段基指,IP 寄存器存偏移。这里简单介绍下为什么要用段表示。了解汇编的同学可能知道汇编代码中会把代码分成很多段,有代码段、数据段等。这样程序在载入内存时也是分成不同的段载入内存的。这样取指令时就需要找到段的基址,然后再确定偏移,就能得出指令的地址。具体方式是把 CS 寄存器中的值取出来向左偏移 4 位,然后加上 IP 偏移的值。比如要得到 BIOS 的地址,那么从CS寄存器得到的段基址就是 0xFFFF,IP 取出的偏移是 0x0000。CS偏移 4 位加 IP 就是 0xFFFF0(地址的表示是16进制的,但左移四位说的是二进制的偏移,十六进制偏移一位和二进制偏移四位效果是一样的)。这样,BIOS的位置就找到了,接下来取出指令并执行就好了。
CS寄存器中的值取出来后先偏移 4 位就是因为 CS 寄存器和 IP 寄存器都是16位的,16位只能表示 64k 的大小。将 CS 向左移4位加上偏移,就能用20位表示地址了,寻址范围能达到 1M。
虽然通过将从CS寄存器中取出的段基址左移四位后寻址范围增加了,达到1M,但对于现代的计算机来说寻址范围实在太小了(就算是32位的计算机 CS 寄存器也是16位的),这就是实模式下的一大缺点。我们再看看保护模式是怎么做的。
Part2保护模式
对于 32 位的计算机,如果内存寻址只能寻到 1M,显然太小了。理论上,32 位的计算机适配的地址线也有 32 位,我们应该以 32 位的地址去表示地址。但是为了适配之前的系统(现在的操作系统在加载 BIOS,用 BIOS 执行载入 bootsect.s 等一直用着上面所表述的实模式的寻址方式),CS 这个段寄存器一直就是 16位 的,只是存偏移的那个寄存器多了一个叫 EIP 的 32 位的寄存器。为了让寻址适应这个变化,CS 寄存器存的不再是真实的段基址了,而是段选择子,其中段选择子的 0-2 位用作权限控制,3-15 记录的是 GDT 表的索引。
拿到段选择子后就可以通过选择子找到段描述符,然后再通过段描述符(GDT表)找到相应的段基址,然后再把段基址和偏移相加就是真实的地址了。
这个 GDT 表可是有大用,不光解决寻址的问题,对于内存的保护也可以靠他。
这个段描述符中段长和段基址都是断开的,这是由于历史原因构成的,咱们还是主要看段描述符中的内容以及对应的功能。段长度可以限定段内偏移地址,也就是说你拿到一个段基址后只能在这个界限内偏移,如果偏移超过限定值访问就不合法了。段基址就和实模式下的段基址一样,只是现在有了32位的段基址。描述方面主要是对段的可读可执行,代码段还是数据段进行描述,另外描述中有一个相当重要的就是 DPL,这个 DPL 限定了权限级别,只有段选择子中记录的发起访问者的访问权限满足 DPL 的限定,才能访问。
不难看出,GDT 表对相应的段的可读可写以及段是代码段还是数据段等情况都做了详细的描述,每次通过段选择子找到段描述符时,都会判断能不能访问成功。
下面通过流程图看下保护模式的完整取指过程。