Прерывание клавиатуры в защищенном режиме x86 вызывает ошибку процессора
Я работаю над простым ядром и пытаюсь реализовать обработчик прерываний клавиатуры, чтобы избавиться от опроса портов. Я использую QEMU в -kernel
режим (чтобы уменьшить время компиляции, потому что генерация ISO с использованием grub-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:
1 ответ
Прошлой ночью я догадывался, почему загрузка через GRUB и загрузка через мультизагрузку -kernel
Функция QEMU может работать не так, как ожидалось. Это отражено в комментариях. Мне удалось подтвердить выводы, основываясь на большей части исходного кода, выпущенного ОП.
В спецификации Mulitboot есть примечание о GDTR и GDT в отношении изменения селекторов, которое имеет отношение к:
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.
Хотя в нем говорится, какие типы дескрипторов будут настроены, на самом деле не указывается, что дескриптор должен иметь определенный индекс. Один загрузчик Mulitboot может иметь дескриптор сегмента кода с индексом 0x08, а другой загрузчик может использовать 0x10. Это особенно актуально, когда вы смотрите на одну строку вашего кода:
load_idt_entry (0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8E);
Это создает дескриптор IDT для прерывания 0x21
, Третий параметр 0x08
это селектор кода, который ЦП должен использовать для доступа к обработчику прерываний. Я обнаружил, что это работает на QEMU, где селектор кода 0x08
, но в GRUB, похоже, 0x10
, В GRUB 0x10
селектор указывает на неисполняемый сегмент данных, и это не будет работать.
Чтобы обойти все эти проблемы, лучше всего настроить собственный GDT вскоре после запуска ядра и перед настройкой IDT и разрешением прерываний. Если вы хотите получить больше информации, в WD OSDev есть руководство по 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 seelctor 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, который имеет дескриптор NULL с индексом 0x 00, 32-битный дескриптор кода в 0x08 и 32-битный дескриптор данных в 0x10. Поскольку мы используем 0x08 в качестве селектора кода, это соответствует тому, что вы указали в качестве селектора кода при инициализации записи IDT для прерывания 0x21:
load_idt_entry (0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8E);
Единственное, что вам нужно изменить kernel.c
звонить load_gdt
, Это можно сделать с помощью чего-то вроде:
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!");
}