I've been reading a ton of tutorials about OS development on x86, and so far all has gone well - until now. I can't figure out what the solution is for the life of me.
My goal is to write the simplest possible keyboard driver for x86. Things work well with QEMU, but not with GRUB.
I did my best to emulate mkeykernel based on the article by Arjun Sreedharan. Unfortunately, this problem also exists for mkeykernel.
When running my compiled kernel using qemu-system-i386 -kernel kernel.bin
, everything works as expected: I type, and letters are displayed on the screen.
However, when I create and run a GRUB ISO using grub-mkrescue
, the system reboots whenever I hit a key.
When running with qemu-system-i386 -cdrom build/myos.iso -d int --no-reboot
, I was able to find out that the CPU exception is 0xd General Protection Fault. At first, I thought this was because the GDT was set in an unexpected way by GRUB. But as you'll see below, I added my own GDT and it did not fix the problem.
I also found a close match on StackOverflow here. I followed pretty much all of the advice in that article, especially the one about packing the structs, to no avail.
This is the first time I've ever been so stumped to the point that I wrote a StackOverflow question :) Hopefully someone will be able to see the issue here!
I have included the source code for all relevant files and instructions to build them / recreate the problem below.
First file: 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:
Second file: 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);
}
Third file: gdt.asm
(heavily based off of 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)
Fourth file: grub.cfg
menuentry "myos" {
multiboot /boot/grub/kernel.bin
}
Fifth file: linker.ld
OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
{
. = 1M;
.text BLOCK(4K) : ALIGN(4K)
{
*(.multiboot)
*(.text)
}
.data : { *(.data) }
.bss : { *(.bss) }
}
Oops, missed a file - here's 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?
Paste all the above files into a directory together on Linux. Then...
To compile the kernel:
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
To run the kernel with QEMU (SHOULD WORK FINE):
qemu-system-i386 -kernel build/kernel-bin
To run the kernel with GRUB (does not work):
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
Has anyone run into this issue before? Is there another resource you'd recommend to get the keyboard as a beginner on x86? I really want to finally get some protected-mode user input with my little mini-OS!
Is there another bootloader than GRUB that I should be using?
TLDR: Simple keyboard driver works with QEMU -kernel
option, but fails when an ISO is created using grub-mkrescue
.
The solution (thanks to @MichaelPetch) was to setup segment registers after loading the GDT. My new entry point:
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
Setting up the GDT and setting the segment registers is required because the Multiboot specification doesn't guarantee that the GDT Record is valid, nor does it guarantee what selector number is for the Code Segment and which one is for the Data Segment. Because of this you need to load your GDT and use the selector values specific to your GDT. Failure to set the Code Segment (CS) selector properly can cause problems when the first interrupt occurs.
I also commented out load_gdt()
in the main method so that I'm not doing it twice.
Thank you again, Michael. If you post as an answer, I'll be sure to accept yours :)
User contributions licensed under CC BY-SA 3.0