Multiboot 键盘驱动程序与 GRUB(与 QEMU 一起工作)出现三重故障 - 为什么?

Multiboot keyboard driver triple faults with GRUB (works with QEMU) - why?

我已经阅读了大量关于 OS x86 开发的教程,到目前为止一切顺利 - 直到现在。我无法弄清楚我一生的解决方案。

我的目标是为 x86 编写最简单的键盘驱动程序。 QEMU 运行良好,但 GRUB 运行不佳。

我尽力模仿 mkeykernel based on the article by Arjun Sreedharan。不幸的是,mkeykernel也存在这个问题。

当 运行 使用 qemu-system-i386 -kernel kernel.bin 编译我的内核时,一切都按预期工作:我输入,字母显示在屏幕上。

但是,当我使用 grub-mkrescue 创建并 运行 GRUB ISO 时,只要我按下一个键,系统就会重新启动。

当 运行 qemu-system-i386 -cdrom build/myos.iso -d int --no-reboot 时,我发现 CPU 异常是 0xd General Protection Fault。起初,我认为这是因为 GRUB 以一种意想不到的方式设置了 GDT。但是正如您将在下面看到的,我添加了自己的 GDT,但并没有解决问题。

我还在 Whosebug 上找到了一个接近的匹配项。我几乎遵循了那篇文章中的所有建议,尤其是关于打包结构的建议,但无济于事。

这是我第一次被难到写了一个 Whosebug 问题 :) 希望有人能在这里看到这个问题!

我已经包含了所有相关文件的源代码和构建它们/重现问题的说明。


第一个文件: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(主要基于 this handy guide

; 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 初学者,您是否有其他资源可以推荐获取键盘?我真的很想用我的小 mini-OS!

最终获得一些保护模式的用户输入

我应该使用除 GRUB 之外的其他引导加载程序吗?

TLDR:简单的键盘驱动程序与 QEMU -kernel 选项一起工作,但在使用 grub-mkrescue.

创建 ISO 时失败

解决方案(感谢@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 Record 有效,也不保证哪个选择器编号用于代码段,哪个选择器编号用于数据段分割。因此,您需要加载 GDT 并使用特定于 GDT 的选择器值。未能正确设置代码段 (CS) 选择器可能会在第一次中断发生时导致问题。

我还在主要方法中注释掉了load_gdt(),这样我就不会重复了。

再次感谢你,迈克尔。如果你 post 作为答案,我一定会接受你的:)