Тройной сбой драйвера мультизагрузочной клавиатуры с GRUB (работает с QEMU) - почему? - PullRequest
2 голосов
/ 14 июля 2020

Я прочитал массу руководств по разработке ОС на x86, и до сих пор все шло хорошо - до сих пор. Я не могу понять, какое решение есть для меня.

Моя цель - написать самый простой драйвер клавиатуры для x86. Все работает хорошо с QEMU, но не с GRUB.

Я сделал все возможное, чтобы эмулировать mkeykernel на основе статьи Арджуна Сридхарана . К сожалению, эта проблема существует и для mkeykernel .

При запуске моего скомпилированного ядра с использованием qemu-system-i386 -kernel kernel.bin все работает, как ожидалось: я печатаю, и буквы отображаются на экране.

Однако, когда я создаю и запускаю ISO-образ GRUB с использованием grub-mkrescue, система перезагружается всякий раз, когда я нажимаю клавишу.

При работе с qemu-system-i386 -cdrom build/myos.iso -d int --no-reboot я смог обнаружить, что исключение ЦП - 0xd Ошибка общей защиты. Сначала я подумал, что это потому, что GDT был установлен GRUB неожиданным образом. Но, как вы увидите ниже, я добавил свой собственный GDT, и это не устранило проблему.

Я также нашел близкое совпадение в StackOverflow здесь . Я последовал практически всем советам из этой статьи, особенно по поводу упаковки структур, но безрезультатно.

Это первый раз, когда я настолько озадачен, что написал StackOverflow вопрос :) Надеюсь, кто-то сможет увидеть здесь проблему!

Я включил исходный код всех соответствующих файлов и инструкции по их созданию / воссозданию проблемы ниже.

Первый файл: kernel.asm

bits 32
section .multiboot
    dd 0x1BADB002   ; Magic number
    dd 0x0          ; Flags
    dd - (0x1BADB002 + 0x0) ; Checksum

section .text

%include "gdt.asm"

; Make global anything that is used in main.c
global start
global print_char_with_asm
global load_gdt
global load_idt
global keyboard_handler
global ioport_in
global ioport_out
global enable_interrupts

extern main         ; Defined in kernel.c
extern handle_keyboard_interrupt

load_gdt:
    lgdt [gdt_descriptor] ; from gdt.asm
    ret

load_idt:
    mov edx, [esp + 4]
    lidt [edx]
    ret

enable_interrupts:
    sti
    ret

keyboard_handler:
    pushad
    cld
    call handle_keyboard_interrupt
    popad
    iretd

ioport_in:
    mov edx, [esp + 4]
    in al, dx
    ret

ioport_out:
    mov edx, [esp + 4]
    mov eax, [esp + 8]
    out dx, al
    ret

print_char_with_asm:
    ; OFFSET = (ROW * 80) + COL
    mov eax, [esp + 8]      ; eax = row
    mov edx, 80                     ; 80 (number of cols per row)
    mul edx                             ; now eax = row * 80
    add eax, [esp + 12]     ; now eax = row * 80 + col
    mov edx, 2                      ; * 2 because 2 bytes per char on screen
    mul edx
    mov edx, 0xb8000            ; vid mem start in edx
    add edx, eax                    ; Add our calculated offset
    mov eax, [esp + 4]      ; char c
    mov [edx], al
    ret

start:
    cli             ; Disable interrupts
    mov esp, stack_space
    call main
    hlt

section .bss
resb 8192           ; 8KB for stack
stack_space:

Второй файл: kernel.c

// ----- Pre-processor constants -----
#define ROWS 25
#define COLS 80
// IDT_SIZE: Specific to x86 architecture
#define IDT_SIZE 256
// KERNEL_CODE_SEGMENT_OFFSET: the first segment after the null segment in gdt.asm
#define KERNEL_CODE_SEGMENT_OFFSET 0x8
// 32-bit Interrupt gate: 0x8E
// ( P=1, DPL=00b, S=0, type=1110b => type_attr=1000_1110b=0x8E) (thanks osdev.org)
#define IDT_INTERRUPT_GATE_32BIT 0x8e
// IO Ports for PICs
#define PIC1_COMMAND_PORT 0x20
#define PIC1_DATA_PORT 0x21
#define PIC2_COMMAND_PORT 0xA0
#define PIC2_DATA_PORT 0xA1
// IO Ports for Keyboard
#define KEYBOARD_DATA_PORT 0x60
#define KEYBOARD_STATUS_PORT 0x64

// ----- Includes -----
#include "keyboard_map.h"

// ----- External functions -----
extern void print_char_with_asm(char c, int row, int col);
extern void load_gdt();
extern void keyboard_handler();
extern char ioport_in(unsigned short port);
extern void ioport_out(unsigned short port, unsigned char data);
extern void load_idt(unsigned int* idt_address);
extern void enable_interrupts();

// ----- Structs -----
struct IDT_pointer {
    unsigned short limit;
    unsigned int base;
} __attribute__((packed));
struct IDT_entry {
    unsigned short offset_lowerbits; // 16 bits
    unsigned short selector; // 16 bits
    unsigned char zero; // 8 bits
    unsigned char type_attr; // 8 bits
    unsigned short offset_upperbits; // 16 bits
} __attribute__((packed));

// ----- Global variables -----
struct IDT_entry IDT[IDT_SIZE]; // This is our entire IDT. Room for 256 interrupts
int cursor_pos = 0;

void init_idt() {
    // Get the address of the keyboard_handler code in kernel.asm as a number
    unsigned int offset = (unsigned int)keyboard_handler;
    // Populate the first entry of the IDT
    // TODO why 0x21 and not 0x0?
    // My guess: 0x0 to 0x2 are reserved for CPU, so we use the first avail
    IDT[0x21].offset_lowerbits = offset & 0x0000FFFF; // lower 16 bits
    IDT[0x21].selector = KERNEL_CODE_SEGMENT_OFFSET;
    IDT[0x21].zero = 0;
    IDT[0x21].type_attr = IDT_INTERRUPT_GATE_32BIT;
    IDT[0x21].offset_upperbits = (offset & 0xFFFF0000) >> 16;
    // Program the PICs - Programmable Interrupt Controllers
    ioport_out(PIC1_COMMAND_PORT, 0x11);
    ioport_out(PIC2_COMMAND_PORT, 0x11);
    // ICW2: Vector Offset (this is what we are fixing)
    ioport_out(PIC1_DATA_PORT, 0x20);
    ioport_out(PIC2_DATA_PORT, 0x28);
    // ICW3: Cascading (how master/slave PICs are wired/daisy chained)
    ioport_out(PIC1_DATA_PORT, 0x0);
    ioport_out(PIC2_DATA_PORT, 0x0);
    // ICW4: "Gives additional information about the environemnt"
    ioport_out(PIC1_DATA_PORT, 0x1);
    ioport_out(PIC2_DATA_PORT, 0x1);
    // Voila! PICs are initialized

    // Mask all interrupts
    ioport_out(PIC1_DATA_PORT, 0xff);
    ioport_out(PIC2_DATA_PORT, 0xff);

    struct IDT_pointer idt_ptr;
    idt_ptr.limit = (sizeof(struct IDT_entry) * IDT_SIZE) - 1;
    idt_ptr.base = (unsigned int) &IDT;
    // Now load this IDT
    load_idt(&idt_ptr);
}

void kb_init() {
    // 0xFD = 1111 1101 in binary. enables only IRQ1
    ioport_out(PIC1_DATA_PORT, 0xFD);
}

void handle_keyboard_interrupt() {
    // Write end of interrupt (EOI)
    ioport_out(PIC1_COMMAND_PORT, 0x20);

    unsigned char status = ioport_in(KEYBOARD_STATUS_PORT);
    // Lowest bit of status will be set if buffer not empty
    // (thanks mkeykernel)
    if (status & 0x1) {
        char keycode = ioport_in(KEYBOARD_DATA_PORT);
        if (keycode < 0 || keycode >= 128) return;
        print_char_with_asm(keyboard_map[keycode],0,cursor_pos);
        cursor_pos++;
    }
}

void clear_screen() {
    int i, j;
    for (i = 0; i < COLS; i++) {
        for (j = 0; j < ROWS; j++) {
            print_char_with_asm(' ',j,i);
        }
    }
}

// ----- Entry point -----
void main() {
    clear_screen();
    load_gdt();
    init_idt();
    kb_init();
    enable_interrupts();
    while(1);
}

Третий файл : gdt.asm (в значительной степени основано на этом удобном руководстве )

; GDT - Global Descriptor Table
gdt_start:
gdt_null:   ; Entry 1: Null entry must be included first (error check)
    dd 0x0  ; double word = 4 bytes = 32 bits
    dd 0x0
gdt_code:   ; Entry 2: Code segment descriptor
    ; Structure:
    ; Segment Base Address (base) = 0x0
    ; Segment Limit (limit) = 0xfffff
    dw 0xffff   ; Limit bits 0-15
    dw 0x0000   ; Base bits 0-15
    db 0x00     ; Base bits 16-23
    ; Flag Set 1:
        ; Segment Present: 0b1
        ; Descriptor Privilege level: 0x00 (ring 0)
        ; Descriptor Type: 0b1 (code/data)
    ; Flag Set 2: Type Field
        ; Code: 0b1 (this is a code segment)
        ; Conforming: 0b0 (Code w/ lower privilege may not call this)
        ; Readable: 0b1 (Readable or execute only? Readable means we can read code constants)
        ; Accessed: 0b0 (Used for debugging and virtual memory. CPU sets bit when accessing segment)
    db 10011010b    ; Flag set 1 and 2
    ; Flag Set 3
        ; Granularity: 0b1 (Set to 1 multiplies limit by 4K. Shift 0xfffff 3 bytes left, allowing to span full 32G of memory)
        ; 32-bit default: 0b1
        ; 64-bit segment: 0b0
        ; AVL: 0b0
    db 11001111b    ; Flag set 3 and limit bits 16-19
    db 0x00     ; Base bits 24-31
gdt_data:
    ; Same except for code flag:
        ; Code: 0b0
    dw 0xfffff  ; Limit bits 0-15
    dw 0x0000   ; Base bits 0-15
    db 0x00     ; Base bits 16-23
    db 10010010b    ; Flag set 1 and 2
    db 11001111b    ; 2nd flags and limit bits 16-19
    db 0x00     ; Base bits 24-31

gdt_end:        ; Needed to calculate GDT size for inclusion in GDT descriptor

; GDT Descriptor
gdt_descriptor:
    dw gdt_end - gdt_start - 1  ; Size of GDT, always less one
    dd gdt_start

; Define constants
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

; In protected mode, set DS = INDEX to select GDT entries
; Then CPU knows to use segment at that offset
; Example: (0x0: NULL segment; 0x8: CODE segment; 0x10: DATA segment)

Четвертый файл: grub.cfg

menuentry "myos" {
    multiboot /boot/grub/kernel.bin
}

Пятый файл: linker.ld

OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
{
    . = 1M;
    .text BLOCK(4K) : ALIGN(4K)
    {
        *(.multiboot)
        *(.text)
    }
    .data : { *(.data) }
    .bss : { *(.bss) }
}

Упс, пропустил файл - вот keyboard_map.h:

unsigned char keyboard_map[128] = {
  // -------- 0 to 9 --------
  ' ',
  ' ', // escape key
  '1','2','3','4','5','6','7','8',
  // -------- 10 to 19 --------
  '9','0','-','=',
  ' ', // Backspace
  ' ', // Tab
  'q','w','e','r',
  // -------- 20 to 29 --------
  't','y','u','i','o','p','[',']',
  ' ', // Enter
  ' ', // left Ctrl
  // -------- 30 to 39 --------
  'a','s','d','f','g','h','j','k','l',';',
  // -------- 40 to 49 --------
  ' ','`',
  ' ', // left Shift
  ' ','z','x','c','v','b','n',
  // -------- 50 to 59 --------
  'm',',','.',
  '/', // slash, or numpad slash if preceded by keycode 224
  ' ', // right Shift
  '*', // numpad asterisk
  ' ', // left Alt
  ' ', // Spacebar
  ' ',
  ' ', // F1
  // -------- 60 to 69 --------
  ' ', // F2
  ' ', // F3
  ' ', // F4
  ' ', // F5
  ' ', // F6
  ' ', // F7
  ' ', // F8
  ' ', // F9
  ' ', // F10
  ' ',
  // -------- 70 to 79 --------
  ' ', // scroll lock
  '7', // numpad 7, HOME key if preceded by keycode 224
  '8', // numpad 8, up arrow if preceded by keycode 224
  '9', // numpad 9, PAGE UP key if preceded by keycode 224
  '-', // numpad hyphen
  '4', // numpad 4, left arrow if preceded by keycode 224
  '5', // numpad 5
  '6', // numpad 6, right arrow if preceded by keycode 224
  ' ',
  '1', // numpad 1, END key if preceded by keycode 224
  // -------- 80 to 89 --------
  '2', // numpad 2, down arrow if preceded by keycode 224
  '3', // numpad 3, PAGE DOWN key if preceded by keycode 224
  '0', // numpad 0, INSERT key if preceded by keycode 224
  '.', // numpad dot, DELETE key if preceded by keycode 224
  ' ',' ',' ',' ',' ',' ',
  // -------- 90 to 99 --------
  ' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',
  // -------- 100 to 109 --------
  ' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',
  // -------- 110 to 119 --------
  ' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',
  // -------- 120-127 --------
  ' ',' ',' ',' ',' ',' ',' ',' ',
};
// Right control, right alt seem to send
// keycode 224, then the left control/alt keycode
// Arrow keys also send two interrupts, one 224 and then their actual code
// Same for numpad enter
// 197: Num Lock
// 157: Pause|Break (followed by 197?)
// Clicking on screen appears to send keycodes 70, 198
  // Is this the MARK command or something like that?

Вставьте все вышеперечисленные файлы в каталог на Linux. Затем ...

Чтобы скомпилировать ядро:

mkdir build
nasm -f elf32 kernel.asm -o build/boot.o
gcc -m32 -ffreestanding -c kernel.c -o build/kernel.o
ld -m elf_i386 -T linker.ld -o build/kernel.bin build/boot.o build/kernel.o

Чтобы запустить ядро ​​с QEMU (ДОЛЖЕН РАБОТАТЬ ОТЛИЧНО):

qemu-system-i386 -kernel build/kernel-bin

Чтобы запустить ядро ​​с GRUB (не работает):

mkdir -p build/iso/boot/grub
cp grub.cfg build/iso/boot/grub
cp build/kernel.bin build/iso/boot/grub
grub-mkrescue -o build/myos.iso build/iso
qemu-system-i386 -cdrom build/myos.iso

Кто-нибудь сталкивался с этой проблемой раньше? Есть ли еще один ресурс, который вы бы порекомендовали, чтобы получить клавиатуру новичку на x86? Я действительно хочу, наконец, получить ввод пользователя в защищенном режиме с помощью моей маленькой мини-ОС!

Есть ли другой загрузчик, кроме GRUB, который мне следует использовать?

TL; DR: простая клавиатура драйвер работает с опцией QEMU -kernel, но не работает, когда ISO создается с использованием grub-mkrescue.

1 Ответ

1 голос
/ 14 июля 2020

Решение (спасибо @MichaelPetch) заключалось в настройке сегментных регистров после загрузки GDT. Моя новая точка входа:

start:
    lgdt [gdt_descriptor]
    jmp CODE_SEG:.setcs       ; Set CS to our 32-bit flat code selector
    .setcs:
    mov ax, DATA_SEG          ; Setup the segment registers with our flat data selector
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, stack_space        ; set stack pointer
    cli             ; Disable interrupts
    mov esp, stack_space
    call main
    hlt

Требуется настройка GDT и установка сегментных регистров, потому что спецификация Multiboot не гарантирует, что запись GDT действительна, а также не гарантирует, какой номер селектора соответствует Сегмент кода, а какой - для сегмента данных. Из-за этого вам нужно загрузить свой GDT и использовать значения селектора, указанные c, для вашего GDT. Неправильная установка селектора Code Segment ( CS ) может вызвать проблемы при первом прерывании.

Я также закомментировал load_gdt() в основном методе, поэтому я не делаю этого это дважды.

Еще раз спасибо, Майкл. Если в ответ напишете, я обязательно приму ваш :)

...