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

时间:2017-04-02 01:01:41

标签: c x86 interrupt osdev multiboot

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

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

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

请记住,只要我不使用GRUB,代码就可以正常工作。 -cdrom(见下文)是interrupts_init()内核函数中调用的第一个内容之一。

基本上问题是:有没有办法让这项工作处于保护模式?

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

main()

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

1 个答案:

答案 0 :(得分:4)

我昨晚有一个预感,为什么通过GRUB加载并通过 QEMU 的多重引导-kernel功能加载可能无法按预期工作。这在评论中被捕获。我已经设法根据OP发布的更多源代码来确认调查结果。

Mulitboot Specification中,有关于修改相关选择器的 GDTR GDT 的说明:

  

<强> GDTR

     

即使段寄存器如上所述设置,'GDTR'可能无效,因此OS映像不能加载任何段寄存器(甚至只是重新加载相同的值!),直到它设置自己的'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选择器指向不可执行的数据段,这不起作用。

要解决所有这些问题,最好的办法是在启动内核后立即设置自己的 GDT ,然后再设置 IDT 并启用中断。如果您想了解更多信息,可以在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作为代码选择器,因此它与您在 IDT 条目初始化中为中断0x21指定的代码选择器相匹配:

  

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!");
}