我正在研究一个简单的内核,我一直在尝试实现一个键盘中断处理程序来摆脱端口轮询。我一直在-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:
我昨晚有一个预感,为什么通过GRUB加载并通过QEMU的Multiboot -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!");
}