【问题标题】:Keyboard interrupt in x86 protected mode causes processor errorx86 保护模式下的键盘中断导致处理器错误
【发布时间】:2017-04-02 01:01:41
【问题描述】:

我正在开发一个简单的内核,我一直在尝试实现一个键盘中断处理程序来摆脱端口轮询。我一直在-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:

【问题讨论】:

  • 我对你有感觉。这很可能是您自己的软件中的错误。问题是您显示的代码并没有真正使它成为一个最小的完整可验证示例。几乎不可能说(我们甚至看不到键盘处理程序,您如何设置 GDT/LIDT 等)。我希望能够提供帮助。例如,如果你在 Git 存储库中有这个,我就可以对其进行测试,看看我能找到什么。我假设有太多代码无法在一个问题中发布所有内容?
  • 在我搜索本地文件并发现看起来非常熟悉的代码之前,您知道这段代码看起来有些熟悉。您是否尝试过此答案中的代码:stackoverflow.com/a/37635449/3857942。您的文件名似乎与我去年回答的问题中的文件名非常相似。我的答案包括最小的完整文件集。我想知道当您从 ISO 启动时该代码是否有效
  • 是的,GRUB 将在引导 multibbot 兼容内核时切换到保护模式。如果您曾经使用兼容多重引导的内核重新加载任何选择器寄存器(CS、DS、ES),您需要确保您也设置了自己的 GDT。如果您不更改它们,那么您就可以了。
  • 如果您的代码与我的答案类似,那么我怀疑我知道可能出了什么问题,甚至可能与我对 GDT 的最后评论有关。在 Multiboot 规范中,Multiboot docs 'GDTR' 中有这个重要说明即使段寄存器如上所述设置,'GDTR' 可能无效,因此 OS 映像不得加载任何段寄存器(甚至只是重新加载相同的值!),直到它设置自己的“GDT”。。可能是在 IRQ 触发时发生故障,因为 CS 选择器在中断发生时被加载。错误的 GDTR 可能会导致失败
  • 这里是凌晨 1:30。我能够重现可能是您的问题。我最好的猜测是您需要在设置 IDT 之前手动设置自己的 GDT。看来 QEMU 多重引导加载程序(与 -kernel 一起使用)对 GDTR/GDT 更加宽容。我能够破解一个 GDT 并且它似乎解决了这个问题。当天晚些时候,我将在另一个链接上更新我的答案。

标签: c x86 interrupt osdev multiboot


【解决方案1】:

昨晚我预感到为什么通过 GRUB 加载和通过 QEMU 的 Multiboot -kernel 功能加载可能无法按预期工作。这是在 cmets 中捕获的。我已经设法根据 OP 发布的更多源代码确认了调查结果。

Mulitboot Specification 中有一条关于 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. 

虽然它说明了将设置什么类型的描述符,但实际上并没有指定描述符必须具有特定的索引。一个多重引导加载程序可能在索引 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!");
}

【讨论】:

  • 非常感谢。您的回答出乎意料地快,它完美地解释了问题。但是我注意到一个奇怪的现象。在 QEMU 和 VMWare 中,引导修改后的代码时 VGA 光标消失了。我已经确认在-kernel 模式下的 QEMU 中不会发生这种情况。
  • 当我有更多时间时,我将不得不稍后查看光标问题。可能我的代码不是原因,但可能与 GRUb 如何设置图形模式有关(我现在只是猜测,直到我有时间查看)
  • 即使我注释掉所有新代码,似乎确实会发生这种情况。我不记得以前发生过这种情况,但可能是我根本没有注意到它。 GRUB 确实给我带来了很多谜团。
  • @user3043260 :在我找到一个好的解决方案之前,您可能可以通过将 grub.cfg 设置为从 0 到 1 来更改 grub.cfg 来显示光标。这是一个快速破解
  • 我是最近才添加的,所以您认为这可能是问题所在?这可以解释为什么我没有确定地注意到。谢谢你的时间,伙计。我很感激!
猜你喜欢
  • 2016-04-06
  • 1970-01-01
  • 2021-07-09
  • 2014-12-14
  • 2015-08-22
  • 2023-04-09
  • 1970-01-01
  • 2013-11-22
相关资源
最近更新 更多