x86保护模式键盘中断导致处理器错误

Keyboard interrupt in x86 protected mode causes processor error

我正在开发一个简单的内核,我一直在尝试实现一个键盘中断处理程序来摆脱端口轮询。我一直在 -kernel 模式下使用 QEMU(以减少编译时间,因为使用 grub-mkrescue 生成 iso 需要相当长的时间)并且它工作得很好,但是当我想切换到 -cdrom 模式突然开始崩溃。我不知道为什么。

最终我意识到,当它从 iso 引导时,它在引导内核本身之前还 运行s 一个 GRUB 引导加载程序。我发现 GRUB 可能会将处理器切换到保护模式,这会导致问题

问题: 通常我会简单地初始化中断处理程序,每当我按下一个键时,它就会被处理。然而,当我 运行 我的内核使用 iso 并按下一个键时,虚拟机就崩溃了。这在 qemu 和 VMWare 中都发生了,所以我认为我的中断一定有问题。

请记住,只要我不使用 GRUB,代码就可以正常工作。 interrupts_init()(见下文)是 main() 内核函数中最先调用的东西之一。

本质上的问题是:有没有办法让它在保护模式下工作?

我的内核的完整副本可以在我的 GitHub repository 中找到。一些相关文件:

lowlevel.asm:

section .text

global keyboard_handler_int
global load_idt

extern keyboard_handler

keyboard_handler_int:
    pushad
    cld
    call keyboard_handler
    popad
    iretd

load_idt:
    mov edx, [esp + 4]
    lidt [edx]
    sti
    ret

interrupts.c:

#include <assembly.h> // defines inb() and outb()

#define IDT_SIZE 256
#define PIC_1_CTRL 0x20
#define PIC_2_CTRL 0xA0
#define PIC_1_DATA 0x21
#define PIC_2_DATA 0xA1

extern void keyboard_handler_int(void);
extern void load_idt(void*);

struct idt_entry
{
    unsigned short int offset_lowerbits;
    unsigned short int selector;
    unsigned char zero;
    unsigned char flags;
    unsigned short int offset_higherbits;
} __attribute__((packed));

struct idt_pointer
{
    unsigned short limit;
    unsigned int base;
} __attribute__((packed));

struct idt_entry idt_table[IDT_SIZE];
struct idt_pointer idt_ptr;

void load_idt_entry(int isr_number, unsigned long base, short int selector, unsigned char flags)
{
    idt_table[isr_number].offset_lowerbits = base & 0xFFFF;
    idt_table[isr_number].offset_higherbits = (base >> 16) & 0xFFFF;
    idt_table[isr_number].selector = selector;
    idt_table[isr_number].flags = flags;
    idt_table[isr_number].zero = 0;
}

static void initialize_idt_pointer()
{
    idt_ptr.limit = (sizeof(struct idt_entry) * IDT_SIZE) - 1;
    idt_ptr.base = (unsigned int)&idt_table;
}

static void initialize_pic()
{
    /* ICW1 - begin initialization */
    outb(PIC_1_CTRL, 0x11);
    outb(PIC_2_CTRL, 0x11);

    /* ICW2 - remap offset address of idt_table */
    /*
    * In x86 protected mode, we have to remap the PICs beyond 0x20 because
    * Intel have designated the first 32 interrupts as "reserved" for cpu exceptions
    */
    outb(PIC_1_DATA, 0x20);
    outb(PIC_2_DATA, 0x28);

    /* ICW3 - setup cascading */
    outb(PIC_1_DATA, 0x00);
    outb(PIC_2_DATA, 0x00);

    /* ICW4 - environment info */
    outb(PIC_1_DATA, 0x01);
    outb(PIC_2_DATA, 0x01);
    /* Initialization finished */

    /* mask interrupts */
    outb(0x21 , 0xFF);
    outb(0xA1 , 0xFF);
}

void idt_init(void)
{
    initialize_pic();
    initialize_idt_pointer();
    load_idt(&idt_ptr);
}

void interrupts_init(void)
{
    idt_init();
    load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8E);

    /* 0xFD is 11111101 - enables only IRQ1 (keyboard)*/
    outb(0x21 , 0xFD);
}

kernel.c

#if defined(__linux__)
    #error "You are not using a cross-compiler, you will most certainly run into trouble!"
#endif

#if !defined(__i386__)
    #error "This kernel needs to be compiled with a ix86-elf compiler!"
#endif

#include <kernel.h>

// These _init() functions are not in their respective headers because
// they're supposed to be never called from anywhere else than from here

