Hotdry.
systems-engineering

从零构建裸机 x86_64 内核:GRUB 引导、IDT 中断、分页机制、系统调用分发与基础设备驱动

以 PatchworkOS 为例,详解 GRUB 多引导加载、IDT 中断表配置、分页内存管理、系统调用分发及 PS/2 驱动实现的关键参数与工程实践。

从零实现一个裸机 x86_64 内核是操作系统开发的经典起点,它要求开发者直接掌控硬件资源,避免依赖现有 OS 库。通过 GRUB 引导程序加载内核镜像、设置 IDT 处理中断、初始化分页机制实现虚拟内存、建立系统调用分发表以及编写基础设备驱动,可以构建一个功能完整的内核基础。PatchworkOS 项目就是一个优秀范例,它采用模块化设计、非 POSIX 接口,严格遵循 “万物皆文件” 哲学,使用 C 和汇编从头编写,支持 EEVDF 调度器和从零 ACPI 解析器。本文聚焦工程实践,给出关键参数、代码清单和监控要点,帮助读者快速上手。

GRUB 引导加载:进入保护模式与长模式切换

GRUB 是 x86_64 裸机开发的首选引导器,支持 Multiboot2 协议,能自动加载内核 ELF 文件并传递内存映射。配置 grub.cfg 文件:

menuentry 'PatchworkOS' {
    multiboot2 /boot/PatchworkOS.img
    boot
}

内核入口函数在 linker script 中定义,通常为 _start(汇编)。GRUB 加载后,CPU 处于实模式,需要立即设置 GDT(全局描述符表)切换到保护模式,再启用 PAE 并加载 CR3 进入长模式。

关键参数:

  • Multiboot2 魔数:0x36D76289,验证 GRUB 传递的 eax。
  • 栈大小:初始 16KB(0x4000),符号 KERNEL_STACK_BOTTOM。
  • 内存映射:遍历 Multiboot 标签,记录可用物理页(从 0x100000 起,避免低地址)。

工程清单:

  1. 解析 Multiboot2 结构:循环标签,提取内存图(type=6)。
  2. 设置 GDT:代码段 0x8(DPL=0,64-bit),数据段 0x10。
  3. 启用长模式:cr4 |= (1<<5) | (1<<7);pml4 [0] = pdpt | 3;cr3 = pml4_phys;eflags |= 0x2800;lgdt。
  4. 远跳:jmp 0x8:_higher_half(higher-half kernel,加载地址 0xffffffff80000000)。

PatchworkOS 使用类似流程,确保早期串口输出用于调试(COM1: 0x3f8,波特 115200)。

IDT 中断处理:异常与外部中断分发

IDT(中断描述符表)是 x86 中断核心,256 项,每项 16 字节(x86_64)。使用 lidt 加载 IDT 基址(IDTR)。

关键参数:

  • 门描述符:低 64-bit(offset0:31 + IST + type=0xE/0xF),高 64-bit(offset32:63 + DPL=3 for syscall)。
  • IST(中断栈表):4 个备用栈,syscall 用 IST1 避免内核栈溢出。
  • PIC/APIC:早期用 PIC(8259A,IRQ 0-15 映射 IDT 32-47),后期 APIC(本地 APIC IDT 入队)。

初始化清单:

  1. 清零 IDT(memset 2KB)。
  2. 设置异常门(IDT [0-31]):现存描述符,任务门可选;如 #GP(13):offset = gp_handler。
  3. 时钟中断(IDT [32]):IRQ0 -> PIT 0x40,频率 100Hz(divisor 1193182/100)。
  4. Syscall 门(IDT [0x80 或 0x300]):type=0xF,DPL=3,offset=syscall_entry;MSR 0xC0000082 = star(用户 CS=0x23,内核 CS=0x8 <<32)。
  5. lidt:limit=0xFFF,base=idt。

PatchworkOS 模块化中断:动态注册 handler,避免硬编码。监控:klog /dev/interrupts 统计中断率,阈值 >10k/s 警报。

分页机制:O (1) 物理页分配与虚拟映射

x86_64 分页用 4 级页表:PML4(512 项)-> PDPT -> PD(1GB)-> PT(2MB/4KB)。启用 5 级可选,但标准 48-bit 虚拟地址。

关键参数:

  • 页大小:4KB(PS=0),huge 2MB(PS=1)。
  • 标志:RW=1(0x2),XD=1(0x8000000000000000),Global=1(缓存命中)。
  • Buddy 分配器:order 0-10,页框链表。
  • Higher-half:内核虚拟基址 0xFFFF800000000000,物理偏移 0x10000000。

PatchworkOS 创新:元数据嵌入页表(unused bits),O (1) 查找空闲页。初始化:

  1. 身份映射前 2MB(PML4 [0][0][0][0-511] = phys | 3)。
  2. 内核映射:递归 PML4 [511] 指向自身(1:1 PT)。
  3. cr0 |= PG;cr4 |= PSE|PGE|PCIDE。
  4. 启用 NX:eflags EFER_NXE=1。

清单:early_alloc(bitmap 初始 1MB),vmm_map(va, pa, len, flags)。风险:TLB 刷新(invlpg),多核 smp_invpcid。

系统调用分发:表驱动与文件化接口

传统 syscall 用 int 0x80/sysenter/sysexit。x86_64 推荐 SYSRET:MSR SFMASK/STAR/LSTAR。

关键参数:

  • Syscall 号:0=read,1=write,2=open,... 最大 512。
  • 寄存器约定:rax=syscall#, rdi/rsi/rdx/r10/8/9=args, rcx/r11=rip/eflags。
  • 栈:用户 rsp -> 内核栈(TSS.ist1)。

分发表:syscall_table [512] = {sys_read, sys_write,...}。

PatchworkOS 独特:文件化 syscall,如 open ("/net/local/seqpacket") 创建 socket,write ("/proc/spawn") 派生进程。仍用底层表:

  1. syscall_entry:保存用户 ctx,syscall_num=rax,handler=table [num]。
  2. 若 invalid,#GP 异常。
  3. 返回:mov r10,rax;sysretq。

参数:最大 arg 6,栈对齐 16B。监控:/proc/syscalls 计数,热点 >1% CPU 优化。

基础设备驱动:PS/2 键盘 / 鼠标与 framebuffer

驱动模块化:ACPI 解析 _CRS 获取 IRQ/IO 端口,避免硬码。

PS/2 示例:

  • IO:data 0x60,cmd 0x64。
  • IRQ:kbd=1,aux=12。
  • 初始化:write_cmd (0xA8 双通道),0xDD 禁用 aux,0xF4 启用 kbd。

代码:

wait_write: in al,0x64; test al,2; jnz wait_write; ret
kbd_irq: cli; in al,0x60; push rax; ; queue; sti; iretq

Framebuffer:VESA 模式 0x4118(1024x768x32),LFB 0xFD000000。

PatchworkOS PS/2 模块:ACPI _HID="PNP0303",动态资源分配。

清单:

  1. IRQ 路由:ioapic 重映射。
  2. DMA:页对齐缓冲。
  3. 超时:500us poll。

回滚:fallback 硬码端口。

工程化要点与监控

  • 调试:QEMU -smp 4 -enable-kvm,GDB remote。
  • 性能:EEVDF vruntime <1us,页故障 <10us。
  • 安全:SMEP/SMAP,KASLR(随机 slide 1MB)。
  • 测试:ACPICA AML 套件,fuzz IRQ。

构建 PatchworkOS:git clone, make all run。内核~500KB,支持 SMP、多线程。

资料来源:

(正文约 1250 字)

查看归档