Linux页表 – – 启动过程临时页表创建过程

  • Post author:
  • Post category:linux



目录


1.为什么需要虚拟内存地址


2.页表


3.内核启动初始页表创建


获取内核在内存中起始页地址


__create_page_tables


1.为什么需要虚拟内存地址

个人理解通过创建页表实现物理地址到虚拟地址的映射,通过虚拟地址、虚拟空间实现了不同应用进程地址空间的隔离,表象上给应用空间提供了足够的内存空间;将内存的管理更多的交给底层系统进行实现,同时避免了物理内存的直接暴露一定程度上提高了系统的安全性。

2.页表

页表用时实现内存物理地址到虚拟地址的映射,实现虚拟地址到物理的转化;是虚、实内存地址转化的媒介。页表,是由物理地址向虚拟地址映射时创建的,由软件代码逻辑进行实现;虚拟地址向物理地址的转化需要借助MMU硬件单元实现。

如上是Arm64系统虚拟地址宽度39bit、(va_bit = 39)page size 为4k(page_shift = 12)、PTG level为3(3级页表)的页表,图中体现了虚拟地址转化为物理地址的实现逻辑。

虚拟地址向物理地址转化过程:

1) A1 取虚拟地址

PGD

段,做为 PGD 数组的下标;

2) B1  PGD[

A1

] 值为该虚拟地址对应的 PMD 的基础物理地址;

3) A2 取虚拟地址

PMD

段,做为 PMD 数组的下标;

4) B2 PMD[

A2

]值为该虚拟地址对应的 PTE 的基础物理地址;

5)  A3 取虚拟地址

PTE

段,做为 PTE 数组的下标;

6) B3  PTE[

A3

] 值为该虚拟地址对应的物理地址的PFN (PFN: page-frame                         number,页帧号);

7)  A4 取虚拟地址

page

段,做为该虚拟地址对应的物理地址页内偏移;

8) PFN +

page

段即为该虚拟地址对应的物理地址

系统中虚拟地址转化物理地址时,还有其他的因素要考虑,如:虚拟地址是内核段的还是用户段、对应的内存属性等,但这些不是本章重点。本章的重点在于介绍虚拟地址到物理地址转换逻辑,对PGD有初步了解、为下一章内核启动过程临时页表的创建做基础准备。

备注:PFN  page-frame number 页帧号表示内存地址所在页的页编号,是针对内存的物理地址右移page_shift后的值。以4K page size系统为例PFN即保留了物理地址右移12的值。

3.内核启动初始页表创建

#define __PHYS_OFFSET	(KERNEL_START - TEXT_OFFSET)

ENTRY(stext)
	bl	preserve_boot_args
	bl	el2_setup			           // Drop to EL1, w0=cpu_boot_mode
	adrp	x23, __PHYS_OFFSET         //@1 获取内核的物理地址
	and	x23, x23, MIN_KIMG_ALIGN - 1   // KASLR offset, defaults to 0
	bl	set_cpu_boot_mode_flag
	bl	__create_page_tables           //@2 创建初始页表
	/*
	 * The following calls CPU setup code, see arch/arm64/mm/proc.S for
	 * details.
	 * On return, the CPU will be ready for the MMU to be turned on and
	 * the TCR will have been set.
	 */
	bl	__cpu_setup			// initialise processor
	b	__primary_switch
ENDPROC(stext)

  • 获取内核在内存中起始页地址


KERNEL_START

为_text 即内核头入口虚拟地址


TEXT_OFFSET

为0x80000 (未开启CONFIG_ARM64_RANDOMIZE_TEXT_OFFSET     时),表示kenerl image距ram起始地址的偏移。在内核启动时,ram的前0x80000内存空间用于存储uboot传递内核的启动参数


__PHYS_OFFSET

从如上的定义可知为内核在内存起始虚拟地址;



adrp    x23, __PHYS_OFFSET


//获取内核在内存中的起始页地址;关于adrp指令解析,可参考

ARMv8汇编指令-adrp、adr、adr_l_业余程序员plus的博客-CSDN博客_adr指令



说明。

  • __create_page_tables

该函数通过调用create_pgd_entry、create_table_entry、create_block_map来创建内核启动过程中临时内存映射,会分别以 idmap_pg_dir 、swapper_pg_dir为页表空间来完成临时页表的创建。

其中创建页表的核心函数create_table_entry如下,在三级页表结构中此函数主要完成PGD、PMD两级页表的创建。

以创建PGD页表过程为例,

入参说明: tbl 指需要创建页表物理地址

virt 为映射到的虚拟地址

shift 为映射PGD索引在虚拟地址中的偏移,即PGDIR_SHIFT

ptrs 为PGD索引在虚拟地址中的位宽度,即PTRS_PER_PGD

tmp1 、tmp2为临时变量

创建过程: 1) 从virt中获取 PGD 页表 index

2) 获取一下级页表的基地址,即PMD页表物理基地址

3) 给PMD页表物理地址设置有效标识,表明该地址是有效的PMD                                 type table

4) 将PMD页表基地址写入PGD[

index

]

5) 将tbl指向一下级页表基地址,为下一级页表的创建做准备

/*
 * Macro to create a table entry to the next page.
 *
 *	tbl:	page table address,页表地址
 *	virt:	virtual address,映射的虚拟地址
 *	shift:	#imm page table shift, 映射位在字中的偏移量
 *	ptrs:	#imm pointers per table page,映射位宽度
 *
 * Preserves:	virt
 * Corrupts:	tmp1, tmp2
 * Returns:	tbl -> next level table page address
 */
	.macro	create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
/* virt 向右移动shift位置,准备获取也表项索引 */
	lsr	\tmp1, \virt, #\shift	

/* table index,按位与操作,保留了tmp1中对应的ptrs-1位数据其他位清零,即获取到页表项索引 */
	and	\tmp1, \tmp1, #\ptrs - 1	

/* tmp2 = tbl + PAGE_SIZE,idmap_pg_dir包含多级页表(每个页表占用一个page),故此处目的会获取下一级也表项入口地址 */
	add	\tmp2, \tbl, #PAGE_SIZE	

/* address of next table and entry type, 按位或操作,设置下一页表项标志位PMD_TYPE_TABLE */	
	orr	\tmp2, \tmp2, #PMD_TYPE_TABLE	

/* 
 * 将下一级页表项基地址写入当前的页表项,
 * 当前页表项地址 = tbl + (tmp1 << 3) 
 * 因每个页表项占用8byte,故对应页表相对页表基地址的偏移应为tmp1 << 3
 * 偏移应为tmp1 << 3
*/
	str	\tmp2, [\tbl, \tmp1, lsl #3]	

/* next level table page ,指向下一级页表 */								
	add	\tbl, \tbl, #PAGE_SIZE	
	
	.endm

如上即完成了PGD页表项的填充,PMD页表项的填充过程与PGD页表项填充过程相同。PTE页表的创建通过   create_block_map 函数完成。

/*
 * Macro to populate block entries in the page table for the start..end
 * virtual range (inclusive).
 * tbl 页表项的物理基地址
 * flags 需要映射页表flag
 * phys 需映射的物理地址
 * start 物理地址映射到的虚拟空间的起始地址
 * end 物理地址映射到的虚拟空间的结束地址
 * Preserves:	tbl, flags
 * Corrupts:	phys, start, end, pstate
 */
	.macro	create_block_map, tbl, flags, phys, start, end
	/* 获取需要映射的物理地址对应的PFN */
	lsr	\phys, \phys, #SWAPPER_BLOCK_SHIFT
	
    /* table start-index,获取start在tbl表中起始索引 */
	lsr	\start, \start, #SWAPPER_BLOCK_SHIFT    
	and	\start, \start, #PTRS_PER_PTE - 1	
	
	/* table entry,获取物理地址所在的物理页地址,并给物理页地址设置页表flag */
	orr	\phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT	
	
	/* table end-index,获取start在tbl表结束索引 */
	lsr	\end, \end, #SWAPPER_BLOCK_SHIFT
	and	\end, \end, #PTRS_PER_PTE - 1
	
	/* 将需要映射物理页地址 写入 虚拟地址对应的 tbl[index]*/
9999:	str	\phys, [\tbl, \start, lsl #3]		// store the entry
	add	\start, \start, #1			// next entry
	add	\phys, \phys, #SWAPPER_BLOCK_SIZE		// next block
	cmp	\start, \end
	b.ls	9999b
	.endm

PTE页表同上两级页表的创建过程比较类似,需要注意的点在于:1) 入参 ,因为是创建PTE页表项,所以需要传入要映射的物理地址phys;此时的tbl页表基地址也已经指向PTE 页表的物理基地址。2)创建过程,是将要映射的物理页地址填充到虚拟地址对应的PTE页表,要映射的空间大小通过入参中起始虚拟地址和结束虚拟地址来实现。 到此即完成三级页表的创建。

