10.虚拟内存管理的实现

1.段页式内存管理

虚拟地址到物理地址通过由页目录和页表组成的二级页表映射,页目录的地址放置在CR3寄存器里。

image-20240513203948806

段页式内存管理:

image-20240515100322404

该过程需要三次访问内存,为提高执行速度,可以增加一个快表,访问数据时利用段号和页号检索它,若可以命中,直接取出物理帧号;否则,进行上述三次内存访问过程获得数据。

  • 将进程按照逻辑模块分段,然后再将各段分页
  • 段页式管理外部采用段的优点,即用户可根据逻辑功能进行分段,内部采用页的优点,进行系统的固定分页,取消了段的长度不等造成的开销,由固定大小的页代替,提高内存利用率

段页式内存管理的优点段式使得内存分配更灵活,页式使得内存碎片减少,段页式内存管理可以方便地重用内存页,提高内存利用效率。

  1. 灵活的内存分配
    • 段页式内存管理允许将内存分成逻辑段,每个段可以独立地增长和缩减。这种灵活性使得程序可以按需分配内存,从而提高内存利用率。
  2. 内存保护
    • 每个段都有独立的段描述符,描述符中包含了段的基址、大小和访问权限。这种机制可以防止进程之间的内存越界访问,提高系统的稳定性和安全性。
  3. 简化的地址空间
    • 段页式内存管理将逻辑地址转换为物理地址时,先通过段表找到段,再通过页表找到页框。这种两级映射简化了内存管理,便于操作系统管理多个进程的内存。
  4. 减少内存碎片
    • 页式内存管理可以有效地减少内存碎片,因为它将内存分成固定大小的页框。段页式内存管理继承了这一优点,从而减少了内存分配和释放过程中产生的内存碎片。
  5. 虚拟内存支持
    • 段页式内存管理可以与虚拟内存机制结合使用,支持将不常用的内存页交换到磁盘上,从而扩展系统的有效内存容量。这种机制可以提高系统的多任务处理能力,允许更多的进程同时运行。
  6. 模块化编程
    • 段页式内存管理支持模块化编程,程序员可以将程序分成多个逻辑段(如代码段、数据段、堆栈段等),每个段可以独立管理。这种方式有助于程序的开发和维护。
  7. 共享和重用
    • 操作系统可以允许不同的进程共享同一个段(如共享库或代码段),从而节省内存空间。此外,段页式内存管理可以方便地重用内存页,提高内存利用效率。

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指针
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; // 1000 0000 0000 0000 0000 0000 0000 0000
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();
//console_write_color("Hello, OS kernel!\n", rc_black, rc_green);
//panic("test");



printk_color(rc_black, rc_green, "Hello OS!!!\n");

init_timer(200);

// `sti` 指令的作用是设置中断标志(Set Interrupt Flag)
// 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); // 换算成kb

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
// 声明全局的 multiboot_t * 指针
extern multiboot_t *glb_mboot_ptr;

// 声明全局的multiboot 指针
// 内核未建立分页机制前缓存的指针
extern multiboot_t *mboot_ptr_tmp;

drivers/console.c 修改文本模式下显存的起始位置,原先的地址0xB8000 加上偏移地址 0xC0000000 才能在分页模式下访问到

1
2
3
#include "vmm.h"
// VGA 的显示缓冲的起点 0xB8000 需要加上分页地址
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;
// 在 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 虚拟内存管理的初始化

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()
{
// 0xC0000000 这个地址在页目录的索引
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++) {
// 此处是内核虚拟地址,MMU 需要物理地址,所以减去偏移,下同
pgd_kern[i] = ((uint32_t)pte_kern[j] - PAGE_OFFSET) | PAGE_PRESENT | PAGE_WRITE;
}

uint32_t *pte = (uint32_t *)pte_kern;
// 不映射第 0 页,便于跟踪 NULL 指针
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;

// 注册页错误中断的处理函数 ( 14 是页故障的中断号 )
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;

// 转换到内核线性地址并清 0
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));
}

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));
}

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);

// 如果地址有效而且指针不为NULL,则返回地址
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

/**
12 * P−− 位 0 是存在 (Present) 标志,用于指明表项对地址转换是否有效。
13 * P = 1 表示有效; P = 0 表示无效。
14 * 在页转换过程中,如果说涉及的页目录或页表的表项无效,则会导致一个异常。
15 * 如果 P = 0 ,那么除表示表项无效外,其余位可供程序自由使用。
16 * 例如,操作系统可以使用这些位来保存已存储在磁盘上的页面的序号。
17 */
#define PAGE_PRESENT 0x1

/**
21 * R/W −− 位 1 是读 / 写 (Read/Write) 标志。如果等于 1 ,表示页面可以被读、写或执行。
22 * 如果为 0 ,表示页面只读或可执行。
23 * 当处理器运行在超级用户特权级(级别 0,1 或) 2 时,则 R/W 位不起作用。
24 * 页目录项中的 R/W 位对其所映射的所有页面起作用。
25 */
#define PAGE_WRITE 0x2

/**
29 * U/S −− 位 2 是用户 / 超级用户 (User/Supervisor) 标志。
30 * 如果为 1 ,那么运行在任何特权级上的程序都可以访问该页面。
31 * 如果为 0 ,那么页面只能被运行在超级用户特权级 (0,1 或 2) 上的程序访问。
32 * 页目录项中的 U/S 位对其所映射的所有页面起作用。
33 */
#define PAGE_USER 0x4

// 虚拟分页的大学 4KB
#define PAGE_SIZE 4096

// 页掩犸
#define PAGE_MASK 0xFFFFF000
// 获取一个地址的页目录项 右移动22
#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))

// 映射 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

**当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);

// 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);
}

objdump - h hx…

image-20240515153030548

得到这个

image-20240515152938282

显然结果不太对劲 —

1
2
3
C_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-builtin -fno-stack-protector -I include
#将makefile 修改如下 即添加-fno-pic
C_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-pic -fno-builtin -fno-stack-protector -I include

image-20240520221554124

非位置无关代码

-fno-pic

位置无关码PIC详解:原理、动态链接库、代码重定位_-pic 位置无关代码-CSDN博客

  • -fno-pic 是 GCC 编译器的一个编译选项,用于生成非位置无关代码(Non-Position Independent Code, 非PIC)

位置无关代码:

位置无关代码是一种在加载时可以不依赖于固定的内存地址而运行的代码。PIC 通常用于共享库(shared libraries),因为它允许相同的代码在不同的进程地址空间中加载到不同的地址。

  • 编译时生成的代码可以在内存中的任何位置运行。
  • 通常使用相对地址进行跳转和数据访问。

非位置无关代码:

  • 编译时生成的代码预期在特定的内存地址运行。

  • 使用绝对地址进行跳转和数据访问。

  • 操作系统内核通常加载在固定的物理地址或虚拟地址空间中,因此不需要位置无关的特性。

  • 内核代码必须能够直接访问硬件和内存,这需要使用绝对地址。