babyos2(5)——分页

加载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 entry
entry:
    # 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),   %edi
1:
    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),%edi
2:
    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。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值