概述

本文主要讲解下,linux kernel在实模式(real mode)中是如何建立所谓的临时页表的, 有了这些临时页表,便可以开启cpu页管理单元, 进入真正的保护模式(protect mode). 这里只考虑32位x86平台.

评估

在开始建立页表之前,我们有必要知道,我们到底需要映射多大的内存. 这里需要考虑几方面的要求:

  • 能够容纳整个kernel image.
  • 能够放下所有的页表项.
  • 不应该映射过大的空间,因为这些映射只是临时的.

之所以说是临时,因为现在我们还不知道真实的物理内存是多大, 当知道了真实的物理内存大小,我们需要重新建立合适的页表.

先来看kernel image有多大,这个信息我们可以通过链接脚本来获得:

SECTIONS
{

	...

	/* Text and read-only data */
	.text :  AT(ADDR(.text) - LOAD_OFFSET) {
		_text = .;
		...
		/* End of text section */
		_etext = .;
	} :text = 0x9090

	...

	/* Data */
	.data : AT(ADDR(.data) - LOAD_OFFSET) {
		/* Start of data section */
		_sdata = .;
		...
		/* End of data section */
		_edata = .;
	} :data

	...

	. = ALIGN(PAGE_SIZE);
	.brk : AT(ADDR(.brk) - LOAD_OFFSET) {
		__brk_base = .;
		. += 64 * 1024;		/* 64k alignment slop space */
		*(.brk_reservation)	/* areas brk users have reserved */
		__brk_limit = .;
	}

	_end = .;

    ...
}

这里有很多内容被省略了,我们来几个关键的导出符号:

  • _text,_etext: kernel代码段的开始和结束地址.
  • _sdata,_edata: kernel数据段的开始和结束地址.
  • __brk_base,__brk_limit: kernel初始时堆的开始与结束地址,需要注意的是,kernel将所有的初始化好的页表都放在堆的开始处.

NOTE: 这里所说的地址都是指运行时的虚拟地址.

知道了image的大小,下面就是要计算所有页表项需要占用的空间.

/* Enough space to fit pagetables for the low memory linear map */
MAPPING_BEYOND_END = PAGE_TABLE_SIZE(LOWMEM_PAGES) << PAGE_SHIFT

由于kernel只能使用线性地址空间中从PAGE_OFFSET(0xc0000000)开始的1Gb(1«32 - 0xc0000000)的空间,所以,我只需要计算这部分空间所占用的页表即可.

在代码中,这部分地址空间被称为LOWMEM,其所需的物理页的个数为:

/* Number of possible pages in the lowmem region */
LOWMEM_PAGES = (((1<<32) - __PAGE_OFFSET) >> PAGE_SHIFT)

下面需要考虑2中情况,一个是开启PAE功能,另一种是未开启的情形.

先来看PAE disable情形下,这种情形比较简答,因为是使用的2级分层模式, 所以只需要计算有几个页目录项即可,每个页目录项指向一个页表, 每个页表含有1024个页表项,每个页表项是对应物理页的物理地址, 所以,一个页表是4Kb,正好是一个物理页的大小, 那么我们只需要计算有几个页表即可:

#define PAGE_TABLE_SIZE(pages) ((pages) / PTRS_PER_PGD)

那么如果PAE enable呢,这时采用的是3级分层模式, 首先我们任然需要一个页目录项,不过这时它指向的是一个页中间目录(PMD), 该表含有512个表项,每个表项8个字节,指向一个页表, 所以该表页正好占一个物理页的大小,那么我们这里除了和之前一样计算需要多少页表, 还需要再加上一个页中间目录:

#define PAGE_TABLE_SIZE(pages) (((pages) / PTRS_PER_PMD) + PTRS_PER_PGD)

页表初始化

知道了我们需要映射的大小,下面就是具体的页表初始化了. 先来开PAE enable的情形, 其中页中间目录是存放kernel的BSS段中的:

initial_pg_pmd:
	.fill 1024*KPMDS,4,0

而页目录是存放在数据段中,其中有部分表项已被静态初始化(主要用于bios,不再本文讨论范围):

__PAGE_ALIGNED_DATA
	/* Page-aligned for the benefit of paravirt? */
	.align PAGE_SIZE
ENTRY(initial_page_table)
	.long	pa(initial_pg_pmd+PGD_IDENT_ATTR),0	/* low identity map */
# if KPMDS == 3
	.long	pa(initial_pg_pmd+PGD_IDENT_ATTR),0
	.long	pa(initial_pg_pmd+PGD_IDENT_ATTR+0x1000),0
	.long	pa(initial_pg_pmd+PGD_IDENT_ATTR+0x2000),0
# elif KPMDS == 2
	.long	0,0
	.long	pa(initial_pg_pmd+PGD_IDENT_ATTR),0
	.long	pa(initial_pg_pmd+PGD_IDENT_ATTR+0x1000),0
# elif KPMDS == 1
	.long	0,0
	.long	0,0
	.long	pa(initial_pg_pmd+PGD_IDENT_ATTR),0
# else
#  error "Kernel PMDs should be 1, 2 or 3"
# endif
	.align PAGE_SIZE		/* needs to be page-sized too */

剩下的页表都是放在预留的堆空间(从__brk_base开始):

	xorl %ebx,%ebx				/* 清0 ebx寄存器 */

	movl $pa(__brk_base), %edi /* edi = 页表开始的物理地址 */
	movl $pa(initial_pg_pmd), %edx /* edx = 页中间目录的开始地址 */
	movl $PTE_IDENT_ATTR, %eax /* eax = 表项flag(rw+present)*/
10:
	leal PDE_IDENT_ATTR(%edi),%ecx		/* ecx = 页表物理地址 | 表项flag */
	movl %ecx,(%edx)			/* 将组合的结果填入到页中间目录的第一个表项中 */
	addl $8,%edx /*指向一个页中间目录的表项 */
	movl $512,%ecx /* 设置循环计数为页目录中表项的个数(512) */
11:
	stosl /* eax = 物理地址 | 表项flag, 将组合的结果填入到对应的页表中的表项低32位 */
	xchgl %eax,%ebx /* eax = 0 */
	stosl /* 表项的高32位清0 */
	xchgl %eax,%ebx /* eax = 0 */
	addl $0x1000,%eax /* eax += 0x1000(一个物理页的大小) */
	loop 11b

	/*
	 * 循环结束的条件: 映射大小 >= end + MAPPING_BEYOND_END.
	 */
	movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
	cmpl %ebp,%eax
	jb 10b
1:
	addl $__PAGE_OFFSET, %edi
	movl %edi, pa(_brk_end) /* _brk_end = 映射的最大地址 */
	shrl $12, %eax
	movl %eax, pa(max_pfn_mapped) /* max_pfn_mapped = 最大的映射的物理页号

	/* 建立fix map映射表(暂不讨论) */
	movl $pa(initial_pg_fixmap)+PDE_IDENT_ATTR,%eax
	movl %eax,pa(initial_pg_pmd+0x1000*KPMDS-8)

当PAE disable, 页目录是存放在BSS段中的:

ENTRY(initial_page_table)
	.fill 1024,4,0

页表初始化的过程和PAE enable时差不多:

	/*
	 * kernel开始的虚拟地址在页目录的偏移 = ((__PAGE_OFFSET >> 22) << 2)
	 * 因为每个表项大小为4个字节,所以需要左移2位
	 */
page_pde_offset = (__PAGE_OFFSET >> 20);

	movl $pa(__brk_base), %edi
	movl $pa(initial_page_table), %edx
	movl $PTE_IDENT_ATTR, %eax
10:
	leal PDE_IDENT_ATTR(%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 the end + MAPPING_BEYOND_END.
	 */
	movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
	cmpl %ebp,%eax
	jb 10b
	addl $__PAGE_OFFSET, %edi
	movl %edi, pa(_brk_end)
	shrl $12, %eax
	movl %eax, pa(max_pfn_mapped)

	/* Do early initialization of the fixmap area */
	movl $pa(initial_pg_fixmap)+PDE_IDENT_ATTR,%eax
	movl %eax,pa(initial_page_table+0xffc)

开启页管理单元

万事俱备,只欠东风,最后一步便是开始页管理单元, 这里需要做2件事:

  • 将页目录的物理地址放入cr3寄存器.
  • 将cr0中的PG标记置1.

代码中也的确是这么做的:

movl $pa(initial_page_table), %eax
movl %eax,%cr3		/* set the page table pointer.. */
movl $CR0_STATE,%eax
movl %eax,%cr0		/* ..and set paging (PG) bit */

FIN.