Прерывание клавиатуры в защищенном режиме 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!");
}
Другие вопросы по тегам