現代操作系統:原理與實現配套實驗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為什么用戶程序不能讀寫內核內存? 保護內核內存的具體機制是什么?
用戶程序必然不允許直接訪問內核空間,保證內核代碼不被惡意破壞。
用戶程序訪問內核空間的頁表時,會將當前狀態寄存器里的標識位與頁表項的標識位進行對比檢查。