目录
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段包含在内核段。