从GRUB 0.97切换至GRUB 2时内核触发GPF异常的问题排查
我最近在把操作系统的启动从GRUB 0.97(stage2_eltorito)切换到GRUB 2,生成可启动ISO时遇到了棘手的问题:内核在GRUB Legacy下运行完全正常,键盘输入、定时器都没问题,但用GRUB 2启动就会触发**General Protection Fault(GPF)**直接停机。我反复检查代码都没找到问题,想问问这到底是哪儿出了问题,该怎么修复?
补充背景:我给内核加了异常处理ISR,还有两个IRQ——一个处理键盘输入,一个处理定时器计时。另外用kvm -kernel启动内核是正常的,所以我觉得不是Multiboot加载的问题,因为两种GRUB都能成功加载内核。
相关代码展示
内核汇编入口点(带Multiboot头)
extern toc extern init extern kmain extern load_gdt global start32 MBALIGN equ 1<<0 MEMINFO equ 1<<1 MAGIC_NUMBER equ 0x1BADB002 FLAGS equ MBALIGN | MEMINFO CHECKSUM equ -(MAGIC_NUMBER + FLAGS) section .text align 4 dd MAGIC_NUMBER dd FLAGS dd CHECKSUM start32: call load_gdt mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov esp, 0x9c000 call init push ebx call kmain
load_gdt及GDT定义
global toc global load_gdt section .text load_gdt: lgdt[toc] ret section .rodata gdt32: dd 0 dd 0 dw 0x0FFFF dw 0 db 0 db 0x9A db 0xCF db 0 dw 0x0FFFF dw 0 db 0 db 0x92 db 0xCF db 0 gdt_end: toc: dw gdt_end - gdt32 - 1 dd gdt32
系统初始化函数init()
#include <kernel.h> void init() { interrupt_disable(); terminal_init(LGREEN, BLACK); idt_install(); isr_install(); irq_install(); interrupt_enable(); timer_install(); keyboard_install(); return; }
主函数kmain()
#include <kernel.h> #include <multiboot.h> char logo[1024] = { ":::::::::::///oyhhyo//:::::::::::/:\n" \ "://////////+yNNmhysoo+////////////:\n" \ "::/://:://yNdy+//::::::+o/:://////:\n" \ "::////://sNdy+//::-:-:::++/://////:\n" \ "::::::://dmdyo/:::::--::++/::::::::\n" \ ":::::::/+mdhhysoo+:/++//+y/::::::::\n" \ ":::::::/oddyooo+/o//+so+/o/::::::::\n" \ "+ = = + + + +++++ Welcome to LunaOS!\n" \ "+ = = + + + +===+ A simple 32-bit operating system\n" \ "+ = = + + + + + Written soley by Safal Aryal\n" \ "++++++ = = = = + + + + All components of this OS,\n" \ ":::::::/+hhyo///+s///::/+//:::::::: are in the public domain!\n" \ "::::::://oshs+/+hdhs+/////::::::::: (excluding the GRUB bootloader)\n" \ "::::::::/++so+shdyhys+////::::::::: Type `help` for a list of commands\n" \ "::::::::/+o++oosso++/+/:/::::::::::\n" \ ":::::::/ossssysoso+//+/:/::::::::::\n" \ ":::://ohysosysso+oo/:///:::::::::::\n" \ "::::--:/ssyoshhsoo+/::+:/-:::::::::\n" \ ":::-----:+shysdyo/-.`-/::--:----:::\n" \ "--::------:+sssyo/.`.:/:----::-----\n" \ "-::/:--::-::---//////--------::----\n" \ }; void kmain(multiboot_info_t *mbd, uint32_t magic) { uint32_t mmap; if((mbd->flags >> 6) & 1) { mmap = mbd->mmap_addr; } terminal_puts(logo); terminal_puts("SHELL> "); for(;;){asm volatile("sti"); asm volatile ("hlt");}; }
问题根源分析
GRUB 2和GRUB Legacy在启动后对硬件状态的初始化有明显差异,最可能的问题出在中断控制器(PIC)的状态上:
- GRUB Legacy会主动将PIC设置为兼容模式(比如屏蔽所有IRQ或设置默认中断向量偏移),但GRUB 2可能会保留BIOS初始化的PIC状态,甚至没有预先屏蔽IRQ。
- 你的
irq_install()负责重新映射PIC,但如果GRUB 2没有先屏蔽IRQ,在重新映射过程中极易触发未处理的中断,进而导致GPF。 - 另外,GDT加载的完整性也可能有问题:虽然你已经重新加载了段寄存器,但CS寄存器无法通过
mov指令直接修改,可能存在刷新不彻底的情况。
修复步骤
初始化PIC前强制屏蔽所有IRQ
在irq_install()函数最开头,先执行以下代码屏蔽主从PIC的所有中断,避免重新映射时出现意外触发:outb(0x21, 0xFF); // 屏蔽主PIC所有IRQ outb(0xA1, 0xFF); // 屏蔽从PIC所有IRQ完善GDT加载逻辑
修改load_gdt函数,通过远跳转强制刷新CS寄存器(CS无法直接用mov修改),同时把段寄存器加载逻辑整合到这里,避免重复操作:load_gdt: lgdt [toc] jmp 0x08:.flush ; 0x08是GDT中代码段的选择子 .flush: mov ax, 0x10 ; 0x10是GDT中数据段的选择子 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax ret记得删除
start32中原有的段寄存器加载代码。验证IRQ向量偏移的正确性
确保你把IRQ0-15映射到了和CPU异常向量不重叠的位置(比如从0x20开始),如果向量重叠,触发IRQ时会被当成未处理的CPU异常,直接导致GPF。检查内存映射的访问边界
GRUB 2提供的内存映射可能和GRUB Legacy不同,访问mbd->mmap_addr时一定要结合mbd->mmap_length判断边界,避免越界访问内存引发GPF。
额外测试建议
- 暂时注释掉
irq_install()、keyboard_install()和timer_install(),用GRUB 2启动测试,如果不再触发GPF,就能确定问题出在IRQ/PIC相关逻辑上。 - 给异常处理添加详细日志,打印GPF触发时的错误码、EIP寄存器值,精准定位出错的代码位置。
内容的提问来源于stack exchange,提问作者Safal Aryal

