x86保护模式下的键盘中断导致处理器错误 [英] Keyboard interrupt in x86 protected mode causes processor error

查看:134
本文介绍了x86保护模式下的键盘中断导致处理器错误的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm working on a simple kernel and I've been trying to implement a keyboard interrupt handler to get rid of port polling. I've been using QEMU in -kernel mode (to reduce compile time, because generating the iso using grub-mkrescue takes quite some time) and it worked just fine, but when I wanted to switch to -cdrom mode it suddenly started crashing. I had no idea why.

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

Eventually I've realized that when it boots from an iso it also runs a GRUB bootloader before booting the kernel itself. I've figured out GRUB probably switches the processor into protected mode and that causes the problem.

问题: 通常,我只是简单地初始化中断处理程序,每当我按下一个键时,都会对其进行处理.但是,当我使用iso运行内核并按一个键时,虚拟机便崩溃了.这种情况在qemu和VMWare中都发生过,因此我认为我的中断一定有问题.

the problem: Normally I'd simply initialize the interrupt handler and whenever I'd press a key it would be handled. However when I run my kernel using an iso and pressed a key the virtual machine simply crashed. This happened in both qemu and VMWare so I assume there must be something wrong with my interrupts.

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

Bear in mind that the code work just fine for as long as I don't use GRUB. interrupts_init()(see below) is one of the first things called in the main() kernel function.

本质上,问题是:是否有办法使它在保护模式下工作?.

可以在我的 GitHub存储库中找到我的内核的完整副本.一些相关文件:

A complete copy of my kernel can be found in my GitHub repository. Some relevant files:

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 的Multiboot -kernel功能加载可能不起作用如预期的那样.这是在注释中记录的.我已经设法根据OP发布的更多源代码来确认调查结果.

I had a hunch last night as to why loading through GRUB and loading through the Multiboot -kernel feature of QEMU might not work as expected. That is captured in the comments. I have managed to confirm the findings based on more of the source code being released by the OP.

Mulitboot规范中,有一条关于 GDTR GDT 关于修改相关的选择器:

In the Mulitboot Specification there is a note about the GDTR and the GDT with regards to modifying selectors that is relevant:

GDTR

即使段寄存器如上所述进行设置,"GDTR"也可能无效,因此OS映像在设置自己的"GDT"之前,不得加载任何段寄存器(即使只是重新加载相同的值!). '.

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 选择器,从而导致问题.

An interrupt routine could alter the CS selector causing issues.

还有另一个问题,很可能是问题的根本原因. Multiboot规范还说明了有关在其 GDT 中创建的选择器的信息:

There is another concern and most likely the root cause of problems. The Multiboot specification also states this about the selectors it creates in its 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.当您查看代码的一行时,这尤其相关:

Although it says what types of descriptors will be set up it doesn't actually specify that a descriptor has to have a particular index. One Multiboot loader may have a Code segment descriptor at index 0x08 and another bootloader may use 0x10. This is of particular relevance when you look at one line of your code:

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

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

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

This creates an IDT descriptor for interrupt 0x21. The third parameter 0x08 is the Code selector the CPU needs to use to access the interrupt handler. I discovered this works on QEMU where the code selector is 0x08, but in GRUB it appears to be 0x10. In GRUB the 0x10 selector points at a non-executable Data segment and this will not work.

要解决所有这些问题,最好的办法是在启动内核之后,设置 IDT 和启用中断之前不久设置自己的 GDT .如果您需要更多内容,请在 OSDev Wiki 中的 GDT 上找到指南.信息.

To get around all these problems the best thing to do is set up your own GDT shortly after starting up your kernel and before setting up an IDT and enabling interrupts. There is a tutorial on the GDT in the OSDev Wiki if you want more information.

要设置 GDT ,我只需在lowlevel.asm中创建一个汇编程序例程,即可通过添加load_gdt函数和数据结构来做到这一点:

To set up a GDT I'll simply create an assembler routine in lowlevel.asm to do it by adding a load_gdt function and data structures:

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 条目初始化中指定为代码选择器的内容匹配:

This creates and loads a GDT that has a NULL Descriptor at index 0x00, a 32-bit code descriptor at 0x08, and a 32-bit data descriptor at 0x10. Since we are using 0x08 as the code selector this matches what you specify as a code selector in your IDT entry initialization for interrupt 0x21:

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

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

唯一的另一件事是您需要修改kernel.c来调用load_gdt.可以使用类似的方法做到这一点:

The only other thing is that you'll need to amend your kernel.c to call load_gdt. One can do that with something like:

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

这篇关于x86保护模式下的键盘中断导致处理器错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