nat*_*iix 4 c x86 interrupt osdev multiboot
我正在开发一个简单的内核,并且一直在尝试实现一个键盘中断处理程序来摆脱端口轮询。我一直在-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
Run Code Online (Sandbox Code Playgroud)
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);
}
Run Code Online (Sandbox Code Playgroud)
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!");
}
Run Code Online (Sandbox Code Playgroud)
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:
Run Code Online (Sandbox Code Playgroud)
昨晚我有一个预感,为什么通过 GRUB 加载和通过QEMU的多引导-kernel功能加载可能无法按预期工作。这是在评论中捕获的。我已经根据 OP 发布的更多源代码设法确认了这些发现。
在Mulitboot 规范中,有关于GDTR和GDT关于修改相关选择器的注释:
广东省税务局
即使段寄存器按上述方式设置,“GDTR”也可能无效,因此操作系统映像不得加载任何段寄存器(即使只是重新加载相同的值!),直到它设置自己的“GDT”。
中断例程可能会改变导致问题的CS选择器。
还有另一个问题,很可能是问题的根本原因。Multiboot 规范还说明了它在其GDT 中创建的选择器:
Run Code Online (Sandbox Code Playgroud)‘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);
这会为中断创建一个IDT描述符0x21。第三个参数0x08是 CPU 需要用来访问中断处理程序的代码选择器。我发现这适用于代码选择器所在的QEMU0x08,但在GRUB 中它似乎是0x10. 在 GRUB 中,0x10选择器指向不可执行的数据段,这将不起作用。
要解决所有这些问题,最好的办法是在启动内核后不久、在设置IDT和启用中断之前设置您自己的GDT。如果您想了解更多信息,可以在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
Run Code Online (Sandbox Code Playgroud)
这将创建并加载一个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!");
}
Run Code Online (Sandbox Code Playgroud)