x86保护模式键盘中断导致处理器错误
Keyboard interrupt in x86 protected mode causes processor error
我正在开发一个简单的内核,我一直在尝试实现一个键盘中断处理程序来摆脱端口轮询。我一直在 -kernel
模式下使用 QEMU(以减少编译时间,因为使用 grub-mkrescue
生成 iso 需要相当长的时间)并且它工作得很好,但是当我想切换到 -cdrom
模式突然开始崩溃。我不知道为什么。
最终我意识到,当它从 iso 引导时,它在引导内核本身之前还 运行s 一个 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 的多重引导 -kernel
功能加载可能无法按预期工作。这是在评论中捕获的。我已经根据 OP 发布的更多源代码设法确认了这些发现。
在 Mulitboot Specification 中有一个关于 GDTR 和 GDT 的注释,关于修改相关的选择器:
GDTR
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 选择器,从而导致问题。
还有另一个问题,很可能是问题的根本原因。 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
选择器指向不可执行的数据段,这将不起作用。
要解决所有这些问题,最好的办法是在启动内核之后和设置 IDT[ 之前设置自己的 GDT =63=] 并启用中断。如果您想了解更多信息,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 作为代码选择器,这与您在中断 0x21:
的 IDT 条目初始化中指定的代码选择器相匹配
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!");
}
我正在开发一个简单的内核,我一直在尝试实现一个键盘中断处理程序来摆脱端口轮询。我一直在 -kernel
模式下使用 QEMU(以减少编译时间,因为使用 grub-mkrescue
生成 iso 需要相当长的时间)并且它工作得很好,但是当我想切换到 -cdrom
模式突然开始崩溃。我不知道为什么。
最终我意识到,当它从 iso 引导时,它在引导内核本身之前还 运行s 一个 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 的多重引导 -kernel
功能加载可能无法按预期工作。这是在评论中捕获的。我已经根据 OP 发布的更多源代码设法确认了这些发现。
在 Mulitboot Specification 中有一个关于 GDTR 和 GDT 的注释,关于修改相关的选择器:
GDTR
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 选择器,从而导致问题。
还有另一个问题,很可能是问题的根本原因。 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
选择器指向不可执行的数据段,这将不起作用。
要解决所有这些问题,最好的办法是在启动内核之后和设置 IDT[ 之前设置自己的 GDT =63=] 并启用中断。如果您想了解更多信息,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 作为代码选择器,这与您在中断 0x21:
的 IDT 条目初始化中指定的代码选择器相匹配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!");
}