我正在研究一个简单的内核,我一直在尝试实现一个键盘中断处理程序来摆脱端口轮询。我一直在-kernel
模式下使用QEMU(为了减少编译时间,因为使用grub-mkrescue
生成iso需要相当长的时间)并且它工作得很好,但是当我想切换到{{1模式突然开始崩溃。我不知道为什么。
最终我意识到,当它从iso引导时,它还会在引导内核之前运行GRUB引导程序。我已经发现GRUB可能会将处理器切换到保护模式并导致问题。
问题: 通常我只是初始化中断处理程序,每当我按下一个键就会被处理掉。但是当我使用iso运行我的内核并按下一个键时,虚拟机就会崩溃。这发生在qemu和VMWare中,所以我认为我的中断一定有问题。
请记住,只要我不使用GRUB,代码就可以正常工作。
-cdrom
(见下文)是interrupts_init()
内核函数中调用的第一个内容之一。
基本上问题是:有没有办法让这项工作处于保护模式?。
我的GitHub repository可以找到我的内核的完整副本。一些相关文件:
main()
:
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
答案 0 :(得分:4)
我昨晚有一个预感,为什么通过GRUB加载并通过 QEMU 的多重引导-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!");
}