现代操作系统:原理与实现配套实验ChCore-02
邮箱:wanglu082@yeah.net
欢迎交流~
问题 1
请简单解释,在哪个文件或代码段中指定了 ChCore 物理内存布局。你可以从两个方面回答这个问题: 编译阶段和运行时阶段。
ChCore的物理内存布局可分为以下几部分:
-
保留(0x00000-0x80000)
-
Bootloader
保留区的end地址,也就是Bootloader的起始地址定义在链接脚本中。
Bootloader包含在镜像文件Kernel.img中,所以img_start = init_star=0x80000.
. = TEXT_OFFSET; /* 当前指针赋值为 TEXT_OFFSET, 即 0x80000 */ img_start = .; /* 镜像开始地址(ELF 文件入口地址)设为当前指针,即 0x80000 */ init : { ${init_object} /* 指定 .init 段的内容为 init_object,即 bootloader 编译后的机器码 */ }
整个init段存放的就是Bootloader的代码和全局变量,其结束地址就是Bootloader的结束地址:
. = ALIGN(SZ_16K); /* 将当前指针对齐至 16K */ init_end = ABSOLUTE(.); /* 记录下对齐后的当前指针的值,便于后面使用 */
-
内核
内核部分包含的内容就是内核代码和全局变量,具体来说就是.text .data .rodata .bss.
链接脚本中也为他们指派了地址(注意是LMA):
/* 将 text 段 VMA 设置为 KERNEL_VADDR + init_end, LMA 设置为 init_end */ /* KERNEL_VADDR 在 boot/image.h 被设置为 0xffffff000000000 */ .text KERNEL_VADDR + init_end : AT(init_end) { *(.text*) } /* 下面的处理同上,不特殊指定的话 LMA 和 VMA 都会自动递增 */ . = ALIGN(SZ_64K); .data : { *(.data*) } . = ALIGN(SZ_64K); .rodata : { *(.rodata*) } _edata = . - KERNEL_VADDR; _bss_start = . - KERNEL_VADDR; .bss : { *(.bss*) } _bss_end = . - KERNEL_VADDR; . = ALIGN(SZ_64K); img_end = . - KERNEL_VADDR;
img_end就是整个内核区域的结束地址。至此链接脚本结束,再向上的物理内存划分就不归他管了。
-
页面元数据
物理页分配器将img_end向上的内存划分为页面元数据和页面两个范围,他们的大小与页面数(npages)有关。
页面元数据存储了:空闲页面链表和页面的属性。
-
页面区
页面区存放需要用的物理页。
页面区的大小 = 页面数(npages)* PAGE_SIZE,PAGE_SIZE在
mm.h
中被定义为4K。
页面元数据区和页面区的地址设定在/kernel/mm.c中的mm_init()
中实现(见下方),因为此时已经启用了MMU,所以操作的地址也必然是虚拟地址。
虽然是虚拟地址,但是内核地址空间VA和PA的映射关系是virtual address = physical address + KBASE
void mm_init(void)
{
vaddr_t free_mem_start = 0;
struct page *page_meta_start = NULL;
u64 npages = 0;
u64 start_vaddr = 0;
kdebug("img_end:0x%lx\n", &img_end); //----------------------------------------(1)
/* 内核镜像结束地址往上称为: free_mem */
/* 将 free_mem_start 指定为 img_end(0xa0000)并对齐页面大小 */
free_mem_start =
phys_to_virt(ROUND_UP((vaddr_t) (&img_end), PAGE_SIZE));
/* 预留最大页数 */
npages = NPAGES;
/* 规定 (24M + KBASE) 是页面区的起始地址 */
start_vaddr = START_VADDR;
kdebug("[CHCORE] mm: free_mem_start is 0x%lx, free_mem_end is 0x%lx, PHYSICAL_MEM_END=0x%lx\n",
free_mem_start, phys_to_virt(PHYSICAL_MEM_END), PHYSICAL_MEM_END);
/* 从start_vaddr开始就是页面区了,自然页元数据的长度不能超过start_vaddr */
/* 这里没有规定元数据区的结束地址,只是在上面说明了页面区的起始是24M+KBASE,确保留给元数据区的空间足够 */
if ((free_mem_start + npages * sizeof(struct page)) > start_vaddr) {
BUG("kernel panic: init_mm metadata is too large!\n");
}
/* 页面元数据区的起始地址 */
page_meta_start = (struct page *)free_mem_start;
kdebug("page_meta_start: 0x%lx, real_start_vadd: 0x%lx,"
"npages: 0x%lx, meta_page_size: 0x%lx\n",
page_meta_start, start_vaddr, npages, sizeof(struct page));
/* buddy alloctor for managing physical memory */
init_buddy(&global_mem, page_meta_start, start_vaddr, npages);
/* slab alloctor for allocating small memory regions */
init_slab();
// map_kernel_space(KBASE + (128UL << 21), 128UL << 21, 128UL << 21);
map_kernel_space(KBASE + (128UL << 1), 128UL << 1, 128UL << 1);
//check whether kernel space [KABSE + 256 : KBASE + 512] is mapped
kernel_space_check();
}
为了方便理解,将上段代码以图的形式表现出来:
+---------------------+-> free_mem_end=phy_to_virt(PHYSICAL_MEM_START+NPAGES*PAGESIZE)
| | =0xffffff0020c00000
| |
| |
| Page |
| |
| |
| |
| |
+-----------------------> start_vaddr=phy_to_virt(PHYSICAL_MEM_START)
| | =0xffffff0001800000
| Page metadata |
| |
| |
+-----------------------> img_end(align PAGESIZE)=free_mem_start=page_meta_start
| | =0xffffff000000a000
+-----------------------> img_end
| Kernel Image |
+-----------------------> img_start
| |
| ..... |
+---------------------+
练习1
实现kernel/mm/buddy.c中的四个函数:buddy_get_pages(),split_page(), buddy_free_pages(), merge_page()。请参考伙伴块索引等功能的辅助函数:get_buddy_chunk()。
在完善任务函数之前,肯定得先看它提供的几个写好的函数。包括初始化函数init_buddy
和寻找伙伴函数get_buddy_chunk
。
一、初始化伙伴系统,其实就包括对上一个问题中的页面元数据区和页面区的初始化。
传入的参数包括:
- 内存池结构体地址;其中保存着页面区和页元数据区的起始地址、大小等。还有一个最重要的空闲页块链表
free_lists
。 - 页面区和页元数据区的起始地址、初始化的页面数量。
/*
* start_page: 页元数据区的起始地址(经过对齐)
* start_addr: 页面区的起始地址
*/
void init_buddy(struct phys_mem_pool *pool, struct page *start_page,
vaddr_t start_addr, u64 page_num)
{
int order;
int page_idx;
struct page *page;
/* Init the physical memory pool. */
pool->pool_start_addr = start_addr;
pool->page_metadata = start_page;
/* 页面区的大小 = page_num * 页面大小 */
pool->pool_mem_size = page_num * BUDDY_PAGE_SIZE;
/* This field is for unit test only. */
pool->pool_phys_page_num = page_num;
/* Init the free lists */
for (order = 0; order < BUDDY_MAX_ORDER; ++order) {
pool->free_lists[order].nr_free = 0;
init_list_head(&(pool->free_lists[order].free_list));
}
/* Clear the page_metadata area. */
memset((char *)start_page, 0, page_num * sizeof(struct page));
/* 初始化页面元数据区,即初始化每个struct page */
/* 初始设定了page_num个空闲页块,每个页块的order=0 */
for (page_idx = 0; page_idx < page_num; ++page_idx) {
page = start_page + page_idx; /* 结构体指针+1 */-------------------------(1)
page->allocated = 1; /* 标记已使用,才能进一步使用buddy_free_pages回收合并 */
page->order = 0;
}
/* 合并回收所有页块 */
for (page_idx = 0; page_idx < page_num; ++page_idx) {
page = start_page + page_idx;
buddy_free_pages(pool, page);------------------------------------------(2)
}
}
(1) 这里结构体指针+page_idx相当与start_page + page_idx * sizeof(struct page)
,可以自己验证一下。
(2) 可以发现最后用到了一个需要我们自己实现的函数buddy_free_pages
。前面一个for循环中将全部的page->order设为0,那么最终我们肯定要将有兄弟页块的page进行合并,放入合适的链表中的。这就是buddy_free_pages
的任务。
二、寻找伙伴
在释放页块时会遇到需要合并两个伙伴块为更大阶数(order)的块的情况,此时就需要快速找到他的伙伴块。
整个函数很简单,只要注意:
buddy_chunk_addr = chunk_addr ^
(1UL << (order + BUDDY_PAGE_SIZE_ORDER));
不难证明,两个伙伴块的虚拟地址只有1位不同,且这一位与块的大小有关。例如,大小位8KB的两个伙伴块的地址分别是0x1000和0x2000,那么他们只有13位不同(8K=2^13)。
下面开始完成练习:
从哪里入手呢,一个好的方法是从测试函数入手。阅读测试方法再结合它给的注释就能比较容易理解需要我们实现的功能以及一些细节。
...
/* skip the metadata area */
init_buddy(&global_mem, start, start_addr, npages);
...
测试中需要我们关注的第一句就是buddy系统的初始化。上面我们对其的分析中说了:buddy_free_pages
需要我们来实现。
执行buddy_free_pages
之前,所有的page.allocated=1
,page.order=0
,且所有的页块都没有挂在free_lists上。
而执行完init
后我们的理想条件是:有伙伴页块的page进行合并,每个page的分配状态(allocated)都是0,且都被挂在到正确的空闲链表上。
释放已分配的页块,既然是已分配那么就肯定不在空闲链表中。
void buddy_free_pages(struct phys_mem_pool *pool, struct page *page)
{
// <lab2>
/* 空闲的块不需要回收 */
if (!page->allocated) {
return;
}
page->allocated = 0; //-------------------------------------------(1)
/* join in proper free_list after merging the buddy pages */
merge_page(pool, page);
// </lab2>
}
(1) 对空闲块在此不先加入链表,而是经过合并操作确定了它最终的位置后(其实就是确定了order)再执行list_append
。
对页块的合并是递归实现的:
static struct page *merge_page(struct phys_mem_pool *pool, struct page *page)
{
// <lab2>
if (page->allocated) {
/* error: can't merge allocated pages */
return NULL;
}
struct page *buddy_page = get_buddy_chunk(pool, page);
/* 递归的界限:order不合法或伙伴页块不可用 */
if (page->order == BUDDY_MAX_ORDER-1 || buddy_page == NULL || \ //-----------------(1)
buddy_page->allocated || page->order != buddy_page->order) {
/* 经过可能的合并操作确定了page的最终位置,
此时再将页块加入相应链表 */
page_append(pool, page);
return page;
}
/* 其伙伴页块可以合并, 先要将其移除原来的空闲链表 */
page_del(pool, buddy_page);
/* 统一page页块和其伙伴页块的相对位置关系 */
/* | (page) | (buddy_page) | */
if(page > buddy_page) {
struct page *tmp = buddy_page;
buddy_page = page;
page = tmp;
}
/* 确保调整位置之后的伙伴页块是已分配的状态 */
buddy_page->allocated = 1;
page->order++;
return merge_page(pool, page);
}
(1) 注意BUDDY_MAX_ORDER的合法范围是开区间()。在buddy.h
中做了声明。
完成了释放页块后,对应的也要实现分配页块函数:buddy_get_pages
:
函数的目的是找到一个空闲的特定大小的页块,将他从空闲列表中移除,标记状态为:已分配。
struct page *buddy_get_pages(struct phys_mem_pool *pool, u64 order)
{
// <lab2>
struct page *page = NULL;
struct page *splitted_page = NULL;
struct list_head *free_node = NULL;
u64 order_index = order;
if (order > BUDDY_MAX_ORDER) {
/* error */
return NULL;
}
/* 找到合适的页块来存 */
while (order_index < BUDDY_MAX_ORDER && pool->free_lists[order_index].nr_free == 0) {
order_index++;
}
if (order_index >= BUDDY_MAX_ORDER) {
/* not find, error */
}
free_node = pool->free_lists[order_index].free_list.next;
splitted_page = list_entry(free_node, struct page, node);
/* mark this page is unallocated temp and delete from corresonding list */
splitted_page->allocated = 0;
page_del(pool, splitted_page);
/* 如果需要分割,就进行拆分 */
page = split_page(pool, order, splitted_page);
page->allocated = 1;
return page;
// </lab2>
}
用于拆分页块的函数split_page
也是需要我们实现的:
参数order是目标order。
order递减代表页块对半拆分(1<<order),其中一块作为空闲块加入链表,另一块用于向下递归,不加入空闲链。直到找到拆出对应大小的页块(page->order == order
)。
static struct page *split_page(struct phys_mem_pool *pool, u64 order,
struct page *page)
{
// <lab2>
if (page->allocated) {
/* 只能拆分空闲块 */
return NULL;
}
/* 递归的界限:分割出目标order的页块 */
if (page->order == order) {
return page;
}
page->order--;
struct page *buddy_page = get_buddy_chunk(pool, page);
if (buddy_page != NULL) {
buddy_page->allocated = 0;
buddy_page->order = page->order;
page_append(pool, buddy_page);
}
/* 递归调用 */
return split_page(pool, order, page);
}
问题2
AArch64 采用了两个页表基地址寄存器,相较于 x86-64 架构中只有一个页表基地址寄存器,这样的好处是什么?请从性能与安全两个角度做简要的回答。
性能:例如应用程序请求系统调用等过程,不需要切换页表,也就省去了TLB刷新等操作。
安全:系统进程与用户进程地址空间相互隔离,从地址转换的方面提升了安全性。
问题3
1.请问在页表条目中填写的下一级页表的地址是物理地址还是虚拟地址?
物理地址。
2.在 ChCore 中检索当前页表条目的时候,使用的页表基地址是虚拟地址还是物理地址?
物理地址。AArch64下存储在TTBR0_EL1或TTBR1_EL1。
AArch64下基于4级页表的地址翻译过程(图源:现代操作系统:原理与实现):
练习2
a 知识梳理
名词解释:
-
page table entry:页表项(页表条目)
-
page table page:页表(一个页面,里边存储的是pte)
页面大小为4KB时,AArch64使用4级页表机制。
一个页表项(pte_t)占8个字节,所以一个页表中能包含PTP_ENTRIES=2^9
个页表项。
页表的数据结构,ptp_t:
/* page_table_page type */
typedef struct {
pte_t ent[PTP_ENTRIES];
} ptp_t;
页表本质上是一个物理页,特殊之处是其内部存储的若干页表项(pte_t)。
页表项的数据结构,pte_t
/* table format */
typedef union {
struct {
u64 is_valid:1, is_table:1, ignored1:10, next_table_addr:36, reserved:4, ignored2:7, PXNTable:1, // Privileged Execute-never for next level
XNTable:1, // Execute-never for next level
APTable:2, // Access permissions for next level
NSTable:1;
} table;
struct {
u64 is_valid:1, is_table:1, attr_index:3, // Memory attributes index
...
reserved1:4, nT:1, reserved2:13, pfn:18, reserved3:2, GP:1, reserved4:1, DBM:1, // Dirty bit modifier
Contiguous:1, PXN:1, // Privileged execute-never
UXN:1, // Execute never
soft_reserved:4, PBHA:4; // Page based hardware attributes
} l1_block;
struct {
u64 is_valid:1, is_table:1, attr_index:3, // Memory attributes index
...
reserved1:4, nT:1, reserved2:4, pfn:27, reserved3:2, GP:1, reserved4:1, DBM:1, // Dirty bit modifier
Contiguous:1, PXN:1, // Privileged execute-never
UXN:1, // Execute never
soft_reserved:4, PBHA:4; // Page based hardware attributes
} l2_block;
struct {
u64 is_valid:1, is_page:1, attr_index:3, // Memory attributes index
...
pfn:36, reserved:3, DBM:1, // Dirty bit modifier
Contiguous:1, PXN:1, // Privileged execute-never
UXN:1, // Execute never
soft_reserved:4, PBHA:4, // Page based hardware attributes
ignored:1;
} l3_page;
u64 pte;
} pte_t;
其有多种类型:table、l1_block、l2_block、l3_page、pte.
- table:table descriptor ,包含下一级页表的地址
- l1_block:block descriptor
- l2_block:block descriptor
- l3_page:block descriptor ,最后一级页表
上面说了页表项有4中类型,且其大小是确定的。于是使用联合体+位域来实现。
上面还漏了一个元素
pte
,这个使用位域时为了快速操作联合体设立的,无实际意义。
b 代码练习
对应给出的任务函数的注释,结合测试函数,分析要实现函数的功能。
测试函数:
root = get_pages(0); /* 分配4K对齐的大小为4KB的内存地址 */
/* 刚分配的页表root无内容,所以查询va对应的pa一定会触发error */
printf("testing function 'query_in_pgtbl'...\n");
va = 0x100000;
err = query_in_pgtbl(root, va, &pa, &entry);
printf("err = %d\n", err);
mu_assert_int_eq(-ENOMAPPING, err);
/* 在 [va, va+PAGE_SIZE] 到 [pa, pa+PAGE_SIZE] 之间建立映射 */
printf("testing function 'map_range_in_pgtbl'...\n");
err = map_range_in_pgtbl(root, va, 0x100000, PAGE_SIZE, DEFAULT_FLAGS);
printf("err = %d\n", err);
mu_assert_int_eq(0, err);
/* 映射建立之后,再去查找va对应的pa,结果应该是0x100000 */
printf("testing function 'query_in_pgtbl'...\n");
err = query_in_pgtbl(root, va, &pa, &entry);
printf("err = %d\n", err);
printf("pa = 0x%llx\n", pa);
mu_assert_int_eq(0, err);
mu_check(pa == 0x100000);
// mu_check(flags == DEFAULT_FLAGS);
/* 测试取消va的映射关系 */
printf("testing function 'unmap_range_in_pgtbl'...\n");
err = unmap_range_in_pgtbl(root, va, PAGE_SIZE);
printf("err = %d\n", err);
mu_assert_int_eq(0, err);
/* 取消映射后,再查找va对应的pa一定是出错的 */
printf("testing function 'query_in_pgtbl'...\n");
err = query_in_pgtbl(root, va, &pa, &entry);
printf("err = %d\n", err);
mu_assert_int_eq(-ENOMAPPING, err);
在测试函数里使用printf
并不似输出到屏幕上,而是page_table.out文件中。
因为mm_test_script.sh中有:
./test_aarch64_page_table > page_table.out
要实现的函数1:query_in_pgtbl
query_in_pgtbl
经过整个页表机制,找到一个虚拟地址对应的真实物理地址。
int query_in_pgtbl(vaddr_t * pgtbl, vaddr_t va, paddr_t * pa, pte_t ** entry)
{
// <lab2>
u32 level = 0;
ptp_t *cur_ptp = NULL; /* 指向当前页表 */
ptp_t *next_ptp = NULL; /* 指向下一级页表 */
pte_t *cur_pte = NULL; /* 指向当前页表中va确定的页表项 */
bool alloc = false;
int ret = 0;
/* 首先找到一级页表 */
cur_ptp = (ptp_t *)pgtbl;
/* 循环找到va对应的四级页表项 */
while (level < 4) {
ret = get_next_ptp(cur_ptp, level, va, &next_ptp, &cur_pte, alloc);
if (ret == BLOCK_PTP) {
/* 搜索到了真实的物理页地址,出错 */
return -ENOMAPPING;
} else if (ret < 0) {
/* 发生其他错误,抛出 */
return ret;
}
level++;
cur_ptp = next_ptp;
}
/* while 正常退出时,cur_ptp = next_ptp = 真实物理页面的地址 */
/* cur_pte 指向四级页表的页表项 */
if (ret != NORMAL_PTP || level != 4) {
return -ENOMAPPING; //--------------------------------------------------(1)
} else if (!cur_pte->l3_page.is_page || !cur_pte->l3_page.is_valid) {
/* 检查页表项是否有效 */
return -ENOMAPPING;
}
*entry = cur_pte;
*pa = (paddr_t)cur_ptp + GET_VA_OFFSET_L3(va);
// </lab2>
return 0;
}
(1)正常情况下,get_next_ptp
返回值有NORMAL_PTP和BLOCK_PTP。前者代表该页表项对应的页面是页表,后者则表示真实的数据页。而While循环正常退出时,cur_pte
为四级页表的表项,对应的页面当然应该是页表。
要实现的函数2:map_range_in_pgtbl
实现 [va, va+PAGE_SIZE] 到 [pa, pa+PAGE_SIZE] 之间的映射。
思路与
int map_range_in_pgtbl(vaddr_t * pgtbl, vaddr_t va, paddr_t pa,
size_t len, vmr_prop_t flags)
{
// <lab2>
u32 level = 0;
ptp_t *cur_ptp = NULL; /* 指向当前页表 */
ptp_t *next_ptp = NULL; /* 指向下一级页表 */
pte_t *cur_pte = NULL; /* 指向当前页表中va确定的页表项 */
int ret = 0;
vaddr_t va_end = va + len; /* 需要映射的末尾虚拟地址 */
/* 因为映射是以页为单位的,需按页遍历 */
for (; va < va_end; va += PAGE_SIZE, pa += PAGE_SIZE) {
cur_ptp = (ptp_t *)pgtbl; //----------------------------------------------(1)
level = 0;
/* 找到四级页表的起始地址就停止 */
while (level < 3) {
ret = get_next_ptp(cur_ptp, level, va, &next_ptp, &cur_pte, true);//----(2)
level++;
cur_ptp = next_ptp;
} //--------------------------------------------------(3)
/* 找到L3_page中对应的页表项 */
u32 index = GET_L3_INDEX(va);
cur_pte = &(cur_ptp->ent[index]);
/* 将pa写入页表项,并配置属性 */
cur_pte->l3_page.is_valid = 1;
cur_pte->l3_page.is_page = 1;
cur_pte->l3_page.pfn = pa >> PAGE_SHIFT;
/* 设置属性 */
set_pte_flags(cur_pte, flags, KERNEL_PTE);
}
flush_tlb(); /* 刷新TLB */
// </lab2>
return 0;
}
(1)整个系统只维护了一张一级页表(也足够了),所以每次映射的一级页表地址不变。
(2)设置get_next_ptp
的alloc字段为true
,表示二、三、四级页表会自动分配空间(如果不存在)。当然因为我们最终是要映射到给定的PA,所以最后的数据页不能自动分配。
(3)这里与query_in_pgtbl
函数不同,只找到四级页表的地址就停止(while条件:level < 3)。因为我们不需要get_next_ptp
帮我们建立新页表了,而是在对应表项中填入要映射的PA即可。
要实现的函数3:unmap_range_in_pgtbl
和map_range_in_pgtbl
的思路基本相同,取消映射直接清空L3页表项。
int unmap_range_in_pgtbl(vaddr_t * pgtbl, vaddr_t va, size_t len)
{
// <lab2>
int ret = 0;
u32 level = 0;
ptp_t *cur_ptp = NULL;
ptp_t *next_ptp = NULL;
pte_t *next_pte = NULL;
vaddr_t va_end = va + len;
for (; va < va_end; va += PAGE_SIZE) {
cur_ptp = (ptp_t *)pgtbl;
level = 0;
while (level < 3) {
ret = get_next_ptp(cur_ptp, level, va, &next_ptp, &next_pte, false);
if (ret < 0) {
/* 无效的页面不需要unmap */
break;
}
level++;
cur_ptp = next_ptp;
}
if (ret == NORMAL_PTP && level == 3 && cur_ptp != NULL) {
/* unmap page */
/* 找到L3_page中对应的页表项 */
u32 index = GET_L3_INDEX(va);
next_pte = &(cur_ptp->ent[index]);
next_pte->pte = 0;
}
}
flush_tlb();
// </lab2>
return 0;
}
问题5
在 AArch64 MMU 架构中,使用了两个 TTBR 寄存器, ChCore 使用一个 TTBR 寄存器映射内核地址空间,另一个寄存器映射用户态的地址空间,那么是否还需要通过设置页表位的属性来隔离内核态和用户态的地址空间?
是需要的。因为虽然在正常情况下,用户空间的地址变换会找TTBR0,内核空间的地址转换会找TTBR1。
但是用户程序本质上还是能够访问内核空间的虚拟地址的,也就是说,虽然OS设置了这么一套规则,但是并不是每个用户程序会乖乖遵循这套规则。所以在每个页表项上还是有必要设立权限位的。
问题6
1- ChCore 为什么要使用块条目组织内核内存? 哪些虚拟地址空间在Boot 阶段必须映射,哪些虚拟地址空间可以在内核启动后延迟?
由于翻译每个内存页都要占用一个TLB条目,页大小为4KB的情况下,访问2MB内存就要占用512个TLB条目。
而使用块条目可以有效缓解TLB条目不够的问题。假设使用2MB的块,访问2MB内存就仅需占用1个TLB条目。
另外,使用大页也能减少页表的级数,也就是加快地址转换速度。
然而,使用块也有缺点。例如,一方面应用程序可能未使用整个大页而造成物理内存资源浪费;另—方面大页的使用也会增加操作
系统管理内存的复杂度,Linux中就存在与大页相关的漏洞。
参考:《现代操作系统 》4.3.5章
在启动MMU之前,需要创建两类映射:
(1)用户空间虚拟地址映射
创建[0, 0x40000000)的恒等映射(PA=VA),L2页表直接映射2MB的大页,省去L3页表。
(2)内核空间虚拟地址映射
创建[0, 0xffffffff)的映射,注意加上0xffffff0000000000的偏移。即VA-0xffffff0000000000=PA。
同样,[0, 0x40000000)的映射在L2级页表直接映射2M的大页。[0x40000000, 0xffffffff)L1级页表直接映射1GB的大页。
/boot/mmu.c 中的init_boot_pt
函数实现了上述过程:
void init_boot_pt(void)
{
u32 start_entry_idx;
u32 end_entry_idx;
u32 idx;
u64 kva;
/* TTBR0_EL1 0-1G */
/* 1- 用户空间映射,虚拟地址从0开始 */
boot_ttbr0_l0[0] = ((u64) boot_ttbr0_l1) | IS_TABLE | IS_VALID;
boot_ttbr0_l1[0] = ((u64) boot_ttbr0_l2) | IS_TABLE | IS_VALID;
/* Usuable memory: PHYSMEM_START ~ PERIPHERAL_BASE */
/* 物理地址[0 - 0x1fffffff]映射到boot_ttbr0_l2对应页表项 */
start_entry_idx = PHYSMEM_START / SIZE_2M;
end_entry_idx = PERIPHERAL_BASE / SIZE_2M;
/* Map each 2M page */
/* 不使用四级页表,全是2M的大页映射 */
for (idx = start_entry_idx; idx < end_entry_idx; ++idx) {
boot_ttbr0_l2[idx] = (PHYSMEM_START + idx * SIZE_2M)
| UXN /* Unprivileged execute never */
| ACCESSED /* Set access flag */
| INNER_SHARABLE /* Sharebility */
| NORMAL_MEMORY /* Normal memory */
| IS_VALID;
}
/* Peripheral memory: PERIPHERAL_BASE ~ PHYSMEM_END */
/* 继续映射[0x20000000 - 0x3fffffff] */
/* Raspi3b/3b+ Peripherals: 0x3f 00 00 00 - 0x3f ff ff ff */
start_entry_idx = end_entry_idx;
end_entry_idx = PHYSMEM_END / SIZE_2M;
/* Map each 2M page */
/* 不使用四级页表,全是2M的大页映射 */
for (idx = start_entry_idx; idx < end_entry_idx; ++idx) {
boot_ttbr0_l2[idx] = (PHYSMEM_START + idx * SIZE_2M)
| UXN /* Unprivileged execute never */
| ACCESSED /* Set access flag */
| DEVICE_MEMORY /* Device memory */
| IS_VALID;
}
/*
* TTBR1_EL1 0-1G
* KERNEL_VADDR: L0 pte index: 510; L1 pte index: 0; L2 pte index: 0.
*/
/* 2- 内核空间映射,虚拟地址使用偏移:0xffffff0000000000 */
kva = KERNEL_VADDR;
boot_ttbr1_l0[GET_L0_INDEX(kva)] = ((u64) boot_ttbr1_l1)
| IS_TABLE | IS_VALID;
boot_ttbr1_l1[GET_L1_INDEX(kva)] = ((u64) boot_ttbr1_l2)
| IS_TABLE | IS_VALID;
/* 物理地址[0 - 0x0fffffff]映射到 boot_ttbr1_l2 对应页表项 */
start_entry_idx = GET_L2_INDEX(kva);
/* Note: assert(start_entry_idx == 0) */
end_entry_idx = start_entry_idx + PHYSMEM_BOOT_END / SIZE_2M;
/* Note: assert(end_entry_idx < PTP_ENTIRES) */
/*
* Map each 2M page
* Usuable memory: PHYSMEM_START ~ PERIPHERAL_BASE
*/
for (idx = start_entry_idx; idx < end_entry_idx; ++idx) {
boot_ttbr1_l2[idx] = (PHYSMEM_START + idx * SIZE_2M)
| UXN /* Unprivileged execute never */
| ACCESSED /* Set access flag */
| INNER_SHARABLE /* Sharebility */
| NORMAL_MEMORY /* Normal memory */
| IS_VALID;
}
/* Peripheral memory: PERIPHERAL_BASE ~ PHYSMEM_END */
/* 物理地址[20000000 - 0x3fffffff]映射到 boot_ttbr1_l2 对应页表项 */
start_entry_idx = start_entry_idx + PERIPHERAL_BASE / SIZE_2M;
end_entry_idx = PHYSMEM_END / SIZE_2M;
/* Map each 2M page */
for (idx = start_entry_idx; idx < end_entry_idx; ++idx) {
boot_ttbr1_l2[idx] = (PHYSMEM_START + idx * SIZE_2M)
| UXN /* Unprivileged execute never */
| ACCESSED /* Set access flag */
| DEVICE_MEMORY /* Device memory */
| IS_VALID;
}
/*
* Local peripherals, e.g., ARM timer, IRQs, and mailboxes
*
* 0x4000_0000 .. 0xFFFF_FFFF
* 1G is enough. Map 1G page here.
*/
/* 物理地址[40000000 - 0xffffffff]映射到 boot_ttbr1_l1 对应页表项 */
/* 1G的大页使用一级页表直接映射 */
kva = KERNEL_VADDR + PHYSMEM_END;
boot_ttbr1_l1[GET_L1_INDEX(kva)] = PHYSMEM_END | UXN /* Unprivileged execute never */
| ACCESSED /* Set access flag */
| DEVICE_MEMORY /* Device memory */
| IS_VALID;
}
启动MMU的函数:/boot/tool.s 的 el1_mmu_activate。首先使init_boot_pt
的映射生效。
/* Write ttbr with phys addr of the translation table */
/* init_boot_pt()中初始化的页表基地址写入寄存器 */
adrp x8, boot_ttbr0_l0
msr ttbr0_el1, x8
adrp x8, boot_ttbr1_l0
msr ttbr1_el1, x8
isb
2为什么用户程序不能读写内核内存? 保护内核内存的具体机制是什么?
用户程序必然不允许直接访问内核空间,保证内核代码不被恶意破坏。
用户程序访问内核空间的页表时,会将当前状态寄存器里的标识位与页表项的标识位进行对比检查。