Linux从头学08:Linux 是如何保护内核代码的?【从实模式到保护模式】
扫描二维码
随时随地手机看文章
作 者:道哥,10 年的嵌入式开发老兵。公众号:【IOT物联网小镇】,专注于:C/C 、Linux操作系统、应用程序设计、物联网、单片机和嵌入式开发等领域。 公众号回复【书籍】,获取 Linux、嵌入式领域经典书籍。
转 载:欢迎转载文章,转载需注明出处。
-
从 16 位进入到 32 位
-
8086 的 16 位模式
-
80386 的 32 位模式
-
从实模式进入到保护模式
-
如何进入保护模式
-
GDT 全局描述符表
-
GDTR 全局描述符表寄存器
-
段描述符的查找原理
PS: 相应的,之前 8086 中的处理器执行模式就叫做“实模式”。虽然80286没有形成一定的气候,但是它对后来的80386处理器提供了基础,让386获得了极大的成功。
从 16 位进入到 32 位
8086 的 16 位模式
在8086处理器中,所有的寄存器都是16位的。
例如:在访问代码段的时候,把 cs 寄存器左移 4 位,再加上 ip 寄存器,就得到 20 位的物理地址了;还记得我们第1篇文章Linux 从头学 01:CPU 是如何执行一条指令的?中的寄存器示意图吗?20 位的地址,最大寻址范围就是 2 的 20 次方 = 1 MB 的空间;
在访问代码段的时候,使用 cs:ip 寄存器;在访问数据段的时候,使用 ds 寄存器;
在访问栈的时候,使用 ss:sp 寄存器;
80386 的 32 位模式
进入到32位的处理器之后,这些寄存器就扩展到32位了:
注意:高 16 位不可以独立使用。下面这张图是32位处理器的另外4个通用寄存器(注意它们是不能按照8位寄存器来使用的):
在 32 位处理器中,依然可以兼容 16 位的处理模式,此时依然使用 16 位的寄存器;是不是感觉到上面的寄存器示意图中漏掉了什么东西?如果不兼容的话,就会失去很大的市场占有率;
有些书上把段寄存器称之为:段选择子;正是因为处理器有32根地址线,可寻址的范围已经非常大了(4 GB),因此理论上它是不需要像8086中那样的寻址方式(段地址左移4位 偏移地址)。也有一些书上把段寄存器中的值称之索引值,称之为选择子;
不必纠结于称呼,明白其中的道理就可以了;
找到了这个段的基地址之后,在访问内存的时候,仍然是按照段机制 偏移量的方式。
- 对于 8086 来说,段寄存器中的内容左移 4 位之后,就是段的基地址;
- 对于 80386 来说,段寄存器中的内容是一个表的索引号,通过这个索引号,去查找表中相应位置中的内容,这个内容中就有段的基地址(如何查找,下文有描述);
从实模式进入到保护模式
如何进入保护模式
CPU是如何判断:当前是执行的是实模式?还是保护模式?
bit0 = 0: 实模式;在处理器上电之后,默认状态下是工作在实模式。
bit1 = 1: 保护模式;
GDT 全局描述符表
由于这张表中的每一个条目(Entry),描述的是一个段的基本信息,包括:基地址、段的长度界限、安全级别等等,因此我们称之为全局描述符表(Global Descriper Table, GDT)。
之所以称之为全局的,是因为每一个应用程序还可以把段描述符信息,放在自己的一个私有的局部描述符表中(Local Descriper Table,LDT),在以后的文章中一定会介绍到。
处理器规定:第一个描述符必须为空,主要是为了规避一些程序错误。从上图中可以看出:GDT中每一个条目的长度是8个字节,其中描述了一个段的具体信息,如下所示:
第一个问题的答案是:历史原因(兼容性)。
- 为什么段的基地址不是用连续的 32 bit 位来表示?
- 段的界限怎么是 20 位的?20 位只能表示 1 MB 的范围啊?
为了完整性,我把所有标志位的含义都汇总如下,方便参考:
- 如果 G = 0: 表示段界限是以字节为单位,此时,段界限的最大表示范围就是 1 MB;
- 如果 G = 1:表示段界限是以 4 KB 为单位,此时,段界限的最大表示范围就是 4 GB( 1 MB 乘以 4KB);
在 Linux 操作系统中,只利用了 0 和 3 这两个特权级别。S (bit12):决定这个段的类型。
GDTR 全局描述符表寄存器
还有一个问题需要处理:GDT表本身也是数据,也是需要存放在内存中的。
也就是说:处理器是到固定的地址0处,查找中断向量表的,这是一个固定的地址。
- 程序代码把每一个中断的处理程序地址,放在中断向量表中的对应位置;
- 中断向量表的起始地址放在内存的 0 地址处;
其实,GDT 在上电刚开始的时候,也不能放在内存中的任意位置。从GDTR寄存器中的内容可以看出,它不仅存储了GDT的起始地址,而且还限制了GDT的长度。因为在进入保护模式之前,处理器还是工作在实模式,只能寻址 1 MB 的内存空间,因此,GDT 只能放在 1 MB 内的地址空间中。
在进入保护模式之后,能寻址更大的地址空间了,此时就可以重新把 GDT 放在更大的地址空间中了,然后把这个新的起始地址,存储到 GDTR 寄存器中。
段描述符的查找原理
在上面的段寄存器示意图中,我们只说明了段寄存器依然是16位的。
RPL: 表示当前正在执行的这个代码段的请求特权级;假设当前代码段寄存器cs的值为0x0008,处理器按照保护模式的机制来解释其中的内容:TI: 表示到哪一个表中去找这个段的描述信息:全局描述符表(GDT) or 局部描述符表(LDT)?
TI = 0 时,到 GDT 中找段描述符;
TI = 1 时,到 LDT 中找段描述符;
找到了这个段描述符条目之后,就可以从中获取到这个代码段的具体信息了:
- TI = 0,表示到 GDT 中查找段描述符;
- RPL = 0,表示请求特权级别是 0;
- 描述符索引是 1,表示这个段描述符在 GDT 中的第 1 个条目中。由于每一个描述符占用 8 个字节,因此这个描述符的开始地址位于 GDT 中的偏移地址为 8 的位置(1 * 8 = 8);
另外,从上文描述的GDTR寄存内容知道,它限制了GDT中最多一共可以存放8192个描述符。
- 代码段的基地址在内存中什么位置;
- 代码段的最大长度是多少(在获取指令时,如果偏移地址超过这个长度,就引发异常);
- 代码段的特权级别是多少,当前是否驻留在物理内存中等等;
2 的 13 次方 = 8192。至此,处理器就在保护模式下,查找到了一个段的所有信息。
------ End ------
这篇文章主要描述了80386处理器中的保护模式下,段寄存器的使用,以及通过段描述符来查找段的具体信息。