从零实现一个操作系统-day14

我的博客: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之前加入这两个段
现在测试就成功了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章