启动过程中要针对哪些物理地址创建虚拟映射?这就要说到提到的idmap_pg_dir 、swapper_pg_dir页表空间。

__create_page_tables:

	/*
	 * Create the identity mapping.
	 */
	adrp	x0, idmap_pg_dir    // __pa(idmap_pg_dir)
	adrp	x3, __idmap_text_start		// __pa(__idmap_text_start)

	create_pgd_entry x0, x3, x5, x6
	mov	x5, x3				// __pa(__idmap_text_start) @1
	adr_l	x6, __idmap_text_end		// __pa(__idmap_text_end)
	create_block_map x0, x7, x3, x5, x6

	/*
	 * Map the kernel image (starting with PHYS_OFFSET).
	 */
	adrp	x0, swapper_pg_dir    // __pa(swapper_pg_dir)
	mov_q	x5, KIMAGE_VADDR + TEXT_OFFSET	// compile time __va(_text)
	create_pgd_entry x0, x5, x3, x6
	adrp	x6, _end			// runtime __pa(_end)
	adrp	x3, _text			// runtime __pa(_text)
	sub	x6, x6, x3			// _end - _text
	add	x6, x6, x5			// runtime __va(_end)
	create_block_map x0, x7, x3, x5, x6

如下是简化后的 idmap  和 kernel 映射代码,从代码逻辑可知:


idmap_pg_dir

是 __idmap_text_start 到 __idmap_text_end 段内核代码物理地址映射到虚拟地址的页表项空间,因 x5 = x3 = __pa(__idmap_text_start) 故该页表空间映射的虚拟地址与物理地址一致。__idmap_text_start 到 __idmap_text_end内存空间保存了section “.idmap.text” 代码,该代码函数主要实现MMU的开关。


swapper_pg_dir

是 _text 到 _end 内核代码运行的物理地址创建映射的页表空间,该页表空间的虚拟起始地址为 KIMAGE_VADDR + TEXT_OFFSET,虚拟空间大小为内核空间大小(_text -_end),物理起始地址为内核加载到物理内存_text的地址。

经此地址映射并开启MMU后, CPU访问的内核代码的虚拟地址与vmlinux.lds的内核链接地址及system.map符号表地址一致(vmlinux.lds地址为虚拟地址)。

备注:1) idmap_pg_dir 、swapper_pg_dir 保存的页表怎么使用?

在enable_mmu函数中idmap_pg_dir 、swapper_pg_dir的地址分别被存进了ttbr0_el1 和 ttbr1_el1, Arm64会根据其他状态寄存来决定使用哪个ttbr(Translation Table Base Register)。

2)__idmap_text_start 到__idmap_text_end空间通过idmap_pg_dir 、swapper_pg_dir两个页表都可以访问?

个人观点:是可以,idmap_pg_dir页表对应的物理空间为__idmap_text_start 到__idmap_text_end。 swapper_pg_dir页表项对应的物理空间为整个内核段,而__idmap_text_start 到__idmap_text_end段包含在内核段。



版权声明:本文为hello_yj原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。