我的博客:startcraft
虛擬內存管理startcraft
虛擬內存就是對每一個進程而言,對它來說它認爲它獨佔所有4G內存,進程內的地址就是以這4G的虛擬內存來表示的,當要執行時,cpu通過分段機制和分頁機制將虛擬地址轉換成物理內存地址進行訪問。同時一個進程也不是所有的頁都在內存中,只有部分在內存中,當需要的頁不在內存時產生一個缺頁中斷,然後進行調度,將需要的頁調入內存
仿照linux的設計,對於一個進程的4G虛擬空間3G-4G的空間給系統內核,0-3G給用戶程序,現在要將內核映射到虛擬地址空間的3G-4G,但是映射完加載內核就需要頁表來指示正式的物理內存地址,但是內核不加載就沒有頁表,所有需要一個臨時的頁表
內核的映射
修改鏈接器的腳本script/kernel.ld
/*
* * kernel.ld -- 針對 kernel 格式所寫的鏈接腳本
* */
ENTRY(start)
SECTIONS
{
PROVIDE( kern_start = 0xC0100000);
/* 段起始位置 */
. = 0x100000;
.init.text :
{
*(.init.text)
. = ALIGN(4096);
}
.init.data :
{
*(.init.data)
. = ALIGN(4096);
}
. += 0xC0000000;
.text : AT(ADDR(.text) - 0xC0000000)
{
*(.text)
. = ALIGN(4096);
}
.data : AT(ADDR(.data) - 0xC0000000)
{
*(.data)
*(.rodata)
. = ALIGN(4096);
}
.bss : AT(ADDR(.bss) - 0xC0000000)
{
*(.bss)
. = ALIGN(4096);
}
.stab : AT(ADDR(.stab) - 0xC0000000)
{
*(.stab)
. = ALIGN(4096);
}
.stabstr : AT(ADDR(.stabstr) - 0xC0000000)
{
*(.stabstr)
. = ALIGN(4096);
}
PROVIDE( kern_end = . );
/DISCARD/ : { *(.comment) *(.eh_frame) }
}
第8行修改了內核的加載地址爲3G,然後新增的兩個.init段放臨時頁表和函數,這兩個段放在0x100000處給grub加載,然後將當前地址加上0xC0000000的偏移量
後面的部分和原來的區別就是加了AT(ADDR(.xxxx) - 0xC0000000)這些,這些是指明區段所載入內存的實際地址,所以將當前偏移量減去0xC0000000就是實際加載地址
鏈接器修改了,相應的其他代碼也要修改
boot/boot.s
......
[BITS 32] ; 所有代碼以 32-bit 的方式編譯
section .init.text ; 臨時代碼段從這裏開始
; 在代碼段的起始位置設置符合 Multiboot 規範的標記
dd MBOOT_HEADER_MAGIC ; GRUB 會通過這個魔數判斷該映像是否支持
dd MBOOT_HEADER_FLAGS ; GRUB 的一些加載時選項,其詳細註釋在定義處
dd MBOOT_CHECKSUM ; 檢測數值,其含義在定義處
[GLOBAL start] ; 向外部聲明內核代碼入口,此處提供該聲明給鏈接器
[GLOBAL mboot_ptr_tmp] ; 向外部聲明 struct multiboot * 變量
[EXTERN kern_entry] ; 聲明內核 C 代碼的入口函數
start:
cli ; 此時還沒有設置好保護模式的中斷處理,要關閉中斷
; 所以必須關閉中斷
mov [mboot_ptr_tmp], ebx ; 將 ebx 中存儲的指針存入全局變量
mov esp, STACK_TOP ; 設置內核棧地址
and esp, 0FFFFFFF0H ; 棧地址按照字節對齊16
mov ebp, 0 ; 幀指針修改爲 0
call kern_entry ; 調用內核入口函數
stop:
hlt ; 停機指令,可以降低 CPU 功耗
jmp stop ; 到這裏結束,關機什麼的後面再說
;-----------------------------------------------------------------------------
section .init.data ; 開啓分頁前臨時數據段
stack: times 1024 db 0 ; 臨時內核棧
STACK_TOP equ $-stack-1 ; 內核棧頂,$ 符指代是當前地址
mboot_ptr_tmp: dd 0 ;臨時的全局multiboot結構體指針
第五行修改代碼段從.init.text
開始,同時指定kern_entry()
函數在代碼段.init.text
處,並且在該函數中定義臨時頁表,切換到高虛擬地址的kern_init()
執行,並且切換內核棧和multiboot結構體指針
修改include/pmm.h
#ifndef INCLUDE_PMM_H
#define INCLUDE_PMM_H
#include "multiboot.h"
// 內核文件在內存中的起始和結束位置
// 在鏈接器腳本中要求鏈接器定義
extern uint8_t kern_start[];
extern uint8_t kern_end[];
extern uint32_t phy_mem_count;//動態分配的物理內存總數
#define PMM_MAX_SIZE 0x20000000//規定最大的物理內存爲512MB
#define PMM_PAGE_SIZE 0x1000 //一頁的大小爲4KB
#define PAGE_MAX_SIZE (PMM_MAX_SIZE/PMM_PAGE_SIZE)//最多的物理頁面的數量
#define STACK_SIZE 8192//線程棧的大小
#define PHY_PAGE_MASK 0xFFFFF000//頁掩碼按照 4096 對齊地址
//打印物理內存佈局
void show_memory_map();
void init_pmm();//初始化內存佈局
uint32_t pmm_alloc_page();//申請一頁物理頁,返回該頁的地址
void pmm_free_page(uint32_t p);//釋放申請的內存
#endif// INCLUDE_PMM_H
修改init/entry.c
#include "console.h"
#include "timer.h"
#include "debug.h"
#include "gdt.h"
#include "idt.h"
#include "pmm.h"
#include "vmm.h"
//內核初始化函數
void kern_init();
// 開啓分頁機制之後的 Multiboot 數據指針
multiboot_t *glb_mboot_ptr;
// 開啓分頁機制之後的內核棧
char kern_stack[STACK_SIZE];
// 內核使用的臨時頁表和頁目錄
// 該地址必須是頁對齊的地址,內存 0-640KB 肯定是空閒的
__attribute__((section(".init.data"))) pgd_t *pgd_tmp = (pgd_t *)0x1000;
__attribute__((section(".init.data"))) pgd_t *pte_low = (pgd_t *)0x2000;
__attribute__((section(".init.data"))) pgd_t *pte_hign = (pgd_t *)0x3000;
// 內核入口函數
__attribute__((section(".init.text"))) void kern_entry()
{
pgd_tmp[0] = (uint32_t)pte_low | PAGE_PRESENT | PAGE_WRITE;
pgd_tmp[PGD_INDEX(PAGE_OFFSET)] = (uint32_t)pte_hign | PAGE_PRESENT |PAGE_WRITE;
// 映射內核虛擬地址 4MB 到物理地址的前 4MB
int i;
for (i = 0; i < 1024; i++) {
pte_low[i] = (i << 12) | PAGE_PRESENT | PAGE_WRITE;
}
// 映射 0x00000000-0x00400000 的物理地址到虛擬地址 0xC0000000-0xC0400000
for (i = 0; i < 1024; i++) {
pte_hign[i] = (i << 12) | PAGE_PRESENT | PAGE_WRITE;
}
// 設置臨時頁表
asm volatile ("mov %0, %%cr3" : : "r" (pgd_tmp));
uint32_t cr0;
// 啓用分頁,將 cr0 寄存器的分頁位置爲 1 就好
asm volatile ("mov %%cr0, %0" : "=r" (cr0));
cr0 |= 0x80000000;
asm volatile ("mov %0, %%cr0" : : "r" (cr0));
// 切換內核棧
uint32_t kern_stack_top = ((uint32_t)kern_stack + STACK_SIZE) & 0xFFFFFFF0;
asm volatile ("mov %0, %%esp\n\t" "xor %%ebp, %%ebp" : : "r" (kern_stack_top));
// 更新全局 multiboot_t 指針
glb_mboot_ptr = mboot_ptr_tmp + PAGE_OFFSET;
// 調用內核初始化函數
kern_init();
}
void kern_init()
{
init_debug();
init_gdt();
init_idt();
console_clear();
printk_color(rc_black, rc_green, "Hello, OS kernel!\n");
init_timer(100);
//開啓中斷
//asm volatile("sti");
printk("kernel in memory start: 0x%08X\n", kern_start);
printk("kernel in memory end: 0x%08X\n", kern_end);
printk("kernel in memory used: %d KB\n\n", (kern_end - kern_start +1023) / 1024);
show_memory_map();
init_pmm();
printk_color(rc_black, rc_red, "\nThe Count of Physical Memory Page is: %u\n\n", phy_mem_count);
uint32_t allc_addr = NULL;
printk_color(rc_black, rc_light_brown , "Test Physical Memory Alloc :\n");
allc_addr = pmm_alloc_page();
printk_color(rc_black, rc_light_brown , "Alloc Physical Addr: 0x%08X\n",allc_addr);
allc_addr = pmm_alloc_page();
printk_color(rc_black, rc_light_brown , "Alloc Physical Addr: 0x%08X\n",allc_addr);
allc_addr = pmm_alloc_page();
printk_color(rc_black, rc_light_brown , "Alloc Physical Addr: 0x%08X\n",allc_addr);
allc_addr = pmm_alloc_page();
printk_color(rc_black, rc_light_brown , "Alloc Physical Addr: 0x%08X\n",allc_addr);
while (1)
{
asm volatile ("hlt");
}
}
把原來的內核入口函數改成了kern_init(),kern_entry()函數裏定義了臨時頁表並且開啓分頁機制,然後修改了內核棧到虛擬地址,然後調用kern_init();attribute((section(".init.text")))是gcc提供的指定函數或數據的存儲區段
一點點來看:
首先定義的pgt_temp是臨時頁目錄(臨時二級頁表),pgd_t這個數據類型在vmm.h內定義是uint32_t,它放在.init.data段,起始地址是0x1000
pte_low是在低端地址的頁表,pte_high是在3g以上高端地址的頁表,起始地址分別爲0x2000,和0x3000,所以臨時頁目錄的大小就是0x2000-0x1000=0x1000是4KB,低端頁表因爲只要映射內核的4MB地址所以一頁頁表就夠了。這些都在臨時數據段(.init.data)
kern_entry()函數放在臨時代碼段(.init.text),該函數的加載地址就是0x100000(在鏈接器腳本中定義的),該函數第一行代碼就是將頁目錄的第一項進行設置
頁目錄和頁表項的格式如下
將頁目錄的第一項映射到低端頁表,PAGE_PRESENT爲0x1代表存在,PAGE_WRITE爲0x2,這樣構造的頁目錄第一項就爲0x1003
然後函數第二行就是將第一張高端頁表映射到頁目錄中,PGD_INDEX(PAGE_OFFSET)
是獲取地址的頁目錄號,因爲一個32位虛擬地址的高10位是頁目錄中偏移,所以#define PGD_INDEX(x) (((x) >> 22) & 0x3FF)
獲取虛擬地址的高10位就是頁目錄中的偏移,這裏就是將高端地址的第一位也就是3G獲取它的頁目錄號,然後繼續構造頁目錄項
然後是映射內核虛擬地址 4MB 到物理地址的前 4MB,4MB也就是1024頁,頁號左移12位剛好就是每一頁的起始地址
映射 0x00000000-0x00400000 的物理地址到虛擬地址 0xC0000000-0xC0400000,也是4MB,對於二級頁表來說更上面是一樣的,它映射0xC0000000是通過頁目錄的偏移實現的
然後往CR3寄存器寫入頁目錄的基址,將CR0寄存器的第31位置爲1代表開啓分頁模式
在kern_entry()定義的頁表將0x00000000-0x00400000 的物理地址到虛擬地址 0xC0000000-0xC0400000,同時還將映射內核虛擬地址 4MB 到物理地址的前 4MB,這是因爲在進入kern_entry()時還沒有開啓分頁機制,開啓分頁機制映射0x00000000-0x00400000 的物理地址到虛擬地址 0xC0000000-0xC0400000後在1MB處的kern_entry()函數會出錯,所以映射一下低位4MB
更新一下include/multiboot.h
......
// 未開啓分頁前的multbbot_t 指針
extern multiboot_t *mboot_ptr_tmp;
// 聲明全局的 multiboot_t * 指針
extern multiboot_t *glb_mboot_ptr;
修改顯存地址 drivers/console.c
......
#include "vmm.h"
static uint16_t *video_memory= (uint16_t *)(0xB8000+PAGE_OFFSET);//顯存的起始地址,每兩個字節表>
......
之前的elf_t結構體存儲的是低端內存的地址,現在也必須加上頁偏移:
kernel/debug/elf.c
......
// 從 multiboot_t 結構獲取信息ELF
elf_t elf_from_multiboot(multiboot_t *mb)
{
int i;
elf_t elf;
elf_section_header_t *sh = (elf_section_header_t *)mb->addr;
uint32_t shstrtab = sh[mb->shndx].addr;
for (i = 0; i < mb->num; i++) {
const char *name = (const char *)(shstrtab + sh[i].name)+ PAGE_OFFSET;
// 在 GRUB 提供的 multiboot 信息中尋找
// 內核 ELF 格式所提取的字符串表和符號表
if (strcmp(name, ".strtab") == 0) {
elf.strtab = (const char *)sh[i].addr+PAGE_OFFSET;
elf.strtabsz = sh[i].size;
}
if (strcmp(name, ".symtab") == 0) {
elf.symtab = (elf_symbol_t*)(sh[i].addr+PAGE_OFFSET);
elf.symtabsz = sh[i].size;
}
}
return elf;
}
......
mm/vmm.c實現內核頁表和映射
#include "idt.h"
#include "string.h"
#include "debug.h"
#include "vmm.h"
#include "pmm.h"
//內核頁目錄
pgd_t pgd_kern[PGD_SIZE] __attribute__ ((aligned(PAGE_SIZE)));
//內核頁表
static pte_t pte_kern[PTE_COUNT][PTE_SIZE] __attribute__ ((aligned(PAGE_SIZE)));
void init_vmm ()
{
//0xC0000000在頁目錄的偏移值
uint32_t kern_pte_first_idx = PGD_INDEX(PAGE_OFFSET);
uint32_t i,j;
for (i=kern_pte_first_idx,j=0;i<kern_pte_first_idx+PTE_COUNT;++i,++j)
{
//將頁表映射到頁目錄,程序內的地址是虛擬地址,所以要減去偏移
pgd_kern[i]=(uint32_t)pte_kern[j]-PAGE_OFFSET|PAGE_PRESENT | PAGE_WRITE;
uint32_t * pte= (uint32_t *)pte_kern;
//映射所有的物理頁
for (i=1;i<PTE_SIZE*PTE_COUNT;++i)
{
pte[i]=(i<<12)|PAGE_WRITE|PAGE_PRESENT;
}
//頁目錄的物理地址
uint32_t pgd_kern_phy_addr = (uint32_t)pgd_kern - PAGE_OFFSET;
// 註冊頁錯誤中斷的處理函數 ( 14 是頁故障的中斷號 )
register_interrupt_handler(14, &page_fault);
//切換頁表
switch_pgd(pgd_kern_phy_addr);
}
}
void switch_pgd (uint32_t pgd_kern_phy_addr)
{
asm volatile ("mov %0, %%cr3" : : "r" (pgd_kern_phy_addr));
}
//使用 flags 指出的頁權限,把物理地址 pa 映射到虛擬地址 va
void map (pgd_t *pgd_now, uint32_t va, uint32_t pa, uint32_t flags)
{
uint32_t pgd_idx=PGD_INDEX (va);
uint32_t pte_idx=PTE_INDEX (va);
pte_t *pte = (pte_t *)(pgd_now[pgd_idx] & PAGE_MASK);
if (!pte)
{
pte=(pte_t*)pmm_alloc_page ();
// 轉換到內核線性地址並清 0
pgd_now [pgd_idx]= (uint32_t)pte|PAGE_PRESENT | PAGE_WRITE;
pte = (pte_t *)((uint32_t)pte + PAGE_OFFSET);
bzero(pte, PAGE_SIZE);
}else
{
//轉換到內核線性地址
pte = (pte_t *)((uint32_t)pte + PAGE_OFFSET);
}
pte [pte_idx]= (pa&PAGE_MASK)|flags;
// 通知 CPU 更新頁表緩存
asm volatile ("invlpg (%0)" : : "a" (va));
}
//取消va虛擬地址的映射
void unmap(pgd_t *pgd_now, uint32_t va)
{
uint32_t pgd_idx=PGD_INDEX (va);
uint32_t pte_idx=PTE_INDEX (va);
pte_t *pte = (pte_t *)(pgd_now[pgd_idx] & PAGE_MASK);
//該頁表不在頁目錄中
if (!pte)
return;
//轉換到內核線性地址
pte = (pte_t*)((uint32_t)pte + PAGE_OFFSET);
pte [pte_idx]=0;
// 通知 CPU 更新頁表緩存
asm volatile ("invlpg (%0)" : : "a" (va));
}
//獲取虛擬地址對應的物理地址,成功獲取返回1並將物理地址寫入pa,否則返回0
uint32_t get_mapping(pgd_t *pgd_now, uint32_t va, uint32_t *pa)
{
uint32_t pgd_idx=PGD_INDEX (va);
uint32_t pte_idx=PTE_INDEX (va);
pte_t *pte = (pte_t *)(pgd_now[pgd_idx] & PAGE_MASK);
//該頁表不在頁目錄中
if (!pte)
return 0;
//轉換到內核線性地址
pte = (pte_t*)((uint32_t)pte + PAGE_OFFSET);
if(pte [pte_idx]!=0&&pa)
{
*pa=pte[pte_idx]&PAGE_MASK;
return 1;
}
return 0;
}
init_vmm()
函數跟之前的臨時頁表的部分差不多,同時註冊了一個14號中斷函數處理頁面出錯
map函數是將虛擬地址映射到物理地址,函數前兩行是獲取虛擬地址對應的頁目錄偏移和頁表偏移,分別是高10位和低10位
然後通過頁目錄取得二級頁表的物理地址,若該頁表不存在則申請一頁內存,然後映射入頁目錄,獲取到二級頁表之後要在函數中訪問它需要取得它的虛擬地址,所以加上偏移,然後將對應物理頁的地址構造成頁表項映射到該二級頁表中
unmap函數用於取消映射,直接將對應二級頁表的頁表項設置爲0,因爲在構建內核頁表的時候沒有映射第0頁就是方便這時候當NULL
get_mapping函數是獲取虛擬地址對應的物理地址成功獲取返回1並將物理地址寫入pa,否則返回0
一些東西的定義mm/vmm.h
#ifndef INCLUDE_VMM_H
#define INCLUDE_VMM_H
#include "types.h"
#include "idt.h"
// 內核的偏移地址
#define PAGE_OFFSET 0xC0000000
/**
* 12 * P−− 位 0 是存在 (Present) 標誌,用於指明表項對地址轉換是否有效。
* 13 * P = 1 表示有效; P = 0 表示無效。
* 14 * 在頁轉換過程中,如果說涉及的頁目錄或頁表的表項無效,則會導致一個異常。
* 15 * 如果 P = 0 ,那麼除表示表項無效外,其餘位可供程序自由使用。
* 16 * 例如,操作系統可以使用這些位來保存已存儲在磁盤上的頁面的序號。
* 17 */
#define PAGE_PRESENT 0x1
/**
* R/W −− 位 1 是讀 / 寫 (Read/Write) 標誌。如果等於 1 ,表示頁面可以被讀、寫或執行。
* 如果爲 0 ,表示頁面只讀或可執行。
* 當處理器運行在超級用戶特權級(級別 0,1 或) 2 時,則 R/W 位不起作用。
* 頁目錄項中的 R/W 位對其所映射的所有頁面起作用。
*/
#define PAGE_WRITE 0x2
/**
* U/S −− 位 2 是用戶 / 超級用戶 (User/Supervisor) 標誌。
* 如果爲 1 ,那麼運行在任何特權級上的程序都可以訪問該頁面。
* 如果爲 0 ,那麼頁面只能被運行在超級用戶特權級 (0,1 或 2) 上的程序訪問。
* 頁目錄項中的 U/S 位對其所映射的所有頁面起作用。
*/
#define PAGE_USER 0x4
// 虛擬分頁大小
#define PAGE_SIZE 4096
// 頁掩碼,用於 4KB 對齊
#define PAGE_MASK 0xFFFFF000
// 獲取一個地址的頁目錄項
#define PGD_INDEX(x) (((x) >> 22) & 0x3FF)
// 獲取一個地址的頁表項
#define PTE_INDEX(x) (((x) >> 12) & 0x3FF)
// 獲取一個地址的頁內偏移
#define OFFSET_INDEX(x) ((x) & 0xFFF)
// 頁目錄數據類型
typedef uint32_t pgd_t;
// 頁表數據類型
typedef uint32_t pte_t;
// 頁表成員數
#define PGD_SIZE (PAGE_SIZE/sizeof(pte_t))
// 頁表成員數
#define PTE_SIZE (PAGE_SIZE/sizeof(uint32_t))
// 映射 512MB 內存所需要的頁表數
#define PTE_COUNT 128
// 內核頁目錄區域
extern pgd_t pgd_kern[PGD_SIZE];
// 初始化虛擬內存管理
void init_vmm();
// 更換當前的頁目錄
void switch_pgd(uint32_t pd);
// 使用 flags 指出的頁權限,把物理地址 pa 映射到虛擬地址 va
void map(pgd_t *pgd_now, uint32_t va, uint32_t pa, uint32_t flags);
// 取消虛擬地址 va 的物理映射
void unmap(pgd_t *pgd_now, uint32_t va);
// 如果虛擬地址 va 映射到物理地址則返回 1
// 同時如果 pa 不是空指針則把物理地址寫入 pa 參數
uint32_t get_mapping(pgd_t *pgd_now, uint32_t va, uint32_t *pa);
// 頁錯誤中斷的函數處理
void page_fault(pt_regs *regs);
#endif // INCLUDE_VMM_H
實現頁錯誤中斷處理函數
#include "vmm.h"
#include "debug.h"
void page_fault(pt_regs *regs)
{
uint32_t cr2;
asm volatile ("mov %%cr2, %0" : "=r" (cr2));
printk("Page fault at 0x%x, virtual faulting address 0x%x\n", regs->eip,cr2);
printk("Error code: %x\n", regs->err_code);
// bit 0 爲 0 指頁面不存在內存裏
if ( !(regs->err_code & 0x1)) {
printk_color(rc_black, rc_red, "Because the page wasn't present.\n");
}
// bit 1 爲 0 表示讀錯誤,爲 1 爲寫錯誤
if (regs->err_code & 0x2) {
printk_color(rc_black, rc_red, "Write error.\n");
} else {
printk_color(rc_black, rc_red, "Read error.\n");
}
// bit 2 爲 1 表示在用戶模式打斷的,爲 0 是在內核模式打斷的
if (regs->err_code & 0x4) {
printk_color(rc_black, rc_red, "In user mode.\n");
} else {
printk_color(rc_black, rc_red, "In kernel mode.\n");
}
// bit 3 爲 1 表示錯誤是由保留位覆蓋造成的
if (regs->err_code & 0x8) {
printk_color(rc_black, rc_red, "Reserved bits being overwritten.\n");
}
// bit 4 爲 1 表示錯誤發生在取指令的時候
if (regs->err_code & 0x10) {
printk_color(rc_black, rc_red, "The fault occurred during an instruction fetch.\n");
}
while (1);
}
debug
測試一下發現無法運行報錯了,然後開始尋找問題,先排查了一遍發現代碼沒有問題
看報錯信息顯示如下
發現是在0xc0105000這裏出錯了
然後我用objdump -h time_kernel
得到如下結果
發現多了兩個段.text.__x86.get_pc_thunk.ax
和.text.__x86.get_pc_thunk.bx
他們的VMA地址是加了偏移量之後的
然後使用objdump -d time_kernel
發現這兩個段的函數在分頁開啓之前就被調用了,查資料知道這兩個函數是傳遞寄存器的值,所以我們要把它們放在分頁之前
修改scripts/kernel.ld
......
.init.data :
{
*(.init.data)
. = ALIGN(4096);
}
.text.__x86.get_pc_thunk.ax :
{
*(.__x86.get_pc_thunk.ax)
. = ALIGN(4096);
}
.text.__x86.get_pc_thunk.bx :
{
*(.__x86.get_pc_thunk.bx)
. = ALIGN(4096);
}
. = ALIGN(4096);
. += 0xC0000000;
.text : AT(ADDR(.text) - 0xC0000000)
......
在.init.data之後.text之前加入這兩個段
現在測試就成功了