写在前面
本文讨论了在保护模式下访问内存的准备工作以及具体访问流程,还点出了一些细节问题。由于笔者水平有限,文中难免出现纰漏,恳请各位读者指正。
IA-32架构下段寄存器的变化
我们知道,在x86处理器中,段寄存器是16位的,里面存放了段地址。在IA-32架构下,段寄存器仍然是16位的,但是每个段寄存器都配备了一个长度为64位的描述符高速缓存器,这个缓存器对于我们来说是不可见的。这时的段寄存器存放的已经不再是段地址,而是段选择子。此时的段寄存器也称为段选择器。
段描述符
在保护模式下,对内存打访问仍然使用段地址和偏移地址的形式,只不过增加了更多的限制。
我们定义一个数据结构来保存段的限制信息:段描述符,段描述符的大小是8字节也就是64位的。这时我们前面所说的描述符高速缓存器的作用就体现出来了:用于存放段描述符,便于我们使用。
接下来,我们看看段描述符中究竟有什么
描述符中的具体内容先不讲解,以后用到哪个再拿出来讲一讲。
引入段描述符表
一个程序可能有很多个段,这时段描述符高速缓存器就不够用了。所以我们可以把程序的段描述符集中放在内存中,在需要访问对应的段的时候再把段描述符的内容复制到描述符高速缓存器中。这块存放着许多段描述符的内存空间,就是所谓的段描述符表。描述符表又分为全局描述符表(GDT)和局部描述符表(LDT)。全局说明这张表可以为整个软硬件所用,局部说明这张表是用户程序私有的。
编写段描述符表
在保护模式下想要访问一个内存段,必须要先在段描述符表中定义这个段,也就是说我们应该先把这个段的段描述符安装到表中。下面以全局段描述符表为例进行说明。
请牢记,此时我们仍然处于实模式下。想要在实模式下安装段描述符,我们就得把段描述符表的32位线性地址转换为段地址和偏移地址,我们使用除法指令完成这一转换。
写入描述符高速缓存器
但是问题又来了,前文提到过描述符高速缓存器是不可见的,也就是说我们不能操作它,那么我们该如何复制段描述符到其中呢?这时段选择器的作用就体现出来了。因为段描述符表在内存中是连续存放的,我们可以把段描述符表看作一个数组,段选择器中存储的段选择子,就相当于数组的下标。我们使用下标就可以将描述符表中的描述符复制到描述符高速缓存器中。
现在还有一个问题需要解决,CPU怎么知道这张表在内存中的什么位置。在CPU内部,有两个48位寄存器:GDTR和LDTR。它们分别存放了GDT、LDT在内存中的位置和大小。下面以GDTR为例进行说明。
从图中我们可以知道一个段描述符表的最大大小为2^16=65536byte=64KB。有因为一个描述符的大小为8个字节,所以一个描述符表最多有8192个描述符,也就是说最多能有8192个段。
GDTR的高32位指明了GDT在内存中的起始地址,也就是编号为0的段描述符的第一个字节的地址。IA-32规定,0号全局段描述符是空的,只是占用8个字节但不保存段描述符。但是这个规定对于局部描述符表(LDT)来说不适用。
我们需要明确,全局段描述符表不是用户程序可以修改的,它一般由操作系统维护。
理论上我们可以把全局段描述符表写在内存的任何地方,但是在进入保护模式之前,我们必须先进入实模式,在实模式下最多只能访问2^20=1MB的内存,所以GDT一般安装在内存起始的1MB范围内。
我们使用lgdt指令来把内存中的内容写入到GDTR中。指令格式如下:
lgdt m ;m为需要写入GDTR中的内容的有效地址
中途总结一下
以上所说的操作,均是在实模式下完成的。是的,我们仍然处于实模式,以上的操作不过是为进入保护模式所做的准备工作。接下来,我们将让CPU从实模式切换到保护模式。
进入保护模式
经过大量的前期准备工作之后,我们已经十分接近保护模式的大门了,打开这个大门的钥匙就是CR0控制寄存器。
在使用钥匙之前,我们需要关闭原来的中断。保护模式下的中断机制和实模式不同,因此,原有的中断向量表不再适用,而且,必须要知道的是,在保护模式下,BIOS中断都不能再用,因为它们是16位的代码。在重新设置保护模式下的中断环境之前,必须关中断。为此,我们使用如下指令关闭中断:
cli
在CLI指令生效后,EFLAG寄存器中的IF位将被清零,所有外部中断都会被屏蔽,从而保证当前运行的代码不被打断,起到保护代码运行的作用。
接着,我们需要设置CR0寄存器的PE(Protection Enable)位为1,从而正式切换到保护模式。PE位即为CR0寄存器的第0位。
mov eax, cr0
or eax, 1
mov cr0,eax
执行上述指令后,CPU就会进入保护模式。
保护模式下的内存访问
在保护模式下,我们使用一个段中的数据,或者执行一个段中的代码时,就会触发保护模式下的内存访问机制。以使用GDT为例,具体有如下过程。
我们通过段寄存器中存储的段选择子来索引到段描述符表中的具体内存段。我们来看看段选择子结构。
第一部分是描述符的索引号,用来在描述符表中选择一个段描述符。TI是描述符表指示器(Table Indicator),TI=0时,表示描述符在GDT中;TI=1时,描述符在LDT中。RPL是请求特权级,表示想要访问这个内存段的程序的级别。
随后,CPU会把段选择子中的索引乘以8,在加上GDTR中的段描述符表基地址,得出段描述符的地址。如果这个地址没有超过GDTR中的界限大小,并且请求访问该内存段的程序权限足够大的话,CPU就自动将找到的描述符加载到不可见的描述符高速缓存部分。
此后,每当有访问内存的指令时,就不再访问GDT中的描述符,直接用当前段寄存器描述符高速缓存器提供线性基地址。有了这个线性基地址,再加上汇编代码中给出的偏移地址,我们就可定位到我们想要的内存地址中去了。
最后
至此,我们已经大致了解了保护模式下的内存访问基本机制。当然,还有诸如特权检查等内容未详细展开。此外,我们还尚未引入分页机制,也没有讨论任务切换的情况。这些内容,我会在后续的文章中展开说明。谢谢大家的观看。