在x86保护模式下的键盘中断引起了处理器错误。

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

最终我意识到,当它从 iso 引导时,它会先运行 GRUB 引导加载程序,然后再引导内核本身。我发现 GRUB 可能会将处理器切换到保护模式,这就引起了问题

问题: 通常情况下,我只需初始化中断处理程序,按下键后就会被处理。 然而,当我使用 iso 运行我的内核并按下键时,虚拟机会崩溃。 这在 qemu 和 VMWare 中都发生过,因此我认为我的中断肯定有问题。

请注意,只要我不使用 GRUB,代码就能正常工作。 interrupts_init() (参见下文)是内核函数 main() 中调用的第一件事情之一。

基本问题是:是否有方法使其在保护模式下运行?
我的整个内核的完整副本可以在GitHub存储库中找到。 一些相关文件:

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:

是的,当启动符合multiboot标准的内核时,GRUB将切换到保护模式。如果您重新加载任何选择器寄存器(CS、DS、ES)与符合multiboot标准的内核,则需要确保您自己设置GDT。如果您不更改它们,则可以正常运行。 - Michael Petch
如果你的代码和我的答案一样,那么我怀疑我知道可能出了什么问题,甚至可能与我关于GDT的最后评论有关。在Multiboot规范中,Multiboot文档中有这个重要的注释:“GDTR”即使段寄存器按上述描述设置,‘GDTR’可能无效,因此操作系统映像必须不加载任何段寄存器(即使重新加载相同的值!)直到它设置自己的“GDT”。当IRQ触发时,CS选择器被加载可能会导致失败的可能性。坏的GDTR可能会导致失败。 - Michael Petch
这里是凌晨1:30。我成功地复现了你可能遇到的问题。我的最佳猜测是,在设置IDT之前,你需要手动设置自己的GDT。看起来_QEMU_多引导加载器(与-kernel一起使用)对_GDTR/GDT_更宽容。我成功地插入了一个_GDT_,并且似乎解决了这个问题。今天晚些时候,我会在另一个链接中更新我的答案。 - Michael Petch
@MichaelPetch 这是那个答案中的代码,我忘了提到它。 - natiiix
我在你的Github仓库中向你发送了一个pull request,其中包含一个更改,希望解决你的问题。它设置了一个基本的4GB平面描述符的GDT,并从你的kernel_main函数中调用了load_gdt函数。 - Michael Petch
显示剩余3条评论
1个回答

5

昨晚我有个想法,关于为什么通过GRUB加载和通过QEMU的Multiboot -kernel功能加载可能不如预期。这在注释中有所体现。根据OP发布的更多源代码,我已经确认了这些发现。

Mulitboot规范中,有一条关于修改选择器的GDTRGDT的注释是相关的:

GDTR

即使如上所述设置了段寄存器,“GDTR”也可能无效,因此操作系统映像必须不加载任何段寄存器(即使重新加载相同的值!)直到它设置自己的“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. 
虽然它说明了将设置哪些描述符,但实际上并没有指定描述符必须具有特定的索引。一个Multiboot加载程序可能在索引0x08处拥有一个代码段描述符,而另一个bootloader可能使用0x10。当您查看代码的一行时,这非常相关: 这会为中断0x21创建一个IDT描述符。第三个参数0x08是CPU需要使用的代码选择器来访问中断处理程序。我发现在QEMU上可以工作,其中代码选择器为0x08,但在GRUB中似乎是0x10。在GRUB中,0x10选择器指向一个不可执行的数据段,这将无法正常工作。
为了避免所有这些问题,最好的方法是在启动内核之后、设置IDT并启用中断之前,建立自己的GDT。如果您想要更多信息,可以在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。该GDT在0x00处具有空描述符,在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!");
}

非常感谢。您的回答来得出乎意料地快,而且完美地解释了问题。然而,我注意到了一个奇怪的现象。在QEMU和VMWare中,当启动您修改过的代码时,VGA光标消失了。我已经确认,在“-kernel”模式下,QEMU不会发生这种情况。 - natiiix
当我有更多时间时,我需要稍后查看光标问题。很可能我的代码不是原因,而是与GRUB如何设置图形模式相关的问题(在我有时间查看之前,我只是猜测)。 - Michael Petch
@user3043260:在我找到一个好的解决方案之前,你可以通过修改grub.cfg文件中的timeout值从0改为1来显示光标。这是一个快速的hack。 - Michael Petch
我最近才添加了这个,所以你认为这可能是问题吗?这可以解释为什么我肯定没有注意到。谢谢你的时间,伙计。我非常感激! - natiiix
这个hack实际上让菜单显示出来(超时为0不会使其出现)。通过让菜单出现,GRUB启用了光标并将光标设置为下划线类型字符。正确修复此问题的方法是通过视频硬件端口启用光标(在内核终端初始化中),然后告诉它在字符中哪些扫描行上光标将出现。明天我有机会时会发布一些代码。通过GRUB时出现没有光标的问题。 - Michael Petch
显示剩余22条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接