You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C语言实现的QEMU键盘中断处理器仅响应一次,求解决方案

解决C语言+QEMU键盘中断处理器仅工作一次的问题

根据你提供的代码和现象,我梳理出几个核心问题,逐一给出修复方案:

核心问题分析

  1. IDT存储在栈上(局部变量):setup_idt()里的IDT[256]是栈上的局部变量,函数返回后栈空间被释放,IDT的内容会被后续操作覆盖,导致第一次中断后中断门地址失效。
  2. 中断处理函数不符合调用约定:普通C函数的栈帧和中断发生时CPU自动压入的栈帧不匹配,手动添加iret会导致栈不平衡,引发系统崩溃(表现为QEMU乱码、清屏)。
  3. PIC初始化参数错误:主PIC的ICW3配置错误,会影响PIC的级联功能(虽然暂时不影响键盘IRQ1,但整体配置不规范)。
  4. 键盘扫描码处理不严谨:没有区分按键按下(通码)和释放(断码),会错误处理释放事件。

具体修复步骤

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,提问作者Гоша Обыночный

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 09:07:01