linux 临时页目录和页表初始化分析

Posted 青青子衿,悠悠我心。

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux 临时页目录和页表初始化分析相关的知识,希望对你有一定的参考价值。

linux 内核在编译的时候会初始化一个静态的临时的 全局页目录(page global directory) 和一个 页表(page table)。初始化是在 arch/i386/kernel/head.S 中的 startup_32 汇编函数中初始化的。这个还包含了其他的初始化部分,因此,只截取和分页相关的部分,部分汇编代码如下:

/*
 * Initialize page tables.  This creates a PDE and a set of page
 * tables, which are located immediately beyond _end.  The variable
 * init_pg_tables_end is set up to point to the first "safe" location.
 * Mappings are created both at virtual address 0 (identity mapping)
 * and PAGE_OFFSET for up to _end+sizeof(page tables)+INIT_MAP_BEYOND_END.
 *
 * Warning: don\'t use %esi or the stack in this code.  However, %esp
 * can be used as a GPR if you really need it...
 */
page_pde_offset = (__PAGE_OFFSET >> 20);

	movl $(pg0 - __PAGE_OFFSET), %edi
	movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
	movl $0x007, %eax			/* 0x007 = PRESENT+RW+USER */
10:
	leal 0x007(%edi),%ecx			/* Create PDE entry */
	movl %ecx,(%edx)			/* Store identity PDE entry */
	movl %ecx,page_pde_offset(%edx)		/* Store kernel PDE entry */
	addl $4,%edx
	movl $1024, %ecx
11:
	stosl
	addl $0x1000,%eax
	loop 11b
	/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
	/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
	leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
	cmpl %ebp,%eax
	jb 10b
	movl %edi,(init_pg_tables_end - __PAGE_OFFSET)

swapper_pg_dir 的值是全局页目录的线性地址,但是由于它的线性地址是在0xC0000000之后的,所以只需要将它减去0xC0000000即 __PAGE_OFFSET 之后,就可以得到它的物理地址,换句话说,这行代码:

movl $(swapper_pg_dir - __PAGE_OFFSET), %edx

是将 swapper_pg_dir 的物理地址保存在 %edx 寄存器中。同样,这一样代码:

movl $(pg0 - __PAGE_OFFSET), %edi

是将 pg0 的值也就是页表的物理地址保存在 edi 寄存器中。内核的线性地址是从0xC0000000开始的,但是实际上的物理地址是从0x00000000开始的。第一阶段的页表初始化的目的是为了不管是实模式还是受保护模式,都可以通过分页机制访问内存,所以需要将从0x00000000开始的物理地址和0xC0000000开始的线性的地址都映射到对应的页表和页目录项中。0x00000000的页目录索引为0,0xC0000000的页目录索引为768,所以页目录的初始化从这两处开始,具体的代码如下:

leal 0x007(%edi),%ecx			/* Create PDE entry */
movl %ecx,(%edx)			/* Store identity PDE entry */
movl %ecx,page_pde_offset(%edx)		/* Store kernel PDE entry */
addl $4,%edx

edi 寄存器的值 pg0 的地址,也就是页表的起始地址,加上0x007是为了添加对应的权限标志位。将计算的值保存在 ecx 寄存器之后,在将它写入 内存地址为 edx 处,从前面就可以知道,edx 的值是 swapper_pg_dir 的地址,也就是全局页目录的地址 。

接下来继续将 ecx 寄存器的值写入到页表偏移 page_pde_offset 字节的地址,这个 page_pde_offset 是什么东西呢?它的定义如下:

page_pde_offset = (__PAGE_OFFSET >> 20);

而 __PAGE_OFFSET 的含义是内核线性地址的起始地址,在 i386 架构下它的值是0xC0000000,那么乍一看将 __PAGE_OFFSET 右移20位是一个很奇怪的事。但是,我们知道,如果要计算一个线性地址的全局页目录的索引,只需要将线性地址右移22位即可。由于全局页目录的每一项的大小为4个字节,因此,如果要计算某一项的偏移的字节数量,还需要乘以4,或者换个说法,左移2位,到这里就很明白了,右移动20位就相当于先右移动22位,然后再向左移动2位,这么一抵消,也只需要右移20位即可,因此 page_pde_offset 计算的是线性地址 __PAGE_OFFSET 对应的全局页目录项偏移的字节数量,更直白一点,就是项768偏移的字节数量。这样,这一行代码:

movl %ecx,page_pde_offset(%edx)		/* Store kernel PDE entry */

还是页表的物理地址写入到全局页目录中的768项中。写完之后,将 edx 的值加4,毕竟需要往后移动一项了。

接下来开始填充页表,首先是这条指令:

movl $1024, %ecx

将1024写入到寄存器 ecx 中,这条指令是和下面的 stosl 指令联动的,用来表示 stosl 将会循环执行1024次。接着从物理地址0x00000000(物理页帧0的地址)开始,将地址值写入对应的表项,每循环一次将地址加上0x1000(下一物理页帧的地址)。同时, edi 的寄存器的值也会加4(下一表项的地址)。待循环结束之后,edi 寄存器的值就是紧随着页表之后的起始地址。到这里还远远没有结束,那么填充什么时候结束呢?这就和 INIT_MAP_BEYOND_END 宏有关了,该宏的定义如下:

#define INIT_MAP_BEYOND_END (128*1024)

128*1024的含义是128KB,再结合下面的指令:

leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
cmpl %ebp,%eax
jb 10b

可以知道,映射的物理页地址必须大于或者等于页表之后再偏移128KB(也就是 INIT_MAP_BEYOND_END)的地址,如果小于,那么就跳回 10b,继续重复上述的过程。最后将 edi 寄存器(也就是页表再加上128KB的地址)写入到 init_pg_tables_end 这个变量中,这个变量表示的就是内核的结束地址。

以上是关于linux 临时页目录和页表初始化分析的主要内容,如果未能解决你的问题,请参考以下文章

Linux 是不是对页目录和页表使用自映射?

理论+实例,带你掌握Linux的页目录和页表

Linux从头学15:页目录和页表-理论 + 实例 + 图文的最完全最接地气详解

Linux从头学15:页目录和页表-理论 + 实例 + 图文的最完全最接地气详解

Linux从头学16:操作系统在加载应用程序时,是如何把页目录和页表当做普通物理页进行操作的?

Linux从头学16:操作系统在加载应用程序时,是如何把页目录和页表当做普通物理页进行操作的?