从零开始搭建自制操作系统——物理内存管理器&简单分页机制实现
这里开始我了解到其实可以选择用GRUB 作为bootloader来加载入自制的内核,不过既然我打算从零开始实现一个学习用的操作系统,这里就用自己写的bootloader。实际上我一开始实现的bootloader 还有些小问题可以优化。
bootloader优化
启用A20总线
之前我们实现的bootloader并没有手动启用A20总线,这里对
https://wiki.osdev.org/A20_Line
这里给出的启动A20总线的汇编代码给出解释。
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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 ; out: ; ax - state (0 - disabled, 1 - enabled) get_a20_state: pushf push si push di push ds push es cli mov ax, 0x0000 ; 0x0000:0x0500(0x00000500) -> ds:si mov ds, ax mov si, 0x0500 not ax ; 0xffff:0x0510(0x00100500) -> es:di mov es, ax mov di, 0x0510 mov al, [ds:si] ; save old values mov byte [.BufferBelowMB], al mov al, [es:di] mov byte [.BufferOverMB], al mov ah, 1 ; check byte [0x00100500] == byte [0x0500] mov byte [ds:si], 0 mov byte [es:di], 1 mov al, [ds:si] cmp al, [es:di] jne .exit dec ah .exit: mov al, [.BufferBelowMB] mov [ds:si], al mov al, [.BufferOverMB] mov [es:di], al shr ax, 8 sti pop es pop ds pop di pop si popf ret .BufferBelowMB: db 0 .BufferOverMB db 0 ; out: ; ax - a20 support bits (bit #0 - supported on keyboard controller; bit #1 - supported with bit #1 of port 0x92) ; cf - set on error query_a20_support: push bx clc mov ax, 0x2403 int 0x15 jc .error test ah, ah jnz .error mov ax, bx pop bx ret .error: stc pop bx ret enable_a20_keyboard_controller: cli call .wait_io1 mov al, 0xad out 0x64, al call .wait_io1 mov al, 0xd0 out 0x64, al call .wait_io2 in al, 0x60 push eax call .wait_io1 mov al, 0xd1 out 0x64, al call .wait_io1 pop eax or al, 2 out 0x60, al call .wait_io1 mov al, 0xae out 0x64, al call .wait_io1 sti ret .wait_io1: in al, 0x64 test al, 2 jnz .wait_io1 ret .wait_io2: in al, 0x64 test al, 1 jz .wait_io2 ret ; out: ; cf - set on error enable_a20: clc ; clear cf pusha mov bh, 0 ; clear bh call get_a20_state jc .fast_gate test ax, ax jnz .done call query_a20_support mov bl, al test bl, 1 ; enable A20 using keyboard controller jnz .keybord_controller test bl, 2 ; enable A20 using fast A20 gate jnz .fast_gate .bios_int: mov ax, 0x2401 int 0x15 jc .fast_gate test ah, ah jnz .failed call get_a20_state test ax, ax jnz .done .fast_gate: in al, 0x92 test al, 2 jnz .done or al, 2 and al, 0xfe out 0x92, al call get_a20_state test ax, ax jnz .done test bh, bh ; test if there was an attempt using the keyboard controller jnz .failed .keybord_controller: call enable_a20_keyboard_controller call get_a20_state test ax, ax jnz .done mov bh, 1 ; flag enable attempt with keyboard controller test bl, 2 jnz .fast_gate jmp .failed .failed: stc .done: popa ret
这段代码模块划分清晰,大致包含:
1 2 3 4 5 enable_a20 ├── get_a20_state ; 检查当前是否启用了 A20(比较内存内容) ├── query_a20_support ; 使用 BIOS 查询系统支持哪些 A20 启用方式 ├── enable_a20_keyboard_controller ; 使用键盘控制器开启 A20 ├── fallback: port 0x92 Fast A20 Gate 或 BIOS int 0x15
get_a20_state
– 检查 A20 是否已经启用
1 2 3 4 mov ds, 0x0000 mov si, 0x0500 mov es, 0xFFFF mov di, 0x0510
对比 0x00000500
和 0x00100510
两个地址是否是 两个不同位置 :
如果 A20 未启用 ,访问 0x00100510
会
wrap 到 0x00000510
,写入值一样
如果 A20 启用 ,则能看到两个地址独立
通过这两个地址互写不同的值,然后判断它们是否相等来判断 A20 状态。
query_a20_support
– 用 int 0x15, ax=0x2403
检查系统支持的 A20 开启方式
BIOS 返回 BX
的位表示支持方式:
Bit
方式
0
键盘控制器(KBC)
1
快速 A20 端口(0x92)
这部分是用于判断优先使用哪种方法开启 A20。
enable_a20_keyboard_controller
– 用键盘控制器方式开启
A20
1 2 3 4 5 6 7 out 0x64, 0xAD ; 禁用键盘 out 0x64, 0xD0 ; 命令: 读输出端口 in al, 0x60 ; 读取当前值 or al, 2 ; 设置 A20(bit1) out 0x64, 0xD1 ; 命令: 写输出端口 out 0x60, al ; 写入修改值 out 0x64, 0xAE ; 重新启用键盘
这种方式比较传统但慢,兼容性较好。
.fast_gate
– 使用端口 0x92
快速开启
A20(现代推荐)
1 2 3 4 in al, 0x92 or al, 2 ; 设置 A20 enable(bit1) and al, 0xFE ; 清除重启位(bit0) out 0x92, al
这种方式不经过键盘控制器,简单、快速,现代系统普遍支持。
enable_a20
– 主函数流程逻辑总结
尝试检测是否已经开启 A20 ,如果是,直接返回。
查询系统支持的开启方式 。
优先尝试:
BIOS int 15h
(启用 A20)
快速端口(0x92)
键盘控制器(慢)
每次尝试后重新调用 get_a20_state
检查是否启用成功。
若都失败,返回 CF=1 代表失败。
但实际上当我进行调试时发现,在一开始get_a20_state时,两者就已经相等,直接跳转到.done了,这是因为QEMU、VirtualBox、VMware
等虚拟机 几乎都会提前启用 A20。
获取可用内存
INT 15h, AX=E820h
是 BIOS
提供的一个服务,用于返回系统物理内存的分布情况(即内存映射表),告诉你哪些内存是可用的、保留的、ACPI
的、设备的等等。这对后续实现物理内存分配器具有重要作用。
调用方式如下:
1 2 3 4 5 6 7 mov ax, 0xe820 ; 功能号 mov edx, 0x534D4150 ; "SMAP" 魔数,必须设定,否则调用失败 mov ecx, 20 ; 请求的数据结构大小(bytes) mov ebx, 0 ; 第一次调用时 EBX = 0,之后 BIOS 会填充下一个索引 mov es:di, buffer ; 内存缓冲区,用于接受返回的结构体 int 0x15
返回值
寄存器
含义
CF
清除,表示成功(即 CF=0)
EAX
返回 0x534D4150
("SMAP"),确认有效响应
ECX
返回实际写入的字节数(一般为 20)
EBX
下一个结构的索引(递归查询用,传入 0 获取第一个)
ES:DI
缓冲区里存放了以下结构体:
结构体格式(20字节,每次读取)
1 2 3 4 5 typedef struct { uint64_t base_addr; uint64_t length; uint32_t type; } __attribute__((packed)) E820Entry;
Type 值
含义
1
可用内存(可用于加载内核)
2
保留区(系统/设备使用)
3
ACPI 可恢复内存
4
ACPI NVS
5+
保留
我们可以用如下方式进行循环读取到0x5000地址处,每个结构体大小为20字节,其中我们把读取的个数存在0x4FFE地址处。
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 get_memory_map: push es ; 设置 ES:DI = 0x5000 mov ax, 0x500 mov es, ax xor di, di ; ES:DI = 0x5000 xor ebx, ebx ; 初始化 continuation value 为 0 mov word [0x4FFE], 0 ; 保存 entry 总数 .get_next_entry: mov eax, 0xE820 mov edx, 0x534D4150 ; 'SMAP' mov ecx, 20 ; 请求返回结构大小为 20 字节(不含 ACPI) mov dword [es:di + 20], 1 ; 保证 type 有默认值 int 0x15 jc .done ; 如果调用失败,跳出 cmp eax, 0x534D4150 jne .done ; 如果返回的签名不正确,退出 add di, 20 ; 下一个结构体写入位置(每个是 20 字节) inc word [0x4FFE] ; 增加 entry 统计数量 test ebx, ebx jne .get_next_entry ; 如果 EBX 非 0,则还有更多条目 .done: pop es ret
之后我们写一个打印这个结构体的函数,就可以看到对应空闲可分配的物理内存了(type=1)。
更换内核加载地址
自己搓的小内核从0x9000-0x9FC00的大约75KB,这点大小可能以后会不够用,所以最好将其加载到0x100000处。这里我们开启A20总线后,实际上我们就可以通过段寄存器加偏移访问到1MB以上的内存了,比如ES:BX = 0xFFFF:0x10 => 0x100000
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ; boot.asm ; 读取 kernel (加载 kernel.c 编译后的镜像) 到 0x100000 mov ah, 0x02 ; BIOS function: read sector mov al, 20 ; 读取 20 个扇区 mov ch, 0 ; 柱面 mov cl, 4 ; 起始扇区(第4个扇区) mov dh, 0 ; 磁头 mov dl, 0x00 ; 软盘 mov bx, 0xffff ; 加载地址0x100000 , 0xffff * 16 + 0x10 = 0x100000 mov es, bx mov bx, 0x10 int 0x13 jc disk_error
这里stage2跳转到内核需要进行一些修改,retf
是
远返回 指令,会从栈顶弹出 offset 和
segment(段选择器)并跳转。所以通过先 push 目标段地址、再 push
目标偏移,就可以模拟远跳转。
1 2 3 4 5 6 7 8 9 ; stage2.asm [BITS 32] start: mov eax, 0x100000 ; 内核入口地址 push dword 0x08 ; 代码段选择器 push eax ; 入口地址 retf ; 执行远返回,相当于 jmp 0x08:0x100000
然后把linker.ld 的加载起始地址换成0x100000,就可以正确进入内核执行了。
但这里还有个调试的问题,这里real-mode-gdb 打印时是根据IP的地址进行打印,这里我们所以看到的code又从0地址处开始了,实际上执行还是正常的。
那我们看到gdb_init_real_mode.txt 里是怎么定义的,这里打印CODE是用x /i $rip
,这里我们改成eip 即可。调试相关内容可见第一章节 。
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 define context printf "---------------------------[ STACK ]---\n" _dump_memw $r_ss_sp 8 printf "\n" set $_a = $r_ss_sp + 16 _dump_memw $_a 8 printf "\n" printf "---------------------------[ DS:SI ]---\n" print_data $ds $rsi printf "---------------------------[ ES:DI ]---\n" print_data $es $rdi printf "----------------------------[ CPU ]----\n" print_regs print_eflags printf "---------------------------[ CODE ]----\n" set $_code_size = $CODE_SIZE # disassemble # first call x/i with an address # subsequent calls to x/i will increment address if ($_code_size > 0) x /i $rip set $_code_size-- end while ($_code_size > 0) x /i set $_code_size-- end end
PMM实现
bitmap方案
pmm是一个物理内存的分配器,是后续实现slub等内存分配器的基础,所以我们要先进行实现。这里比较简单高效的方法是维护一个位图,一个二进制位表示一个内存页是否被分配出去。但实际上我们维护的bitmap 是一个一个字节存在数组里的,每个bitmap 表项代表8个物理页面。
这里我为了方便,选择把第一个可用entry完全交给内核使用。其中我把MAX_PHYS_MEM_SIZE 直接写死到宏了,而不是通过之前得到的memory_map 来获取最大可用物理空间大小,这是因为bitmap 的定义此时只能在声明时指定其大小,目前还没有堆管理机制的动态内存可供分配,这里就简单的先手动指定一下,这时我们需要注意要进行边界检查,因为memory_map 得到的内存段可能会超过bitmap 的大小,从而在clear_bit 时发生越界写。这里自己实现的简易printf 函数调用了之前实现的screen.c 里的输出到显存的接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #pragma once #include <stdint.h> void pmm_init (uint32_t mem_size) ;void * pmm_alloc_page () ;void pmm_free_page (void * p) ;void pmm_print_info () ;
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 #include "pmm.h" #include "printf.h" #include "screen.h" #include "common.h" #include "layout.h" #define PAGE_SIZE 4096 #define MAX_PHYS_MEM_SIZE (1024 * 1024 * 128) #define BITMAP_SIZE (MAX_PHYS_MEM_SIZE / PAGE_SIZE / 8) static uint8_t bitmap[BITMAP_SIZE];static uint32_t total_pages;struct MemoryMapEntry { uint64_t base_addr; uint64_t length; uint32_t type; }; void set_bit (uint32_t bit) { bitmap[bit / 8 ] |= (1 << (bit % 8 )); } void clear_bit (uint32_t bit) { bitmap[bit / 8 ] &= ~(1 << (bit % 8 )); } int test_bit (uint32_t bit) { return bitmap[bit / 8 ] & (1 << (bit % 8 )); } void pmm_init (uint32_t memory_map_addr) { uint16_t entry_count = *(uint16_t *)MEMORY_MAP_COUNT_ADDR; struct MemoryMapEntry * mmap = (struct MemoryMapEntry*)memory_map_addr; total_pages = BITMAP_SIZE * 8 ; for (uint32_t i = 0 ; i < BITMAP_SIZE; i++) { bitmap[i] = 0xFF ; } for (uint32_t i = 1 ; i < entry_count; i++) { if (mmap[i].type != 1 ) continue ; uint64_t base = mmap[i].base_addr; uint64_t length = mmap[i].length; uint32_t start_page = base / PAGE_SIZE; uint32_t end_page = (base + length) / PAGE_SIZE; for (uint32_t j = start_page; j < end_page; j++) { if (j/8 >=BITMAP_SIZE) break ; clear_bit(j); } } extern uint32_t kernel_end; uint32_t used_pages = ((uint32_t )&kernel_end + PAGE_SIZE - 1 ) / PAGE_SIZE; for (uint32_t i = 0 ; i < used_pages; i++) { set_bit(i); } } void * pmm_alloc_page () { for (uint32_t i = 0 ; i < total_pages; i++) { if (!test_bit(i)) { set_bit(i); return (void *)(i * PAGE_SIZE); } } return 0 ; } void pmm_free_page (void * addr) { uint32_t index = (uint32_t )addr / PAGE_SIZE; clear_bit(index); } void pmm_print_info () { const int pages_per_row = 64 ; kernel_printf("Physical Memory Bitmap (#=used, .=free):\n" ); for (uint32_t i = 0 ; i < total_pages; i++) { if (i % pages_per_row == 0 ) { kernel_printf("\n%u: " , i); } print_char(test_bit(i) ? '#' : '.' ); } kernel_printf("\n" ); }
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 #include "idt.h" #include "isr.h" #include "irq.h" #include "printf.h" #include "screen.h" #include "info.h" #include "layout.h" #include "pmm.h" void kernel_main (void ) { clear_screen(); print_string("Hello, Kernel!\n" ); print_memory_map(); idt_install(); irq_remap(); isr_install(); irq_install(); pmm_init(MEMORY_MAP_ADDR); void * page1 = pmm_alloc_page(); kernel_printf("Allocated page: %x\n" ,page1); void * page2 = pmm_alloc_page(); kernel_printf("Allocated page: %x\n" ,page2); pmm_free_page(page1); pmm_free_page(page2); asm volatile ("sti" ) ; while (1 ) { asm volatile ("hlt" ) ; } }
1 2 3 4 5 6 7 8 #pragma once #define MEMORY_MAP_COUNT_ADDR 0x4FFE #define MEMORY_MAP_ADDR 0x5000 #define STAGE2_ADDR 0x8000 #define KERNEL_ADDR 0x9000
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 #include "screen.h" #include "printf.h" #include <stdarg.h> void kernel_printf (const char *format, ...) { va_list args; va_start(args, format); while (*format) { if (*format == '%' ) { format++; if (*format == 'd' ) { int val = va_arg(args, int ); print_dec(val); } else if (*format == 'u' ) { unsigned int val = va_arg(args, unsigned int ); print_unsigned(val); } else if (*format == 'x' ) { unsigned int val = va_arg(args, unsigned int ); print_hex(val); } else if (*format == 'l' && *(format + 1 ) == 'l' && *(format + 2 ) == 'x' ) { format += 2 ; unsigned long long val = va_arg(args, unsigned long long ); print_hex64(val); } else if (*format == 's' ) { const char *str = va_arg(args, const char *); print_string(str); } else if (*format == 'c' ) { char c = (char )va_arg(args, int ); print_char(c); } else { print_char('%' ); print_char(*format); } } else { print_char(*format); } format++; } va_end(args); }
buddy system方案
实际上直接用bitmap方案实现物理内存分配存在诸多问题,比如分配效率较低,每次都需要遍历bitmap来查找第一段可用的内存,尤其是当内存快占用满时效率很低。所以可以换用buddy
system方案。核心思路就是用链表管理不同的连续的页块,连续的大小都为2的幂次,当某个大小的链表为空而又有新的内存分配申请时,就会往高一级大小的链表取大块进行拆分,如果高一级链表也为空,就逐个向上找有块的链表。释放时也会判断与其相邻的块是否空闲,空闲则合并了放入较大大小的链表。
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 #pragma once #include <stdint.h> #define MAX_ORDER 10 #define PAGE_SIZE 4096 #define MAX_PAGES (512 * 1024 * 1024 / 4096) typedef struct Page { struct Page * next ; uint8_t order; uint8_t used; uint8_t is_head; } Page; typedef struct PageLinkList { struct Page * head ; uint32_t count; } PageLinkList; void pmm_init (uint32_t mem_size) ;void * pmm_alloc_page () ;void * pmm_alloc_pages (uint32_t count) ;void pmm_free_page (Page* p) ;void pmm_print_info () ;
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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 #include "pmm.h" #include "printf.h" #include "screen.h" #include "common.h" #include "layout.h" #include "panic.h" struct MemoryMapEntry { uint64_t base_addr; uint64_t length; uint32_t type; }; static Page page_array[MAX_PAGES]; static PageLinkList free_list[MAX_ORDER+1 ]; static uint32_t total_pages;void add_to_free_list (Page* p, uint8_t order) { if (p == NULL ||order > MAX_ORDER||!p->is_head) return ; p->next = free_list[order].head; free_list[order].head = p; free_list[order].count++; } void remove_from_free_list (Page* p, uint8_t order) { if (p == NULL ||order > MAX_ORDER||!p->is_head) return ; if (p == free_list[order].head) { free_list[order].head = p->next; free_list[order].count--; return ; } for (Page* cur = free_list[order].head; cur->next; cur = cur->next){ if (cur->next == p) { cur->next = p->next; free_list[order].count--; return ; } } } Page* fetch_from_free_list (uint8_t order) { if (order > MAX_ORDER) return NULL ; if (free_list[order].count==0 ) return NULL ; Page *p = free_list[order].head; free_list[order].head = p->next; free_list[order].count--; return p; } void pmm_init (uint32_t memory_map_addr) { uint16_t entry_count = *(uint16_t *)MEMORY_MAP_COUNT_ADDR; struct MemoryMapEntry * mmap = (struct MemoryMapEntry*)memory_map_addr; for (uint32_t i = 0 ; i < MAX_PAGES; i++) { page_array[i].used = 1 ; page_array[i].order = 0 ; page_array[i].next = NULL ; page_array[i].is_head = 0 ; } for (uint32_t i = 0 ; i < MAX_ORDER; i++) { free_list[i].head = NULL ; free_list[i].count = 0 ; } extern uint32_t kernel_end; uint32_t kernel_pages = ((uint32_t )&kernel_end + PAGE_SIZE - 1 ) / PAGE_SIZE; kernel_printf("kernel_end: %x\n" , &kernel_end); for (uint32_t i = 1 ; i < entry_count; i++) { if (mmap[i].type != 1 ) continue ; uint32_t base = mmap[i].base_addr; uint32_t length = mmap[i].length; uint32_t start_page = base / PAGE_SIZE; uint32_t end_page = (base + length) / PAGE_SIZE; for (uint32_t j = start_page; j < end_page && j < MAX_PAGES; ) { if (j < kernel_pages) { j = kernel_pages; continue ; } uint32_t order = MAX_ORDER; while (order > 0 ) { uint32_t block_size = 1 << order; if (j % block_size == 0 && j + block_size <= end_page) break ; order--; } page_array[j].order = order; page_array[j].used = 0 ; page_array[j].is_head = 1 ; add_to_free_list(&page_array[j], order); j += (1 << order); } } } uint8_t pmm_get_order (uint32_t count) { for (uint8_t i = 0 ; i <= MAX_ORDER; i++) { if (count <= (1 << i)) { return i; } } panic("pmm_get_order: too many pages requested" ); } void * pmm_alloc_pages (uint32_t count) { uint8_t order = pmm_get_order(count); for (uint8_t current_order = order; current_order <= MAX_ORDER; current_order++) { if (free_list[current_order].count > 0 ) { Page* block = fetch_from_free_list(current_order); while (current_order > order) { current_order--; Page* buddy = &page_array[block - page_array + (1 << current_order)]; buddy->order = current_order; buddy->used = 0 ; buddy->is_head = 1 ; add_to_free_list(buddy, current_order); } block->used = 1 ; block->order = order; block->next = NULL ; return (void *)((block - page_array) * PAGE_SIZE); } } return NULL ; } void * pmm_alloc_page () { return pmm_alloc_pages(1 ); } void pmm_free_page (Page* p) { if (p == NULL || p->used == 0 || p->is_head == 0 ) panic("pmm_free_page: invalid page" ); uint8_t order = p->order; p->used = 0 ; p->next = free_list[order].head; uint32_t page_idx = p - page_array; while (order < MAX_ORDER) { uint32_t buddy_idx = page_idx ^ (1 << order); Page* buddy = &page_array[buddy_idx]; if (!buddy->used && buddy->order == order) { remove_from_free_list(buddy, order); if (buddy_idx < page_idx) { p->is_head = 0 ; page_idx = buddy_idx; } else { buddy->is_head = 0 ; } order++; } else { break ; } } Page* merged_page = &page_array[page_idx]; merged_page->used = 0 ; merged_page->order = order; merged_page->is_head = 1 ; add_to_free_list(merged_page, order); } void pmm_print_info () { kernel_printf("pmm info:\n" ); for (uint32_t i = 0 ; i <= MAX_ORDER; i++){ kernel_printf("size %d (%d): " , 1 << i, free_list[i].count); uint32_t cnt = 0 ; for (Page* p = free_list[i].head; p; p = p->next){ if (cnt>=7 ) { kernel_printf("..." ); break ; } cnt++; kernel_printf("%x -> " , (p - page_array)* PAGE_SIZE); } kernel_printf("\n" ); } }
buddy判断
假设你有一个块从页号 page_idx
开始,大小为
2^order
:
1 uint32_t buddy_idx = page_idx ^ (1 << order);
这个异或操作就是计算 buddy 的位置。
例如:page_idx = 8
, order = 2
(块大小 4)
buddy_idx = 8 ^ 4 = 12
,所以 8~11
是一个块,它的 buddy 是 12~15
最后在kernel.c中添加如下内容进行测试是否可以正确维护链表。
1 2 3 4 5 6 7 8 9 10 clear_screen(); print_memory_map(); idt_install(); irq_remap(); isr_install(); irq_install(); pmm_init(MEMORY_MAP_ADDR); pmm_print_info();
这里我们通过在make run 对应的指令中加入-m
x就可以指定给虚拟机分配的内存。并把头文件的MAX_PAGES 宏设置为512MB对应的页数。然后跑起来后就能看到有很多1024的块,计算后也很接近512MB。正常工作!
简单分页机制实现
虚拟地址映射
由于我们设计的是32位的操作系统,所以这里用到的是两级页表。由于一页的大小是0x1000 ,32位下一个地址占4字节,所以一个页表下能存
\(4096/4=1024=2^{10}\)
这么多地址,如果这个页表里面用来存下一级页表的地址,就可以构成二级页表。由于我们要把一个32位的虚拟地址映射成为物理地址,我们需要索引到一个页表的项的下标,需要用掉10位,最后的12位又需要保留作为页内的偏移。所以实际上我们的物理地址可以拆分为10-bit 页目录索引
+ 10-bit 页表索引
+
12-bit 页内偏移
进行映射,而且最多只能用到二级页表。最多能映射
\(2^{20} 页 × 2^{12} 字节 = 2^{32} 字节 =
4GB\) 的内存。
实际上我们的地址翻译最终是由硬件进行实现的,在x86架构下,我们需要做的是把页目录设置好后,将其地址加载进cr3 寄存器,然后最后设置好cr0 寄存器的值,打开分页功能。这里我们需要知道的是,硬件需要事先知道页表结构分为了几级,之后才可以正确翻译地址。分页结构由
CPU 模式决定:
模式
名称
特征
页表结构
实模式
无分页
20位地址(1MB)
无页表
保护模式 (32位)
分页模式
CR0.PG=1
两级页表(10+10+12)
PAE 模式 (Physical Address Extension)
高地址分页
启用 CR4.PAE=1
三级页表
长模式 (64位)
64位分页
启用 IA-32e 模式
四级页表(9+9+9+9+12)
而页表项(Page Table
Entry)中也包含一系列的控制位,硬件最后会根据控制位来确定其行为,比如对一个只读的内存写入就会触发Page
Fault 缺页异常。32位下常见的控制位如下:
位数
名称
含义
0
P(Present)
页面是否存在
1
R/W(Read/Write)
0
= 只读,1
= 可读可写
2
U/S(User/Supervisor)
0
= 仅内核访问,1
= 用户态也可访问
3
PWT
Page-level Write-Through
4
PCD
Page-level Cache Disable
5
A(Accessed)
是否被访问过
6
D(Dirty)
是否被写过(仅在页表中)
然后就可以开始着手实现我们的分页机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #pragma once #include <stdint.h> #define PAGE_PRESENT 0x1 #define PAGE_RW 0x2 #define PAGE_USER 0x4 #define PAGE_FLAGS (PAGE_PRESENT | PAGE_RW) #define PAGE_SIZE 0x1000 #define PAGE_ENTRIES 1024 typedef uint32_t page_entry_t ; typedef struct { page_entry_t entries[PAGE_ENTRIES]; } __attribute__((aligned(PAGE_SIZE))) page_table_t ; void vmm_init () ; void map_page (uint32_t virt_addr, uint32_t phys_addr, uint32_t flags) ;
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 #include "vmm.h" #include "pmm.h" #include "string.h" #include "printf.h" #include "common.h" page_table_t * kernel_page_directory __attribute__((aligned(PAGE_SIZE)));void vmm_init () { kernel_page_directory = (page_table_t *)pmm_alloc_page(); if (kernel_page_directory == 0 ) { kernel_printf("vmm_init: failed to allocate page directory\n" ); return ; } memset (kernel_page_directory, 0 , PAGE_SIZE); page_table_t * identity_pt = (page_table_t *)pmm_alloc_page(); memset (identity_pt, 0 , PAGE_SIZE); for (uint32_t i = 0 ; i < PAGE_ENTRIES; i++) { identity_pt->entries[i] = (i * PAGE_SIZE) | PAGE_FLAGS; } kernel_page_directory->entries[0 ] = ((uint32_t )identity_pt) | PAGE_FLAGS; kernel_page_directory->entries[768 ] = ((uint32_t )identity_pt) | PAGE_FLAGS; enable_paging(kernel_page_directory); } void enable_paging (page_table_t * kernel_page_directory) { asm volatile ("mov %0, %%cr3" :: "r" (kernel_page_directory)) ; uint32_t cr0; asm volatile ("mov %%cr0, %0" : "=r" (cr0)) ; cr0 |= 0x80000000 ; asm volatile ("mov %0, %%cr0" :: "r" (cr0)) ; } void map_page (uint32_t virt_addr, uint32_t phys_addr, uint32_t flags) { uint32_t pd_index = virt_addr >> 22 ; uint32_t pt_index = (virt_addr >> 12 ) & 0x03FF ; page_table_t * page_table; if (!(kernel_page_directory->entries[pd_index] & PAGE_PRESENT)) { page_table = (page_table_t *)pmm_alloc_page(); memset (page_table, 0 , PAGE_SIZE); kernel_page_directory->entries[pd_index] = ((uint32_t )page_table) | flags | PAGE_PRESENT; } else { page_table = (page_table_t *)(kernel_page_directory->entries[pd_index] & 0xFFFFF000 ); } if (!(page_table->entries[pt_index] & PAGE_PRESENT)) { page_table->entries[pt_index] = (phys_addr & 0xFFFFF000 ) | flags | PAGE_PRESENT; } else { kernel_printf("map_page: virt_addr %x already exists\n" , virt_addr); } asm volatile ("invlpg (%0)" :: "r" (virt_addr) : "memory" ) ; } void unmap_page (page_table_t * pd, uint32_t virt_addr) { uint32_t pd_idx = (virt_addr >> 22 ) & 0x3FF ; uint32_t pt_idx = (virt_addr >> 12 ) & 0x3FF ; page_table_t * pt = (page_table_t *)(pd->entries[pd_idx] & 0xFFFFF000 ); if (!pt) return ; uint32_t entry = pt->entries[pt_idx]; if (entry & PAGE_PRESENT) { uint32_t phys_addr = entry & 0xFFFFF000 ; pt->entries[pt_idx] = 0 ; pmm_free_page(phys_addr); asm volatile ("invlpg (%0)" :: "r" (virt_addr) : "memory" ) ; } }
这里的页目录和页表所占用的页都由之前实现的pmm 来进行分配,其中初始化时我们将第一个页目录项下的所有页表进行恒等映射,也就是虚拟地址映射到相同的物理地址,这样前4MB内存建立了恒等映射后,我们刚切换成页表模式后,就不会因为执行下一个指令而立即触发缺页异常。之后我们往高地址0xC0000000 处也映射了前4MB内存,这方便我们后续跳转到高虚拟地址处执行,取消掉前面4MB的恒等映射。
这样当我们执行完vmm_init 切换页表后,仍然可以在低地址处正常执行下一个指令,而且在虚拟地址高地址处也可以正确解析到对应代码。
panic实现
为了后续我们缺页异常处理时遇到一些错误情况可以直接停止系统运行,这里先实现一个panic函数,可以打印错误信息,后续可以扩展到打印当前堆栈。
1 2 3 4 #pragma once void panic (const char * msg) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdint.h> #include "printf.h" void panic (const char * message) { asm volatile ("cli" ) ; kernel_printf("\n[!! KERNEL PANIC !!]\n" ); kernel_printf(" %s\n" , message); kernel_printf(" System halted.\n" ); while (1 ) { asm volatile ("hlt" ) ; } }
缺页异常实现
缺页异常通常发生由以下四点触发:
当发生 Page Fault 异常时,CPU
会自动把“访问失败的虚拟地址”放入
CR2
,供异常处理函数使用。
而且触发 page fault 时,CPU
会自动压栈错误码(uint32_t
),含义如下:
Bit
名称
含义
0
P(Present)
0 = 页不存在(没映射);1 = 页存在(但访问违规)
1
W/R
0 = 读引起;1 = 写引起
2
U/S
0 = CPL=0(内核);1 = CPL=3(用户)
3
RSVD
页表中保留位被错误设置(对某些 CPU 有效)
4
I/D
0 = 数据访问;1 = 指令执行(仅某些CPU支持执行权限)
首先为缺页异常(14)写一个回调函数,先从cr2读取到触发缺页异常的地址,然后从传入的寄存器状态可以得到error_code ,用来判断是由什么触发了缺页异常,进而分别进行处理。一般而言缺页异常都会发生在用户态,内核态的内存一开始就需要全部映射好。所以要进行判断触发缺页异常的地址是否在合理的用户态地址范围内,由于0xC0000000 开始的虚拟地址一般用来存内核,所以我们将范围限制在0x1000-0xC0000000 之间,低虚拟地址保留是因为处理addr为null 的情况。
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 void isr_install () { memset (interrupt_handlers, 0 , sizeof (isr_t ) * 256 ); idt_set_gate(0 , (uint32_t )isr0, 0x08 , 0x8E ); idt_set_gate(6 , (uint32_t )isr6, 0x08 , 0x8E ); idt_set_gate(13 , (uint32_t )isr13, 0x08 , 0x8E ); idt_set_gate(14 , (uint32_t )isr14, 0x08 , 0x8E ); idt_set_gate(0x20 , (uint32_t )irq0, 0x08 , 0x8E ); idt_set_gate(0x21 , (unsigned )irq1, 0x08 , 0x8E ); register_interrupt_handler(0 , divide_by_zero_handler); register_interrupt_handler(6 , invalid_opcode_handler); register_interrupt_handler(13 , general_protection_fault_handler); register_interrupt_handler(14 , page_fault_handler); } void page_fault_handler (registers_t *regs) { uint32_t faulting_address; uint32_t new_physical_page; asm volatile ("mov %%cr2, %0" : "=r" (faulting_address)) ; uint32_t err_code = regs->err_code; int present = err_code & 0x1 ; int rw = err_code & 0x2 ; int us = err_code & 0x4 ; int reserved = err_code & 0x8 ; int id = err_code & 0x10 ; kernel_printf("Page fault at 0x%x (err_code: 0x%x): " , faulting_address, err_code); if (!present) kernel_printf("page not present, " ); if (rw) kernel_printf("write, " ); else kernel_printf("read, " ); if (us) kernel_printf("user-mode, " ); else kernel_printf("kernel-mode, " ); if (reserved) kernel_printf("RSVD bit set, " ); if (id) kernel_printf("instruction fetch, " ); kernel_printf("\n" ); if (present == 1 ) { panic("Page fault: protection violation" ); } else { if (is_valid_user_address(faulting_address)) { new_physical_page = pmm_alloc_page(); kernel_printf("Allocated page at 0x%x\n" , new_physical_page); if (!new_physical_page) { panic("Out of memory" ); } map_page(faulting_address, new_physical_page, PAGE_USER | PAGE_RW); } else { panic("Segmentation Fault: invalid address" ); } } } #define USER_SPACE_START 0x00001000 #define USER_SPACE_END 0xC0000000 bool is_valid_user_address (uint32_t addr) ;bool is_valid_user_address (uint32_t addr) { return addr >= USER_SPACE_START && addr < USER_SPACE_END; }
然后这里我们在内核入口中测试一下,访问一个未被映射的地址,先读取后写入。这里我为了调试方便在page_fault_handler 中打印出了pmm分配的物理地址,然后就可以发现确实是只会触发一次缺页异常,并且写入后我们也可以在对应地址处看到写入的值。(这里还没移除前4MB的恒等映射,所以虚拟地址等于物理地址)
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 #include "idt.h" #include "isr.h" #include "irq.h" #include "printf.h" #include "screen.h" #include "info.h" #include "layout.h" #include "pmm.h" #include "vmm.h" void kernel_main (void ) { clear_screen(); print_string("Hello, Kernel!\n" ); print_memory_map(); idt_install(); irq_remap(); isr_install(); irq_install(); pmm_init(MEMORY_MAP_ADDR); vmm_init(); test_page_fault(); asm volatile ("sti" ) ; while (1 ) { asm volatile ("hlt" ) ; } } void test_page_fault () { volatile int *ptr = (int *)0x0E000000 ; int val = *ptr; *ptr = 0x666 ; }
参考资料
https://ordoflammae.github.io/littleosbook/#linking-the-kernel