4

我正在研究一个简单的内核,我一直在尝试实现一个键盘中断处理程序来摆脱端口轮询。我一直在-kernel模式下使用 QEMU(以减少编译时间,因为使用生成 isogrub-mkrescue需要相当长的时间)并且它工作得很好,但是当我想切换到-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:
4

1 回答 1

5

昨晚我预感到为什么通过 GRUB 加载和通过QEMU-kernel的 Multiboot功能加载可能无法按预期工作。这在评论中有所体现。我已经设法根据 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);

这会为中断创建一个IDT0x21描述符。第三个参数0x08是 CPU 需要用来访问中断处理程序的代码选择器。我发现这适用于代码选择器所在的QEMU0x08,但在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,该 GDT 在索引 0x00 处具有 NULL 描述符,在 0x08 处具有 32 位代码描述符,在 0x10 处具有 32 位数据描述符。由于我们使用 0x08 作为代码选择器,这与您在IDT条目初始化中为中断 0x21 指定的代码选择器匹配:

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

唯一的另一件事是您需要修改您的kernel.cto call 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!");
}
于 2017-04-02T18:17:50.237 回答