C语言实现的QEMU键盘中断处理器仅响应一次,求解决方案
解决C语言+QEMU键盘中断处理器仅工作一次的问题
根据你提供的代码和现象,我梳理出几个核心问题,逐一给出修复方案:
核心问题分析
- IDT存储在栈上(局部变量):
setup_idt()里的IDT[256]是栈上的局部变量,函数返回后栈空间被释放,IDT的内容会被后续操作覆盖,导致第一次中断后中断门地址失效。 - 中断处理函数不符合调用约定:普通C函数的栈帧和中断发生时CPU自动压入的栈帧不匹配,手动添加
iret会导致栈不平衡,引发系统崩溃(表现为QEMU乱码、清屏)。 - PIC初始化参数错误:主PIC的ICW3配置错误,会影响PIC的级联功能(虽然暂时不影响键盘IRQ1,但整体配置不规范)。
- 键盘扫描码处理不严谨:没有区分按键按下(通码)和释放(断码),会错误处理释放事件。
具体修复步骤
1. 将IDT改为全局/静态变量
把IDT从局部变量移到全局作用域,避免函数返回后内存被回收:
// 全局IDT,确保内存不会被栈回收 struct IDT_entry IDT[256]; void setup_idt() { unsigned long keyboard_address; unsigned long idt_address; unsigned long idt_ptr[2]; // 初始化键盘中断门 keyboard_address = (unsigned long)keyboard_handler; IDT[0x21].offset_lowerbits = keyboard_address & 0xffff; IDT[0x21].selector = 0x8; // 需与你的GDT代码段选择子一致 IDT[0x21].zero = 0; IDT[0x21].type_attr = 0x8e; // 32位中断门,特权级0,存在 IDT[0x21].offset_higherbits = (keyboard_address & 0xffff0000) >> 16; // 修正PIC初始化参数 outb(0x20, 0x11); // ICW1:边沿触发,级联模式,需要ICW4 outb(0xA0, 0x11); outb(0x21, 0x20); // ICW2:主PIC中断号起始0x20 outb(0xA1, 0x28); // 从PIC中断号起始0x28 outb(0x21, 0x04); // ICW3:主PIC的IRQ2连接从PIC outb(0xA1, 0x02); // ICW3:从PIC连接到主PIC的IRQ2 outb(0x21, 0x01); // ICW4:8086模式,手动EOI outb(0xA1, 0x01); // 仅启用IRQ1(键盘) outb(0x21, 0xFD); // 0xFD = 11111101,关闭其他主PIC中断 outb(0xA1, 0xFF); // 关闭所有从PIC中断 // 加载IDT idt_address = (unsigned long)IDT; idt_ptr[0] = (sizeof(struct IDT_entry) * 256) + ((idt_address & 0xffff) << 16); idt_ptr[1] = idt_address >> 16; __asm__ __volatile__("lidt %0" :: "m"(*idt_ptr)); __asm__ __volatile__("sti"); // 开启全局中断 }
2. 修正中断处理函数的调用约定
使用GCC的__attribute__((interrupt))属性,让编译器自动生成正确的栈帧处理和iret指令,无需手动添加:
unsigned int location = 0; char* vga = (char*)0xB8000; // 标准PC键盘扫描码映射表(仅包含常用按键) unsigned char keyboard_map[] = { 0, 27, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b', '\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n', 0, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`', 0, '\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 0, '*', 0, ' ', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '-', 0, 0, 0, '+', 0, 0, 0, 0, 0, 0, 0, 0 }; // 声明为中断函数,编译器自动处理栈和iret void __attribute__((interrupt)) keyboard_handler() { unsigned char status = inb(0x64); // 仅处理按键按下事件(忽略断码) if ((status & 0x01) != 0) { unsigned char scancode = inb(0x60); if (!(scancode & 0x80)) { // 排除释放事件(断码最高位为1) vga[location] = keyboard_map[scancode]; vga[location + 1] = 0x04; // 红色前景,黑色背景 location += 2; // 防止超出VGA缓冲区(80*25字符,每个占2字节) if (location >= 80*25*2) { location = 0; } } } // 发送中断结束信号(EOI)给主PIC outb(0x20, 0x20); }
3. 验证GDT配置(关键前提)
确保你的GDT中代码段选择子0x8是有效的,代码段需设置为可执行、特权级0,示例GDT初始化:
struct gdt_entry { unsigned short limit_low; unsigned short base_low; unsigned char base_middle; unsigned char access; unsigned char granularity; unsigned char base_high; }; struct gdt_entry gdt[3]; struct gdt_ptr gp; void setup_gdt() { gp.limit = (sizeof(struct gdt_entry) * 3) - 1; gp.base = (unsigned long)&gdt; // 空描述符 gdt[0].limit_low = 0; gdt[0].base_low = 0; gdt[0].base_middle = 0; gdt[0].access = 0; gdt[0].granularity = 0; gdt[0].base_high = 0; // 代码段:选择子0x8,特权级0,可执行可读 gdt[1].limit_low = 0xFFFF; gdt[1].base_low = 0; gdt[1].base_middle = 0; gdt[1].access = 0x9A; gdt[1].granularity = 0xCF; gdt[1].base_high = 0; // 数据段:选择子0x10,特权级0,可读写 gdt[2].limit_low = 0xFFFF; gdt[2].base_low = 0; gdt[2].base_middle = 0; gdt[2].access = 0x92; gdt[2].granularity = 0xCF; gdt[2].base_high = 0; __asm__ __volatile__("lgdt %0" :: "m"(gp)); __asm__ __volatile__( "mov $0x10, %%ax\n" "mov %%ax, %%ds\n" "mov %%ax, %%es\n" "mov %%ax, %%fs\n" "mov %%ax, %%gs\n" "mov %%ax, %%ss\n" "jmp $0x8, $flush\n" "flush:\n" ); }
注意:必须在调用setup_idt()前初始化GDT,否则代码段选择子0x8无效。
总结
最致命的问题是IDT存储在栈上,导致函数返回后中断门地址失效;其次是中断处理函数未遵循中断调用约定,引发栈不平衡。修正这两个核心问题后,再调整PIC参数和扫描码处理,键盘中断就能正常工作了。
内容的提问来源于stack exchange,提问作者Гоша Обыночный
相关产品推荐
相关产品推荐

