10.虚拟内存管理的实现
1.段页式内存管理
虚拟地址到物理地址通过由页目录和页表组成的二级页表映射,页目录的地址放置在CR3寄存器里。
段页式内存管理:
该过程需要三次访问内存,为提高执行速度,可以增加一个快表,访问数据时利用段号和页号检索它,若可以命中,直接取出物理帧号;否则,进行上述三次内存访问过程获得数据。
- 将进程按照逻辑模块分段,然后再将各段分页
- 段页式管理外部采用段的优点,即用户可根据逻辑功能进行分段,内部采用页的优点,进行系统的固定分页,取消了段的长度不等造成的开销,由固定大小的页代替,提高内存利用率
段页式内存管理的优点:段式使得内存分配更灵活,页式使得内存碎片减少,段页式内存管理可以方便地重用内存页,提高内存利用效率。
- 灵活的内存分配:
- 段页式内存管理允许将内存分成逻辑段,每个段可以独立地增长和缩减。这种灵活性使得程序可以按需分配内存,从而提高内存利用率。
- 内存保护:
- 每个段都有独立的段描述符,描述符中包含了段的基址、大小和访问权限。这种机制可以防止进程之间的内存越界访问,提高系统的稳定性和安全性。
- 简化的地址空间:
- 段页式内存管理将逻辑地址转换为物理地址时,先通过段表找到段,再通过页表找到页框。这种两级映射简化了内存管理,便于操作系统管理多个进程的内存。
- 减少内存碎片:
- 页式内存管理可以有效地减少内存碎片,因为它将内存分成固定大小的页框。段页式内存管理继承了这一优点,从而减少了内存分配和释放过程中产生的内存碎片。
- 虚拟内存支持:
- 段页式内存管理可以与虚拟内存机制结合使用,支持将不常用的内存页交换到磁盘上,从而扩展系统的有效内存容量。这种机制可以提高系统的多任务处理能力,允许更多的进程同时运行。
- 模块化编程:
- 段页式内存管理支持模块化编程,程序员可以将程序分成多个逻辑段(如代码段、数据段、堆栈段等),每个段可以独立管理。这种方式有助于程序的开发和维护。
- 共享和重用:
- 操作系统可以允许不同的进程共享同一个段(如共享库或代码段),从而节省内存空间。此外,段页式内存管理可以方便地重用内存页,提高内存利用效率。
2.内存映射
Linux采用的方案是 把内核映射到线性地址空间3G以上,而应用程序占据线性地址空间0-3G的位置。我们的内 核采取和Linux内核一样的映射,把物理地址0从虚拟地址0xC0000000(3G)处开始往上映 射,因为我们只管理最多512MB的内存,所以3G-4G之间能完全的映射全部的物理地址。
物理地址和内核虚拟地址满足以下的关系:
物理地址 + 0xC0000000 = 内核虚拟地址
VMA(Virtual Memory Address):链接器生成可执行文件时的偏移计算地址,
LMA(Load Memory Address):区段所载入内存的 实际地址
通常情况下,VMA = LMA
问题:如果简单的 把0xC0100000 修改为代码段的起始位置,那么会报错, 因为GRUB是从1MB处加载内核的,而链接器是以0xC0100000这个参考地址进行地址重定位的。此时尚未开启虚拟页面映射,运行 涉及到寻址的代码肯定就会出错。— 链接器将内核重定位到虚拟地址 0xC0100000
,但是在虚拟地址映射启用之前,所有的内存访问都是物理地址。直接访问 0xC0100000
会导致访问失败,因为该地址在物理内存中没有对应的实际位置
解决方案:有一段程序和数据按照 0x100000的地址进行重定位,能帮助我们设置好一个临时的页表,再跳转到内核入口函数
GCC提供了这样的扩展机制:允许程序员指定某个函数或者某个变量所存储的区段。 同时ld的链接脚本又可以自由定制,所以这个无解的问题就有了解决方案。用于设置这个 临时页表和函数我们指定它存储在.init段,只需要指定该段从0x100000地址开始,其他 的.text和.data等段按照0xC0100000作为起始地址即可。当然这里还有要注意的细节, 具体在下面的新链接脚本中可以看。
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
| 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) } }
|
. += 0xC0000000; 从这里开始是虚拟地址的映射
1 2 3 4 5 6
| . += 0xC0000000; .text : AT(ADDR(.text) - 0xC0000000) { *(.text) . = ALIGN(4096); }
|
- **
AT(ADDR(.text) - 0xC0000000)
**:指定 .text
段的加载地址(物理地址)为当前虚拟地址减去 0xC0000000
。这意味着虚拟地址 0xC0100000
的代码实际加载在物理地址 0x100000
。
- **
\*(.text)
**:将所有 .text
段的内容放入此处。
- **
. = ALIGN(4096)
**:将下一个地址对齐到 4096 字节(4KB)的边界
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 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
| ; ---------------------------------------------------------------- ; ; boot.s -- 内核从这里开始 ; ; 这里还有根据 GRUB Multiboot 规范的一些定义 ; ; ----------------------------------------------------------------
MBOOT_HEADER_MAGIC equ 0x1BADB002 ; Multiboot 魔数,由规范决定的
MBOOT_PAGE_ALIGN equ 1 << 0 ; 0 号位表示所有的引导模块将按页(4KB)边界对齐 MBOOT_MEM_INFO equ 1 << 1 ; 1 号位通过 Multiboot 信息结构的 mem_* 域包括可用内存的信息 ; (告诉GRUB把内存空间的信息包含在Multiboot信息结构中)
; 定义我们使用的 Multiboot 的标记 MBOOT_HEADER_FLAGS equ MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO
; 域checksum是一个32位的无符号值,当与其他的magic域(也就是magic和flags)相加时, ; 要求其结果必须是32位的无符号值 0 (即magic + flags + checksum = 0) MBOOT_CHECKSUM equ - (MBOOT_HEADER_MAGIC + MBOOT_HEADER_FLAGS)
; 符合Multiboot规范的 OS 映象需要这样一个 magic Multiboot 头
; Multiboot 头的分布必须如下表所示: ; ---------------------------------------------------------- ; 偏移量 类型 域名 备注 ; ; 0 u32 magic 必需 ; 4 u32 flags 必需 ; 8 u32 checksum 必需 ; ; 我们只使用到这些就够了,更多的详细说明请参阅 GNU 相关文档 ;-----------------------------------------------------------
;-----------------------------------------------------------------------------
[BITS 32] ; 所有代码以 32-bit 的方式编译
section .init.text ; 临时代码段从这里开始
; 在代码段的起始位置设置符合 Multiboot 规范的标记
dd MBOOT_HEADER_MAGIC ; GRUB 会通过这个魔数判断该映像是否支持 dd MBOOT_HEADER_FLAGS ; GRUB 的一些加载时选项,其详细注释在定义处 dd MBOOT_CHECKSUM ; 检测数值,其含义在定义处
[GLOBAL start] ; 内核代码入口,此处提供该声明给 ld 链接器 [GLOBAL mboot_ptr_tmp] ; 全局的 struct multiboot * 变量 [EXTERN kern_entry] ; 声明内核 C 代码的入口函数
start: cli ; 此时还没有设置好保护模式的中断处理,所以必须关闭中断 mov [mboot_ptr_tmp], ebx ; 将 ebx 中存储的指针存入 glb_mboot_ptr 变量 mov esp, STACK_TOP ; 设置内核栈地址,按照 multiboot 规范,当需要使用堆栈时,OS 映象必须自己创建一个 and esp, 0FFFFFFF0H ; 栈地址按照 16 字节对齐 mov ebp, 0 ; 帧指针修改为 0 call kern_entry ; 调用内核入口函数
;-----------------------------------------------------------------------------
section .init.data ; 开启分页前临时的数据段 stack: times 1024 db 0 ; 这里作为临时内核栈 STACK_TOP equ $-stack-1 ; 内核栈顶,$ 符指代是当前地址
mboot_ptr_tmp: dd 0 ; 全局的 multiboot 结构体指针
;-----------------------------------------------------------------------------
|
主要的修改是第5行的代码所在段声明和第29行的数据所在段声明,因为此处代码和 数据是在参考0x100000(1MB)编址的。所以在进入分页后需要更换新的内核栈和新的 multiboot结构体指针。除此之外,仍就需要指定kern_entry函数所在区段为.init.text 段,并且在该函数中建立临时页表并跳转到高虚拟地址处的kern_init函数正式执行
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| #include "types.h" #include "console.h" #include "debug.h" #include "gdt.h" #include "idt.h" #include "timer.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!!!\n"); init_timer(200); 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_page_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"); } }
|
解析:
__attribute__((section(".init.data")))
是GCC编译器的扩展功能, 用来指定变量或者函数的存储区段。
- 将虚拟地址
0xC0000000
(高端虚拟地址,通常对应于内核的虚拟地址空间)开始的 4MB 映射到物理地址 0-4MB
。
- 同时将虚拟地址
0-4MB
直接映射到物理地址 0-4MB
。这称为“恒等映射”(identity mapping)。
理解:
- 当启用分页(通过将
CR0
寄存器的最高位置为 1)时,CPU 立即开始按照分页机制来进行内存寻址。
- 如果没有进行恒等映射,CPU 将无法正确地执行当前正在运行的代码,因为这些代码在启用分页之前是以物理地址方式访问内存的。
- 在启用分页之前,
kern_entry
函数及其调用的代码是按物理地址访问的。
- 一旦启用分页,所有内存访问都将基于页表进行。为了确保在切换过程中代码可以继续运行,必须保证这些地址的映射是正确的
也就是说 当CR0 最高位置1 的时候也就是 启动分页时,这之前都运行在物理内存,一旦启用分页将会立马切换到分页模式,为了保证当前代码能够顺利进行,也会将物理的地址的0-4MB 同时映射到 虚拟地址的0-4MB 和 虚拟地址的高端地址 0xC0000000的4MB上,低端恒等映射的主要目的是确保在分页机制切换过程中,所有正在执行的代码地址依旧有效。
include/multiboot.h 更新声明
1 2 3 4 5 6
| extern multiboot_t *glb_mboot_ptr;
extern multiboot_t *mboot_ptr_tmp;
|
drivers/console.c 修改文本模式下显存的起始位置,原先的地址0xB8000 加上偏移地址 0xC0000000 才能在分页模式下访问到
1 2 3
| #include "vmm.h"
static uint16_t *video_memory = (uint16_t *)(0xB8000 + PAGE_OFFSET);
|
kern/debug/elf.c 低端内存地址 也需要更改 libs/elf.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| #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 < PTE_COUNT + kern_pte_first_idx; 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_COUNT * PTE_SIZE; i++) { pte[i] = (i << 12) | PAGE_PRESENT | PAGE_WRITE; }
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 pd) { asm volatile ("mov %0, %%cr3" : : "r" (pd)); }
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; }
|
__attribute__ ((aligned(PAGE_SIZE)))
是GCC的扩展指令,功能是使得变量的起始地址按照某个数值 对齐,所以我们轻轻松松的就解决了这个难题。
include/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 86 87 88 89
| #ifndef INCLUDE_VMM_H #define INCLUDE_VMM_H
#include "types.h" #include "idt.h" #include "vmm.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 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
|
**当cpu 进入分页模式,一旦发生内存访问的页错误,就会产生14号中断 **
mm/page_fault.c 14号中断处理函数
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
| #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); }
|
objdump - h hx…
得到这个
显然结果不太对劲 —
1 2 3
| C_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-builtin -fno-stack-protector -I include
C_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-pic -fno-builtin -fno-stack-protector -I include
|
非位置无关代码
-fno-pic
位置无关码PIC详解:原理、动态链接库、代码重定位_-pic 位置无关代码-CSDN博客
-fno-pic
是 GCC 编译器的一个编译选项,用于生成非位置无关代码(Non-Position Independent Code, 非PIC)
位置无关代码:
位置无关代码是一种在加载时可以不依赖于固定的内存地址而运行的代码。PIC 通常用于共享库(shared libraries),因为它允许相同的代码在不同的进程地址空间中加载到不同的地址。
- 编译时生成的代码可以在内存中的任何位置运行。
- 通常使用相对地址进行跳转和数据访问。
非位置无关代码: