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。

到目前为止,实现了设置内核页目录、页表、开启分页,可以在内核中通过虚拟地址寻址。因为现在还都是内核在执行,未进入用户态,所以只需要映射内核虚拟地址就可以了。至于怎样进入用户空间,后面再描述。

原创粉丝点击