x86 在启动的时候,CPU 处于实模式;而在保护模式下,为了将线性(内存)地址转换为物理内存地址,我们需要给 CPU 设置页表。本篇文件主要给《深入理解 LINUX 内核》临时内核页表一节(P74)中作者描述不太详细的部分添加更多的解释,并不打算把书中相关的知识点都搬上来,相关知识读者可以参考书中的描述。

本文基于 Linux 2.6.24,x86 平台

在使用分页(paging)的时候,需要构造相应的页表。对于使用一级页表的 x86 系统来说,页目录是必须的。但是这 1024 个页目录项并不需要都指向具体的页表,按需分配就行。看到“分配”这个词,读者就要注意了,它意味着动态内存分配。啊!我们连页表都没有,怎么动态分配内存!!没关系,我们弄个临时页表就可以解决这个鸡蛋和鸡的问题了。

内核临时页面的设置由 startup_32() 函数完成,这个函数是用汇编语言实现的,位于 arch/x86/kernel/head_32.S

我们需要特别留意的是文中提到的,“在这个阶段PAE支持并未激活”。PAE 未激活,意味着此时使用的是二级页表接口,一个页表项 32 bits,DIRECTORY 字段 10 bits,TABLE 字段 10 bits 和 OFFSET 12 bits(参考第 52 页图 2-7)。

OFFSET 12 bits,所有一个 page 的大小是 2^12 = 4K。TABLE 字段有 10 bits,所以一个页目录项可以指向的页大小为 2^10 * 4K = 4M。我们需要映射 8M 的内存,所以两个页目录项就足够了。

接下来我们需要明白的是,内核映像是加载在物理地址的低端的(P73 图 2-13)。而内核在链接(link)的时候,起始地址却是 0xc000 0000。在实模式,线性地址直接翻译为物理地址;而在保护模式,线性地址需要经过页表的转换后才是物理地址。当我们把线性 0x0000 0000 ~ 0x007f ffff0xc000 0000 ~ 0xc07f ffff 都映射到物理地址 0x0000 0000 ~ 0x007f ffff 时,内核代码就不需要区分当前使用的是线性地址还是物理地址了(直接使用物理地址,经过页表转换后的值是一样的)。

注意,这里 0x0000 0000 ~ 0x007f ffff 的大小就刚好是 8M

确定了需要映射的内存范围后,便可以开始构造页表项。我们设置 swapper_pg_dir 的第 0、1 项的目的很明显(前面我们说了,8M 的内存,共需要 2 个页目录项),它们就对应着线性地址 0x0000 0000 ~ 0x007f ffff。这样一来,第 768、769 项应该就对应着内存 0xc000 0000 ~ 0xc07f ffff 了。这里我们关心都是,768 是怎么出来的?

我们知道,DIRECTORY 字段有 10 bits,共 2^10 = 1024 个目录项。0xc000 0000 刚好在 4G 内存的 3/4 处。利用这个信息,我们就能够计算 0xc000 0000 对应的页表项为 1024 * (3/4) = 768。另一种更直接的方法是,按照 MMU 转换线性地址时的方法,取 0xc000 0000 的前 10 个位,即得到 0x300

在最后,我们来看启用分页的一小段汇编代码:

1
2
3
4
5
movl $swapper_pg_dir-__PAGE_OFFSET,%eax
movl %eax,%cr3 /* set the page table pointer.. */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* ..and set paging (PG) bit */

swapper_pg_dir 就是我们构造的临时页表,问题在于,为什么需要减去 __PAGE_OFFSET(其实就是 0xc0000000)呢?答案在前面我们其实已经说过了,内核在链接(link)的时候,起始地址是 0xc000 0000。也就是说,从内核的视角来看,他以为自己是从 0xc0000000 开始的。$swapper_pg_dir - __PAGE_OFFSET 后,才是 swapper_pg_dir 真正的内存地址(在执行这一行代码的时候,CPU 还处于实模式,这个模式下,线性地址就是物理地址)。