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

问题描述 投票:2回答:1

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

最后我意识到,当它从iso引导时,它还会在引导内核之前运行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:
c x86 interrupt osdev multiboot
1个回答
4
投票

我昨晚有一个预感,为什么通过GRUB加载并通过QEMU的Multiboot -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!");
}
© www.soinside.com 2019 - 2024. All rights reserved.