babyos2(5)——分页
来源:互联网 发布:2012nba总决赛数据 编辑:程序博客网 时间:2024/06/06 02:46
加载elf格式的内核完成后,babyos正式开始执行内核代码,首先要开启分页。
这张图表示了段页式内存管理的基本流程。要开启保护模式,一定会开启分段,而分页是可选的。但现代操作系统,分页也是最基本的功能。
如图所示,左半边展示了分段。进入保护模式之前,我们用lgdt指令加载了GDT,GDT名字叫全局描述符表,它是一张表。每一个表项是一个全局描述符:
它描述了一个段的基地址、限长及一些属性,唯一描述了一个段。
高级语言编程中,要找到数组中的一个项,只要一个索引就可以,但要找到一个全局描述符,需要一个段选择子(Segment Selector)。
index可以理解成数组的索引,TI则表示这是一个GDT还是IDT的选择子,RPL表示这个选择子的特权级。关于特权级,RPL,CPL,DPL的关系比较复杂,可以查阅Intel的文档。
当操作系统或应用程序执行时,访问内存过程中,CPU发出的是逻辑地址(Logical Address)。如果未开启保护模式,则逻辑地址==物理地址。如果开启了保护模式,意味着开启了分段,则首先会根据段寄存器(CS/DS/ES..)中存放的段选择子找到所要访问的段,然后根据段的基地址+offset,将逻辑地址转化成线性地址。如果未开启分页,线性地址数值上等于物理地址。如果开启了分页,对于32位,4K标准页,将线性地址分为三段,分别表示页目录,页表,偏移。
一张更清晰的分页内存管理图如上图所示。
CR3寄存器中存放了页目录的物理地址。页目录是一个4K的页,存放1024个unsigned int,每一项表示一个页表项的物理地址。当MMU得到到一个线性地址后,会根据它的22-31bit,到页目录中找到对应的项。然后根据12-21bit到页表中找到对应的项(PTE),这一项记录了一个页的物理地址,再加上0-11bit 偏移量,最终将线性地址转化成了物理地址。
/* * guzhoudiaoke@126.com * 2017-10-22 */OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")OUTPUT_ARCH(i386)ENTRY(_start)SECTIONS{ . = 0xc0100000; .text : AT(0x100000) { *(.text .stub .text.* .gnu.linkonce.t.*) } PROVIDE(etext = .); .rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) } . = ALIGN(0x1000); PROVIDE(data = .); .data : { *(.data) } PROVIDE(edata = .); .bss : { *(.bss) } PROVIDE(end = .); /DISCARD/ : { *(.eh_frame .note.GNU-stack) }}
babyos2将elf加载到3G+1M 的位置,每个进程4G的地址空间,3G~4G为内核空间,0~3G为用户地址空间。elf加载完成后执行elf的entry。
## guzhoudiaoke@126.com# 2017-10-22##include "kernel.h".global _start_start = entry - KERNEL_BASE.global entryentry: # 1. clear pg_dir, pg_table0 and pg_table_vram xorl %eax, %eax movl $entry_pg_dir, %edi movl $1024, %ecx cld rep stosl xorl %eax, %eax movl $entry_pg_table0, %edi movl $1024, %ecx cld rep stosl xorl %eax, %eax movl $entry_pg_table_vram, %edi movl $1024, %ecx cld rep stosl # 2. set pg_dir[0] and pg_dir[0xc0000000/4M*4] as pg_table0 movl $(entry_pg_table0-KERNEL_BASE), %ebx orl $(PTE_P|PTE_W), %ebx movl %ebx, (entry_pg_dir-KERNEL_BASE) movl %ebx, (entry_pg_dir-KERNEL_BASE) + (KERNEL_BASE >> 20) # 3 set pg_dir[VRAM >> 22] = pg_table_vram movl $(entry_pg_table_vram-KERNEL_BASE),%ebx orl $(PTE_P|PTE_W), %ebx movl $(entry_pg_dir-KERNEL_BASE), %eax movl (VIDEO_INFO_ADDR+8), %edx shrl $20, %edx addl %edx, %eax movl %ebx, (%eax) # 4. set pg_table0[] = {0, 4k, 8k, 12k ... 4M-4k} | (PTE_P | PTE_W) cld movl $(PTE_P|PTE_W), %eax movl $1024, %ecx movl $(entry_pg_table0-KERNEL_BASE), %edi1: stosl addl $0x1000, %eax decl %ecx jg 1b # 5. set pg_table_vram[] = {VRAM, VRAM+4k, VRAM+8k, VRAM+12k ... } | (PTE_P | PTE_W) cld movl (VIDEO_INFO_ADDR + 8), %eax xorl $(PTE_P|PTE_W), %eax movl $1024, %ecx movl $(entry_pg_table_vram-KERNEL_BASE),%edi2: stosl addl $0x1000, %eax decl %ecx jg 2b # 6. setup page directory movl $(entry_pg_dir-KERNEL_BASE), %eax movl %eax, %cr3 # 7. turn on paging movl %cr0, %eax orl $(CR0_PG), %eax movl %eax, %cr0 # 8. set a new stack movl $(kernel_stack + 2*KSTACK_SIZE), %esp # 9. jump to main mov $main, %eax jmp *%eax
为了开启分页后能正常访问内存,需要开启分页之前设置好响应的页目录和页表。
entry_pg_dir: 页目录
entry_pg_table0: 一个页表项,为了在开启分页的开始阶段正常访问内存,babyos2将pg_dir[0],pg_dir[0xc0000000/4M*4] 都设置为pg_table0,pg_table0映射物理内存的开始4M。
entry_pg_table_vram: 用于vram的页表项,为了能正常使用显存,方便显示一些信息。
所以上面一大段汇编的意思是:
memset(entry_pg_dir, 0, 4k);memset(entry_pg_table0, 0, 4k);memset(entry_pg_table_vram, 0, 4k);entry_pg_dir[0] = VA2PA(&entry_pg_table0) | PTE_P | PTE_W;entry_pg_dir[3G/4M*4] = VA2PA(&entry_pg_table0) | PTE_P | PTE_W;entry_pg_dir[VRAM >> 22] = VA2PA(&entry_pg_table_vram) | PTE_P | PTE_W;entry_page_table0[] = {0, 4K, 8K, ... 4M-4K} | PTE_P | PTE_W;entry_page_table_vram = {VRAM, VRAM+4K,... } | PTE_P | PTE_W;cr3 = VA2PA(entry_pg_dir);cr0.CR0_PG = 1;esp = kernel_stack + 8K;
到此为止,设置好了页目录,开启了分页,还设置了新的内核栈,至于为什么+8K后面再解释。
这些结构的定义:
/* * guzhoudiaoke@126.com * 2017-10-23 */#include "babyos.h"#include "kernel.h"#include "mm.h"#include "x86.h"#include "console.h"#include "string.h"__attribute__ ((__aligned__(2*PAGE_SIZE))) uint8 kernel_stack[KSTACK_SIZE*2] = { 0xff, };/* pg_dir and pte for entry */__attribute__ ((__aligned__(PAGE_SIZE))) pte_t entry_pg_table0[NR_PTE_PER_PAGE] = { [0] = (0) | PTE_P | PTE_W, };__attribute__ ((__aligned__(PAGE_SIZE))) pte_t entry_pg_table_vram[NR_PTE_PER_PAGE] = { [0] = (0) | PTE_P | PTE_W, };__attribute__ ((__aligned__(PAGE_SIZE))) pde_t entry_pg_dir[1024] = { [0] = (0) | PTE_P | PTE_W, };
可以看到,目前将物理地址0~4M映射到了虚拟地址0~4M以及3G~3G+4M。也就是说,现在访问3G~3G+4M虚拟地址的时候,MMU找到的物理地址是0~4M。这只是内核启动时候的权宜之计,紧接着就会重新设置内核页表。
void mm_t::init(){ init_mem_range(); init_paging(); init_free_area();}
内存管理的初始化如上面所示,mem_range的初始化上一篇已经说过了,它会通过BIOS获取各个可用的内存区间。然后会重新初始化页表,然后初始化空闲内存页,用于物理内存管理,后面再做。
void mm_t::init_paging(){ // mem for m_kernel_pg_dir m_kernel_pg_dir = (pde_t *)boot_mem_alloc(PAGE_SIZE, 1); memset(m_kernel_pg_dir, 0, PAGE_SIZE); // first 1MB: KERNEL_BASE ~ KERNEL_LOAD -> 0~1M map_pages(m_kernel_pg_dir, (uint8 *)KERNEL_BASE, 0, EXTENED_MEM, PTE_W); // kernel text + rodata: KERNEL_LOAD ~ data -> 1M ~ VA2PA(data) map_pages(m_kernel_pg_dir, (uint8 *)KERNEL_LOAD, VA2PA(KERNEL_LOAD), VA2PA(data) - VA2PA(KERNEL_LOAD), 0); // kernel data + memory: data ~ KERNEL_BASE+MAX_PHY_MEM -> VA2PA(data) ~ MAX_PHY_MEM map_pages(m_kernel_pg_dir, data, VA2PA(data), VA2PA(m_mem_end) - VA2PA(data), PTE_W); // map the video vram mem uint32 screen_vram = (uint32)os()->get_screen()->vram(); m_kernel_pg_dir[((uint32)screen_vram)>>22] = ((uint32)(VA2PA(entry_pg_table_vram)) | (PTE_P | PTE_W)); set_cr3(VA2PA(m_kernel_pg_dir)); // FIXME: debug test_page_mapping();}
如上面代码所示,映射所有可用的物理内存到内核虚拟地址3G~3G+mem_end。
之后设置了video vram的页表,然后重置cr3寄存器为新页表的物理地址。
void mm_t::map_pages(pde_t *pg_dir, void *va, uint32 pa, uint32 size, uint32 perm){ uint8 *v = (uint8 *) (((uint32)va) & PAGE_MASK); uint8 *e = (uint8 *) (((uint32)va + size) & PAGE_MASK); pa = (pa & PAGE_MASK); pde_t *pde = &pg_dir[PD_INDEX(va)]; pte_t *pg_table; while (v < e) { if ((*pde) & PTE_P) { pg_table = (pte_t *)(PA2VA((*pde) & PAGE_MASK)); } else { pg_table = (pte_t *)boot_mem_alloc(PAGE_SIZE, 1); memset(pg_table, 0, PAGE_SIZE); *pde = (VA2PA(pg_table) | PTE_P | PTE_W | 0x04); } pde++; for (uint32 i = PT_INDEX(v); i < NR_PTE_PER_PAGE && v < e; i++, v += PAGE_SIZE, pa += PAGE_SIZE) { pte_t *pte = &pg_table[i]; if (v < e) { *pte = pa | PTE_P | perm; } } }}
页表设置根据指定的物理地址、虚拟地址建立映射关系。
void mm_t::test_page_mapping(){ uint32 total = 0; for (uint8 *v = (uint8 *)KERNEL_BASE; v < m_mem_end; v += 1*KB) { pde_t *pde = &m_kernel_pg_dir[PD_INDEX(v)]; if ((*pde) & PTE_P) { pte_t *pg_table = (pte_t *)(PA2VA(((*pde) & PAGE_MASK))); pte_t *pte = &pg_table[PT_INDEX(v)]; if (!((*pte) & PTE_P)) { console()->kprintf(WHITE, "page fault: v: 0x%p, *pde: 0x%p, *pte: 0x%p\n", v, *pde, *pte); break; } } else { console()->kprintf(WHITE, "page fault2: v: 0x%p, *pde: 0x%p\n", v, *pde); break; } uint8 x = *v; total += x; }}
然后做了一个简单的测试,尝试访问所有地址,如果页表设置的有问题,会出现Page Fault。
到目前为止,实现了设置内核页目录、页表、开启分页,可以在内核中通过虚拟地址寻址。因为现在还都是内核在执行,未进入用户态,所以只需要映射内核虚拟地址就可以了。至于怎样进入用户空间,后面再描述。
- babyos2(5)——分页
- babyos2(0)——从零开始
- babyos2(1)——boot
- babyos2(3)—— console, kprintf
- babyos2(4)——memory ranges
- babyos2(9)——系统调用
- babyos2(16)—— sleep, wakeup
- babyos2(2)—— load elf format kernel
- babyos2(6)——IDT,中断,异常
- babyos2(8)——读IDE硬盘
- babyos2(11)——物理内存管理,伙伴系统
- babyos2(12)——pagefault, vm_area, mmap
- babyos2(15)—— bug fix 1
- babyos2(7)——键盘中断、时钟中断、实时钟
- babyos2(10)——进程,调度,fork,exec,用户空间
- babyos2(13)——进程页表,fork, COW(copy on write)
- babyos2(14)—— 用户态栈的扩展,加载elf
- DataGrid连接Access的快速分页法(5)——实现快速分页
- 数据结构与算法
- for中的i++与++i的差别
- 解决本地tomcat部署项目乱码问题
- Euclid(欧几里得)算法
- Effective Java读书笔记-覆盖equals时总要覆盖hashCode
- babyos2(5)——分页
- OpenCV学习第五篇:图像操作
- HRBUST 1547
- Idea自带的文件过滤功能,功能类似gitignore不过只能过滤文件夹
- LSH系列二:p-稳定E2LSH
- T
- 如何编写测试用例(APP)
- 关于链表的C语言实现(中级)
- SSH框架和SVN技术分析以及客户端的使用