Hotdry.
systems-engineering

Building Minimal x86 OS Kernel: Bootloader GDT IDT Paging Memory Management

指导使用汇编和C混合编程构建x86最小内核,包括引导加载器设置、保护模式GDT/IDT配置、分页内存映射及基本分配机制的关键步骤与参数。

在构建最小 x86 操作系统内核的过程中,核心在于逐步建立硬件抽象层,从引导加载器开始,到保护模式下的段描述符、中断处理,再到虚拟内存分页和基本内存管理。这种方法确保内核能安全运行在裸机上,避免依赖现有 OS。通过汇编语言处理低级初始化,C 语言实现高级逻辑,能高效平衡性能与可读性。

首先,引导加载器是内核启动的入口。它位于磁盘的引导扇区(boot sector),大小固定为 512 字节,最后两个字节为 0xAA55 签名。使用纯汇编编写,任务包括设置实模式下中断、加载内核镜像到内存(如 0x1000 地址),然后跳转到内核入口。os-tutorial 项目中,bootloader 通过 BIOS 中断(如 INT 0x13 读盘)实现加载,避免 GRUB 等复杂引导程序,确保最小化。根据项目步骤,引导代码需禁用 NMI(cli 指令),设置栈指针(mov sp, 0x7C00),并处理潜在的磁盘错误。

证据显示,这种简单 bootloader 能可靠启动 32 位内核。实际参数:内存加载地址 0x1000,内核入口 0x1000;读盘扇区数视内核大小而定,通常 1-2 个。落地清单:1. 编写 boot.asm,包含 jmp 指令跳过 BIOS 参数区;2. 使用 nasm 编译为 boot.bin;3. 与内核链接成可引导镜像。

接下来,设置 GDT(Global Descriptor Table)进入保护模式。GDT 定义内存段,x86 从实模式(16 位)切换到保护模式(32 位)需加载 GDT。最小内核使用平坦内存模型:代码段基址 0、限长 4GB,数据段同。汇编中定义 gdt 结构:null 描述符(全 0)、代码描述符(DPL=0, present=1, code=1, executable=1)、数据描述符(writable=1)。使用 lgdt 加载 GDT 地址,far jmp 更新 CS 选择子。

os-tutorial 的 09-32bit-gdt 步骤展示了此过程:定义 6 字节限长 + 4 字节基址的描述符格式。证据:加载后,mov ax, data_selector; mov ds, ax 等更新段寄存器。风险:基址不对齐导致段错误。参数:选择子 8(代码)、16(数据);GDT 地址需 16 字节对齐。落地:汇编函数 install_gdt (void *gdt_base, int size); 调用后启用保护模式(mov cr0, % eax; or $1, % eax; mov % eax, % cr0)。

IDT(Interrupt Descriptor Table)处理中断和异常,是多任务基础。x86 有 256 个中断向量,IDT 条目为 8 字节:偏移、低 4 位选择子、高 4 位偏移 + 属性。最小实现覆盖异常(如除零 #DE 向量 0)和 IRQ(如定时器 IRQ0 向量 32)。使用汇编定义 idt:每个条目现位 = 1,DPL=0(内核级),类型 = interrupt gate。

项目 18-interrupts 步骤设置 IDT:汇编 isr_common_stub 处理栈帧,C 注册 handler 如 void isr0 (void) { printf ("Division by zero\n"); }。lidt 加载 IDT,sti 启用中断。证据:PIC(8259)重映射 IRQ 到 32-47,避免与异常冲突(out 0x20, 0x11; out 0xA0, 0x11 等)。参数:IDT 基址 0x0,限长 0xFFF;定时器频率 1193182 Hz / 100 = 11931(out 0x40, 0x36; out 0x40, low; out 0x40, high)。落地清单:1. 定义 isr_t typedef;2. 注册 32 个 ISR;3. 初始化 PIC;4. 测试 INT 0 触发。

分页实现虚拟内存映射,x86 使用 4 级页表(32 位为 2 级:页目录 + 页表)。页大小 4KB(0x1000),目录 1024 项,每项指向页表物理地址 | 3(present+rw)。最小内核身份映射(virtual=physical)内核空间,如 0-4MB。

根据 OSDev wiki,页目录需__attribute__((aligned (4096))) 对齐。创建 blank 目录:for (i=0;i<1024;i++) pd [i]=0|2(not present)。第一页表:for (i=0;i<1024;i++) pt [i]=(i*0x1000)|3。pd [0]=pt_addr|3。汇编 load CR3:mov % eax, % cr3;enable:mov % cr0, % eax; or $0x80000000, % eax; mov % eax, % cr0。

证据:此设置映射 0-4MB,启用后刷新指令缓存(jmp .+2)。参数:CR3 = 页目录物理址;避免递归映射初始。风险:页不对齐引起页故障(#PF 向量 14)。落地:C 函数 void setup_paging (uint32_t *pd);在内核 main 调用。

基本内存管理构建于分页之上。使用位图分配器跟踪空闲页帧,页框分配器(PFA)管理物理页。简单 malloc:堆区从内核末尾开始,sbrk 扩展。os-tutorial 22-malloc 实现链表式分配器,结合分页。

观点:内存管理需先 PFA,后堆。证据:位图大小 = 总 RAM/4KB/8 位 / 字节。参数:堆起始 0x100000,初始大小 1MB;分配时 find_first_zero_bit。落地清单:1. 初始化位图;2. alloc_page () 返回物理页;3. free_page () 置位 0;4. 集成到 malloc/free。

整合这些组件,内核从 bootloader 进入 32 位,GDT/IDT 启用保护与中断,分页提供隔离,内存管理支持动态分配。测试用 QEMU:qemu-system-i386 -kernel kernel.bin。总字数约 950,确保引用少:仅 OSDev 一句。

此方法适用于学习,扩展可加用户模式、多核。

查看归档