void term_init(void);
void mem_init(void);
void dev_init(void);

void interrupts_init(void);
void shell_init(void);

void kernel_main(void)
{
    // Initialize basic components
    term_init();
    mem_init();
    dev_init();
    interrupts_init();

    // Start the Shell module
    shell_init();

    // This should be unreachable code
    kernel_panic("End of kernel reached!");
}

boot.asm:

bits 32
section .text
;grub bootloader header
        align 4
        dd 0x1BADB002            ;magic
        dd 0x00                  ;flags
        dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero

global start
extern kernel_main

start:
  mov esp, stack_space  ;set stack pointer
  call kernel_main

; We shouldn't get to here, but just in case do an infinite loop
endloop:
  hlt           ;halt the CPU
  jmp endloop

section .bss
resb 8192       ;8KB for stack
stack_space:

我昨晚有预感,为什么通过 GRUB 加载和通过 QEMU 的多重引导 -kernel 功能加载可能无法按预期工作。这是在评论中捕获的。我已经根据 OP 发布的更多源代码设法确认了这些发现。

Mulitboot Specification 中有一个关于 GDTRGDT 的注释,关于修改相关的选择器:

GDTR

Even though the segment registers are set up as described above, the ‘GDTR’ may be invalid, so the OS image must not load any segment registers (even just reloading the same values!) until it sets up its own ‘GDT’.

中断例程可能会改变 CS 选择器,从而导致问题。

还有另一个问题,很可能是问题的根本原因。 Multiboot 规范还说明了它在 GDT:

中创建的选择器
‘CS’
Must be a 32-bit read/execute code segment with an offset of ‘0’ and a
limit of ‘0xFFFFFFFF’. The exact value is undefined. 
‘DS’
‘ES’
‘FS’
‘GS’
‘SS’
Must be a 32-bit read/write data segment with an offset of ‘0’ and a limit
of ‘0xFFFFFFFF’. The exact values are all undefined. 

虽然它说明了将设置什么类型的描述符,但实际上并未指定描述符必须具有特定索引。一个多重引导加载程序可能在索引 0x08 处有一个代码段描述符,而另一个引导加载程序可能使用 0x10。当您查看一行代码时,这一点尤为重要:

load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8E);

这为中断 0x21 创建了一个 IDT 描述符。第三个参数 0x08 是 CPU 需要用来访问中断处理程序的代码选择器。我发现这适用于 QEMU,其中代码选择器是 0x08,但在 GRUB 中它似乎是 0x10。在 GRUB 中,0x10 选择器指向不可执行的数据段,这将不起作用。

要解决所有这些问题,最好的办法是在启动内核之后和设置 IDT[ 之前设置自己的 GDT =63=] 并启用中断。如果您想了解更多信息,OSDev Wiki 中有关于 GDT 的教程。

要设置 GDT 我将在 lowlevel.asm 中简单地创建一个汇编例程,通过添加 load_gdt 函数和数据结构来完成:

global load_gdt

; GDT with a NULL Descriptor, a 32-Bit code Descriptor
; and a 32-bit Data Descriptor
gdt_start:
gdt_null:
    dd 0x0
    dd 0x0

gdt_code:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10011010b
    db 11001111b
    db 0x0

gdt_data:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10010010b
    db 11001111b
    db 0x0
gdt_end:

; GDT descriptor record
gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start

CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

; Load GDT and set selectors for a flat memory model
load_gdt:
    lgdt [gdt_descriptor]
    jmp CODE_SEG:.setcs              ; Set CS selector with far JMP
.setcs:
    mov eax, DATA_SEG                ; Set the Data selectors to defaults
    mov ds, eax
    mov es, eax
    mov fs, eax
    mov gs, eax
    mov ss, eax
    ret

这将创建并加载一个 GDT,它在索引 0x00 处有一个 NULL 描述符,在 0x08 处有一个 32 位代码描述符,在 0x10 处有一个 32 位数据描述符。由于我们使用 0x08 作为代码选择器,这与您在中断 0x21:

IDT 条目初始化中指定的代码选择器相匹配

load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8E);

唯一的另一件事是您需要修改 kernel.c 才能调用 load_gdt。可以用类似的东西来做到这一点:

extern void load_gdt(void);

void kernel_main(void)
{
    // Initialize basic components
    load_gdt();
    term_init();
    mem_init();
    dev_init();
    interrupts_init();

    // Start the Shell module
    shell_init();

    // This should be unreachable code
    kernel_panic("End of kernel reached!");
}