虚拟内存管理
虚拟内存就是对每一个进程而言,对它来说它认为它独占所有4G内存,进程内的地址就是以这4G的虚拟内存来表示的,当要执行时,cpu通过分段机制和分页机制将虚拟地址转换成物理内存地址进行访问。同时一个进程也不是所有的页都在内存中,只有部分在内存中,当需要的页不在内存时产生一个缺页中断,然后进行调度,将需要的页调入内存
仿照linux的设计,对于一个进程的4G虚拟空间3G-4G的空间给系统内核,0-3G给用户程序,现在要将内核映射到虚拟地址空间的3G-4G,但是映射完加载内核就需要页表来指示正式的物理内存地址,但是内核不加载就没有页表,所有需要一个临时的页表
内核的映射
修改链接器的脚本script/kernel.ld
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| /* * * 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| ......
[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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #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 #define PMM_PAGE_SIZE 0x1000 #define PAGE_MAX_SIZE (PMM_MAX_SIZE/PMM_PAGE_SIZE) #define STACK_SIZE 8192 #define PHY_PAGE_MASK 0xFFFFF000
void show_memory_map();
void init_pmm(); uint32_t pmm_alloc_page(); void pmm_free_page(uint32_t p); #endif
|
修改init/entry.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| #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_t *glb_mboot_ptr;
char kern_stack[STACK_SIZE];
__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; int i; for (i = 0; i < 1024; i++) { pte_low[i] = (i << 12) | PAGE_PRESENT | PAGE_WRITE; } 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; 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)); 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); 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
1 2 3 4 5 6
| ......
extern multiboot_t *mboot_ptr_tmp;
extern multiboot_t *glb_mboot_ptr;
|
修改显存地址 drivers/console.c
1 2 3 4
| ...... #include "vmm.h" static uint16_t *video_memory= (uint16_t *)(0xB8000+PAGE_OFFSET); ......
|
之前的elf_t结构体存储的是低端内存的地址,现在也必须加上页偏移:
kernel/debug/elf.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| ...... 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; 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实现内核页表和映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| #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 () { 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; 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)); } 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 (); 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; asm volatile ("invlpg (%0)" : : "a" (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; asm volatile ("invlpg (%0)" : : "a" (va)); }
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| #ifndef INCLUDE_VMM_H #define INCLUDE_VMM_H
#include "types.h" #include "idt.h"
#define PAGE_OFFSET 0xC0000000
#define PAGE_PRESENT 0x1
#define PAGE_WRITE 0x2
#define PAGE_USER 0x4
#define PAGE_SIZE 4096
#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))
#define PTE_COUNT 128
extern pgd_t pgd_kern[PGD_SIZE];
void init_vmm();
void switch_pgd(uint32_t pd);
void map(pgd_t *pgd_now, uint32_t va, uint32_t pa, uint32_t flags);
void unmap(pgd_t *pgd_now, uint32_t va);
uint32_t get_mapping(pgd_t *pgd_now, uint32_t va, uint32_t *pa);
void page_fault(pt_regs *regs);
#endif
|
实现页错误中断处理函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #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);
if ( !(regs->err_code & 0x1)) { printk_color(rc_black, rc_red, "Because the page wasn't present.\n"); } if (regs->err_code & 0x2) { printk_color(rc_black, rc_red, "Write error.\n"); } else { printk_color(rc_black, rc_red, "Read error.\n"); } 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"); } if (regs->err_code & 0x8) { printk_color(rc_black, rc_red, "Reserved bits being overwritten.\n"); } 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ...... .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之前加入这两个段
现在测试就成功了