diff --git a/18-interrupts/Makefile b/18-interrupts/Makefile new file mode 100644 index 0000000..8a68aa0 --- /dev/null +++ b/18-interrupts/Makefile @@ -0,0 +1,46 @@ +C_SOURCES = $(wildcard kernel/*.c drivers/*.c cpu/*.c) +HEADERS = $(wildcard kernel/*.h drivers/*.h cpu/*.h) +# Nice syntax for file extension replacement +OBJ = ${C_SOURCES:.c=.o cpu/interrupt.o} + +# Change this if your cross-compiler is somewhere else +CC = /usr/local/i386elfgcc/bin/i386-elf-gcc +GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb +# -g: Use debugging symbols in gcc +CFLAGS = -g + +# First rule is run by default +os-image.bin: boot/bootsect.bin kernel.bin + cat $^ > os-image.bin + +# '--oformat binary' deletes all symbols as a collateral, so we don't need +# to 'strip' them manually on this case +kernel.bin: boot/kernel_entry.o ${OBJ} + i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary + +# Used for debugging purposes +kernel.elf: boot/kernel_entry.o ${OBJ} + i386-elf-ld -o $@ -Ttext 0x1000 $^ + +run: os-image.bin + qemu-system-i386 -fda os-image.bin + +# Open the connection to qemu and load our kernel-object file with symbols +debug: os-image.bin kernel.elf + qemu-system-i386 -s -fda os-image.bin -d guest_errors,int & + ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" + +# Generic rules for wildcards +# To make an object, always compile from its .c +%.o: %.c ${HEADERS} + ${CC} ${CFLAGS} -ffreestanding -c $< -o $@ + +%.o: %.asm + nasm $< -f elf -o $@ + +%.bin: %.asm + nasm $< -f bin -o $@ + +clean: + rm -rf *.bin *.dis *.o os-image.bin *.elf + rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o cpu/*.o diff --git a/18-interrupts/README.md b/18-interrupts/README.md new file mode 100644 index 0000000..dfc87ee --- /dev/null +++ b/18-interrupts/README.md @@ -0,0 +1,80 @@ +*Concepts you may want to Google beforehand: C types and structs, include guards, type attributes: packed, extern, volatile, exceptions* + +**Goal: Set up the Interrupt Descriptor Table to handle CPU interrupts** + +This lesson and the following ones have been heavily inspired +by [JamesM's tutorial](http://www.jamesmolloy.co.uk/tutorial_html/index.html) + +Data types +---------- + +First, we will define some special data types in `cpu/types.h`, +which will help us uncouple data structures for raw bytes from chars and ints. +It has been carefully placed on the `cpu/` folder, where we will +put machine-dependent code from now on. Yes, the boot code +is specifically x86 and is still on `boot/`, but let's leave +that alone for now. + +Some of the already existing files have been changed to use +the new `u8`, `u16` and `u32` data types. + +From now on, our C header files will also have include guards. + + +Interrupts +---------- + +Interrupts are one of the main things that a kernel needs to +handle. We will implement it now, as soon as possible, to be able +to receive keyboard input in future lessons. + +Another examples of interrupts are: divisions by zero, out of bounds, +invalid opcodes, page faults, etc. + +Interrupts are handled on a vector, with entries which are +similar to those of the GDT (lesson 9). However, instead of +programming the IDT in assembly, we'll do it in C. + +`cpu/idt.h` defines how an idt entry is stored `idt_gate` (there need to be +256 of them, even if null, or the CPU may panic) and the actual +idt structure that the BIOS will load, `idt_register` which is +just a memory address and a size, similar to the GDT register. + +Finally, we define a couple variables to access those data structures +from assembler code. + +`cpu/idt.c` just fills in every struct with a handler. +As you can see, it is a matter +of setting the struct values and calling the `lidt` assembler command. + + +ISRs +---- + +The Interrupt Service Routines run every time the CPU detects an +interrupt, which is usually fatal. + +We will write just enough code to handle them, print an error message, +and halt the CPU. + +On `cpu/isr.h` we define 32 of them, manually. They are declared as +`extern` because they will be implemented in assembler, in `cpu/interrupt.asm` + +Before jumping to the assembler code, check out `cpu/isr.c`. As you can see, +we define a function to install all isrs at once and load the IDT, a list of error +messages, and the high level handler, which kprints some information. You +can customize `isr_handler` to print/do whatever you want. + +Now to the low level which glues every `idt_gate` with its low-level and +high-level handler. Open `cpu/interrupt.asm`. Here we define a common +low level ISR code, which basically saves/restores the state and calls +the C code, and then the actual ISR assembler functions which are referenced +on `cpu/isr.h` + +Note how the `registers_t` struct is a representation of all the registers +we pushed in `interrupt.asm` + +That's basically it. Now we need to reference `cpu/interrupt.asm` from our +Makefile, and make the kernel install the ISRs and launch one of them. +Notice how the CPU doesn't halt even though it would be good practice +to do it after some interrupts. diff --git a/18-interrupts/boot/32bit_print.asm b/18-interrupts/boot/32bit_print.asm new file mode 100644 index 0000000..4169a17 --- /dev/null +++ b/18-interrupts/boot/32bit_print.asm @@ -0,0 +1,26 @@ +[bits 32] ; using 32-bit protected mode + +; this is how constants are defined +VIDEO_MEMORY equ 0xb8000 +WHITE_OB_BLACK equ 0x0f ; the color byte for each character + +print_string_pm: + pusha + mov edx, VIDEO_MEMORY + +print_string_pm_loop: + mov al, [ebx] ; [ebx] is the address of our character + mov ah, WHITE_OB_BLACK + + cmp al, 0 ; check if end of string + je print_string_pm_done + + mov [edx], ax ; store character + attribute in video memory + add ebx, 1 ; next char + add edx, 2 ; next video memory position + + jmp print_string_pm_loop + +print_string_pm_done: + popa + ret diff --git a/18-interrupts/boot/bootsect.asm b/18-interrupts/boot/bootsect.asm new file mode 100644 index 0000000..085d45c --- /dev/null +++ b/18-interrupts/boot/bootsect.asm @@ -0,0 +1,52 @@ +; Identical to lesson 13's boot sector, but the %included files have new paths +[org 0x7c00] +KERNEL_OFFSET equ 0x1000 ; The same one we used when linking the kernel + + mov [BOOT_DRIVE], dl ; Remember that the BIOS sets us the boot drive in 'dl' on boot + mov bp, 0x9000 + mov sp, bp + + mov bx, MSG_REAL_MODE + call print + call print_nl + + call load_kernel ; read the kernel from disk + call switch_to_pm ; disable interrupts, load GDT, etc. Finally jumps to 'BEGIN_PM' + jmp $ ; Never executed + +%include "boot/print.asm" +%include "boot/print_hex.asm" +%include "boot/disk.asm" +%include "boot/gdt.asm" +%include "boot/32bit_print.asm" +%include "boot/switch_pm.asm" + +[bits 16] +load_kernel: + mov bx, MSG_LOAD_KERNEL + call print + call print_nl + + mov bx, KERNEL_OFFSET ; Read from disk and store in 0x1000 + mov dh, 31 ; Our future kernel will be larger, make this big + mov dl, [BOOT_DRIVE] + call disk_load + ret + +[bits 32] +BEGIN_PM: + mov ebx, MSG_PROT_MODE + call print_string_pm + call KERNEL_OFFSET ; Give control to the kernel + jmp $ ; Stay here when the kernel returns control to us (if ever) + + +BOOT_DRIVE db 0 ; It is a good idea to store it in memory because 'dl' may get overwritten +MSG_REAL_MODE db "Started in 16-bit Real Mode", 0 +MSG_PROT_MODE db "Landed in 32-bit Protected Mode", 0 +MSG_LOAD_KERNEL db "Loading kernel into memory", 0 +MSG_RETURNED_KERNEL db "Returned from kernel. Error?", 0 + +; padding +times 510 - ($-$$) db 0 +dw 0xaa55 diff --git a/18-interrupts/boot/disk.asm b/18-interrupts/boot/disk.asm new file mode 100644 index 0000000..be3d0a9 --- /dev/null +++ b/18-interrupts/boot/disk.asm @@ -0,0 +1,46 @@ +; load 'dh' sectors from drive 'dl' into ES:BX +disk_load: + pusha + ; reading from disk requires setting specific values in all registers + ; so we will overwrite our input parameters from 'dx'. Let's save it + ; to the stack for later use. + push dx + + mov ah, 0x02 ; ah <- int 0x13 function. 0x02 = 'read' + mov al, dh ; al <- number of sectors to read (0x01 .. 0x80) + mov cl, 0x02 ; cl <- sector (0x01 .. 0x11) + ; 0x01 is our boot sector, 0x02 is the first 'available' sector + mov ch, 0x00 ; ch <- cylinder (0x0 .. 0x3FF, upper 2 bits in 'cl') + ; dl <- drive number. Our caller sets it as a parameter and gets it from BIOS + ; (0 = floppy, 1 = floppy2, 0x80 = hdd, 0x81 = hdd2) + mov dh, 0x00 ; dh <- head number (0x0 .. 0xF) + + ; [es:bx] <- pointer to buffer where the data will be stored + ; caller sets it up for us, and it is actually the standard location for int 13h + int 0x13 ; BIOS interrupt + jc disk_error ; if error (stored in the carry bit) + + pop dx + cmp al, dh ; BIOS also sets 'al' to the # of sectors read. Compare it. + jne sectors_error + popa + ret + + +disk_error: + mov bx, DISK_ERROR + call print + call print_nl + mov dh, ah ; ah = error code, dl = disk drive that dropped the error + call print_hex ; check out the code at http://stanislavs.org/helppc/int_13-1.html + jmp disk_loop + +sectors_error: + mov bx, SECTORS_ERROR + call print + +disk_loop: + jmp $ + +DISK_ERROR: db "Disk read error", 0 +SECTORS_ERROR: db "Incorrect number of sectors read", 0 diff --git a/18-interrupts/boot/gdt.asm b/18-interrupts/boot/gdt.asm new file mode 100644 index 0000000..e5db388 --- /dev/null +++ b/18-interrupts/boot/gdt.asm @@ -0,0 +1,35 @@ +gdt_start: ; don't remove the labels, they're needed to compute sizes and jumps + ; the GDT starts with a null 8-byte + dd 0x0 ; 4 byte + dd 0x0 ; 4 byte + +; GDT for code segment. base = 0x00000000, length = 0xfffff +; for flags, refer to os-dev.pdf document, page 36 +gdt_code: + dw 0xffff ; segment length, bits 0-15 + dw 0x0 ; segment base, bits 0-15 + db 0x0 ; segment base, bits 16-23 + db 10011010b ; flags (8 bits) + db 11001111b ; flags (4 bits) + segment length, bits 16-19 + db 0x0 ; segment base, bits 24-31 + +; GDT for data segment. base and length identical to code segment +; some flags changed, again, refer to os-dev.pdf +gdt_data: + dw 0xffff + dw 0x0 + db 0x0 + db 10010010b + db 11001111b + db 0x0 + +gdt_end: + +; GDT descriptor +gdt_descriptor: + dw gdt_end - gdt_start - 1 ; size (16 bit), always one less of its true size + dd gdt_start ; address (32 bit) + +; define some constants for later use +CODE_SEG equ gdt_code - gdt_start +DATA_SEG equ gdt_data - gdt_start diff --git a/18-interrupts/boot/kernel_entry.asm b/18-interrupts/boot/kernel_entry.asm new file mode 100644 index 0000000..6728303 --- /dev/null +++ b/18-interrupts/boot/kernel_entry.asm @@ -0,0 +1,4 @@ +[bits 32] +[extern main] ; Define calling point. Must have same name as kernel.c 'main' function +call main ; Calls the C function. The linker will know where it is placed in memory +jmp $ diff --git a/18-interrupts/boot/print.asm b/18-interrupts/boot/print.asm new file mode 100644 index 0000000..b066e0d --- /dev/null +++ b/18-interrupts/boot/print.asm @@ -0,0 +1,37 @@ +print: + pusha + +; keep this in mind: +; while (string[i] != 0) { print string[i]; i++ } + +; the comparison for string end (null byte) +start: + mov al, [bx] ; 'bx' is the base address for the string + cmp al, 0 + je done + + ; the part where we print with the BIOS help + mov ah, 0x0e + int 0x10 ; 'al' already contains the char + + ; increment pointer and do next loop + add bx, 1 + jmp start + +done: + popa + ret + + + +print_nl: + pusha + + mov ah, 0x0e + mov al, 0x0a ; newline char + int 0x10 + mov al, 0x0d ; carriage return + int 0x10 + + popa + ret diff --git a/18-interrupts/boot/print_hex.asm b/18-interrupts/boot/print_hex.asm new file mode 100644 index 0000000..b537ef8 --- /dev/null +++ b/18-interrupts/boot/print_hex.asm @@ -0,0 +1,46 @@ +; receiving the data in 'dx' +; For the examples we'll assume that we're called with dx=0x1234 +print_hex: + pusha + + mov cx, 0 ; our index variable + +; Strategy: get the last char of 'dx', then convert to ASCII +; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N. +; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40 +; Then, move the ASCII byte to the correct position on the resulting string +hex_loop: + cmp cx, 4 ; loop 4 times + je end + + ; 1. convert last char of 'dx' to ascii + mov ax, dx ; we will use 'ax' as our working register + and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros + add al, 0x30 ; add 0x30 to N to convert it to ASCII "N" + cmp al, 0x39 ; if > 9, add extra 8 to represent 'A' to 'F' + jle step2 + add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7 + +step2: + ; 2. get the correct position of the string to place our ASCII char + ; bx <- base address + string length - index of char + mov bx, HEX_OUT + 5 ; base + length + sub bx, cx ; our index variable + mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx' + ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234 + + ; increment index and loop + add cx, 1 + jmp hex_loop + +end: + ; prepare the parameter and call the function + ; remember that print receives parameters in 'bx' + mov bx, HEX_OUT + call print + + popa + ret + +HEX_OUT: + db '0x0000',0 ; reserve memory for our new string diff --git a/18-interrupts/boot/switch_pm.asm b/18-interrupts/boot/switch_pm.asm new file mode 100644 index 0000000..9394da3 --- /dev/null +++ b/18-interrupts/boot/switch_pm.asm @@ -0,0 +1,22 @@ +[bits 16] +switch_to_pm: + cli ; 1. disable interrupts + lgdt [gdt_descriptor] ; 2. load the GDT descriptor + mov eax, cr0 + or eax, 0x1 ; 3. set 32-bit mode bit in cr0 + mov cr0, eax + jmp CODE_SEG:init_pm ; 4. far jump by using a different segment + +[bits 32] +init_pm: ; we are now using 32-bit instructions + mov ax, DATA_SEG ; 5. update the segment registers + mov ds, ax + mov ss, ax + mov es, ax + mov fs, ax + mov gs, ax + + mov ebp, 0x90000 ; 6. update the stack right at the top of the free space + mov esp, ebp + + call BEGIN_PM ; 7. Call a well-known label with useful code diff --git a/18-interrupts/cpu/idt.c b/18-interrupts/cpu/idt.c new file mode 100644 index 0000000..999d0c9 --- /dev/null +++ b/18-interrupts/cpu/idt.c @@ -0,0 +1,17 @@ +#include "idt.h" +#include "../kernel/util.h" + +void set_idt_gate(int n, u32 handler) { + idt[n].low_offset = low_16(handler); + idt[n].sel = KERNEL_CS; + idt[n].always0 = 0; + idt[n].flags = 0x8E; + idt[n].high_offset = high_16(handler); +} + +void set_idt() { + idt_reg.base = (u32) &idt; + idt_reg.limit = IDT_ENTRIES * sizeof(idt_gate_t) - 1; + /* Don't make the mistake of loading &idt -- always load &idt_reg */ + __asm__ __volatile__("lidtl (%0)" : : "r" (&idt_reg)); +} diff --git a/18-interrupts/cpu/idt.h b/18-interrupts/cpu/idt.h new file mode 100644 index 0000000..27bfac5 --- /dev/null +++ b/18-interrupts/cpu/idt.h @@ -0,0 +1,39 @@ +#ifndef IDT_H +#define IDT_H + +#include "types.h" + +/* Segment selectors */ +#define KERNEL_CS 0x08 + +/* How every interrupt gate (handler) is defined */ +typedef struct { + u16 low_offset; /* Lower 16 bits of handler function address */ + u16 sel; /* Kernel segment selector */ + u8 always0; + /* First byte + * Bit 7: "Interrupt is present" + * Bits 6-5: Privilege level of caller (0=kernel..3=user) + * Bit 4: Set to 0 for interrupt gates + * Bits 3-0: bits 1110 = decimal 14 = "32 bit interrupt gate" */ + u8 flags; + u16 high_offset; /* Higher 16 bits of handler function address */ +} __attribute__((packed)) idt_gate_t ; + +/* A pointer to the array of interrupt handlers. + * Assembly instruction 'lidt' will read it */ +typedef struct { + u16 limit; + u32 base; +} __attribute__((packed)) idt_register_t; + +#define IDT_ENTRIES 256 +idt_gate_t idt[IDT_ENTRIES]; +idt_register_t idt_reg; + + +/* Functions implemented in idt.c */ +void set_idt_gate(int n, u32 handler); +void set_idt(); + +#endif diff --git a/18-interrupts/cpu/interrupt.asm b/18-interrupts/cpu/interrupt.asm new file mode 100644 index 0000000..4d7d71b --- /dev/null +++ b/18-interrupts/cpu/interrupt.asm @@ -0,0 +1,289 @@ +; Defined in isr.c +[extern isr_handler] + +; Common ISR code +isr_common_stub: + ; 1. Save CPU state + pusha ; Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax + mov ax, ds ; Lower 16-bits of eax = ds. + push eax ; save the data segment descriptor + mov ax, 0x10 ; kernel data segment descriptor + mov ds, ax + mov es, ax + mov fs, ax + mov gs, ax + + ; 2. Call C handler + call isr_handler + + ; 3. Restore state + pop eax + mov ds, ax + mov es, ax + mov fs, ax + mov gs, ax + popa + add esp, 8 ; Cleans up the pushed error code and pushed ISR number + sti + iret ; pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP + +; We don't get information about which interrupt was caller +; when the handler is run, so we will need to have a different handler +; for every interrupt. +; Furthermore, some interrupts push an error code onto the stack but others +; don't, so we will push a dummy error code for those which don't, so that +; we have a consistent stack for all of them. + +; First make the ISRs global +global isr0 +global isr1 +global isr2 +global isr3 +global isr4 +global isr5 +global isr6 +global isr7 +global isr8 +global isr9 +global isr10 +global isr11 +global isr12 +global isr13 +global isr14 +global isr15 +global isr16 +global isr17 +global isr18 +global isr19 +global isr20 +global isr21 +global isr22 +global isr23 +global isr24 +global isr25 +global isr26 +global isr27 +global isr28 +global isr29 +global isr30 +global isr31 + +; 0: Divide By Zero Exception +isr0: + cli + push byte 0 + push byte 0 + jmp isr_common_stub + +; 1: Debug Exception +isr1: + cli + push byte 0 + push byte 1 + jmp isr_common_stub + +; 2: Non Maskable Interrupt Exception +isr2: + cli + push byte 0 + push byte 2 + jmp isr_common_stub + +; 3: Int 3 Exception +isr3: + cli + push byte 0 + push byte 3 + jmp isr_common_stub + +; 4: INTO Exception +isr4: + cli + push byte 0 + push byte 4 + jmp isr_common_stub + +; 5: Out of Bounds Exception +isr5: + cli + push byte 0 + push byte 5 + jmp isr_common_stub + +; 6: Invalid Opcode Exception +isr6: + cli + push byte 0 + push byte 6 + jmp isr_common_stub + +; 7: Coprocessor Not Available Exception +isr7: + cli + push byte 0 + push byte 7 + jmp isr_common_stub + +; 8: Double Fault Exception (With Error Code!) +isr8: + cli + push byte 8 + jmp isr_common_stub + +; 9: Coprocessor Segment Overrun Exception +isr9: + cli + push byte 0 + push byte 9 + jmp isr_common_stub + +; 10: Bad TSS Exception (With Error Code!) +isr10: + cli + push byte 10 + jmp isr_common_stub + +; 11: Segment Not Present Exception (With Error Code!) +isr11: + cli + push byte 11 + jmp isr_common_stub + +; 12: Stack Fault Exception (With Error Code!) +isr12: + cli + push byte 12 + jmp isr_common_stub + +; 13: General Protection Fault Exception (With Error Code!) +isr13: + cli + push byte 13 + jmp isr_common_stub + +; 14: Page Fault Exception (With Error Code!) +isr14: + cli + push byte 14 + jmp isr_common_stub + +; 15: Reserved Exception +isr15: + cli + push byte 0 + push byte 15 + jmp isr_common_stub + +; 16: Floating Point Exception +isr16: + cli + push byte 0 + push byte 16 + jmp isr_common_stub + +; 17: Alignment Check Exception +isr17: + cli + push byte 0 + push byte 17 + jmp isr_common_stub + +; 18: Machine Check Exception +isr18: + cli + push byte 0 + push byte 18 + jmp isr_common_stub + +; 19: Reserved +isr19: + cli + push byte 0 + push byte 19 + jmp isr_common_stub + +; 20: Reserved +isr20: + cli + push byte 0 + push byte 20 + jmp isr_common_stub + +; 21: Reserved +isr21: + cli + push byte 0 + push byte 21 + jmp isr_common_stub + +; 22: Reserved +isr22: + cli + push byte 0 + push byte 22 + jmp isr_common_stub + +; 23: Reserved +isr23: + cli + push byte 0 + push byte 23 + jmp isr_common_stub + +; 24: Reserved +isr24: + cli + push byte 0 + push byte 24 + jmp isr_common_stub + +; 25: Reserved +isr25: + cli + push byte 0 + push byte 25 + jmp isr_common_stub + +; 26: Reserved +isr26: + cli + push byte 0 + push byte 26 + jmp isr_common_stub + +; 27: Reserved +isr27: + cli + push byte 0 + push byte 27 + jmp isr_common_stub + +; 28: Reserved +isr28: + cli + push byte 0 + push byte 28 + jmp isr_common_stub + +; 29: Reserved +isr29: + cli + push byte 0 + push byte 29 + jmp isr_common_stub + +; 30: Reserved +isr30: + cli + push byte 0 + push byte 30 + jmp isr_common_stub + +; 31: Reserved +isr31: + cli + push byte 0 + push byte 31 + jmp isr_common_stub + + diff --git a/18-interrupts/cpu/isr.c b/18-interrupts/cpu/isr.c new file mode 100644 index 0000000..f4faea0 --- /dev/null +++ b/18-interrupts/cpu/isr.c @@ -0,0 +1,92 @@ +#include "isr.h" +#include "idt.h" +#include "../drivers/screen.h" +#include "../kernel/util.h" + +/* Can't do this with a loop because we need the address + * of the function names */ +void isr_install() { + set_idt_gate(0, (u32)isr0); + set_idt_gate(1, (u32)isr1); + set_idt_gate(2, (u32)isr2); + set_idt_gate(3, (u32)isr3); + set_idt_gate(4, (u32)isr4); + set_idt_gate(5, (u32)isr5); + set_idt_gate(6, (u32)isr6); + set_idt_gate(7, (u32)isr7); + set_idt_gate(8, (u32)isr8); + set_idt_gate(9, (u32)isr9); + set_idt_gate(10, (u32)isr10); + set_idt_gate(11, (u32)isr11); + set_idt_gate(12, (u32)isr12); + set_idt_gate(13, (u32)isr13); + set_idt_gate(14, (u32)isr14); + set_idt_gate(15, (u32)isr15); + set_idt_gate(16, (u32)isr16); + set_idt_gate(17, (u32)isr17); + set_idt_gate(18, (u32)isr18); + set_idt_gate(19, (u32)isr19); + set_idt_gate(20, (u32)isr20); + set_idt_gate(21, (u32)isr21); + set_idt_gate(22, (u32)isr22); + set_idt_gate(23, (u32)isr23); + set_idt_gate(24, (u32)isr24); + set_idt_gate(25, (u32)isr25); + set_idt_gate(26, (u32)isr26); + set_idt_gate(27, (u32)isr27); + set_idt_gate(28, (u32)isr28); + set_idt_gate(29, (u32)isr29); + set_idt_gate(30, (u32)isr30); + set_idt_gate(31, (u32)isr31); + + set_idt(); // Load with ASM +} + +/* To print the message which defines every exception */ +char *exception_messages[] = { + "Division By Zero", + "Debug", + "Non Maskable Interrupt", + "Breakpoint", + "Into Detected Overflow", + "Out of Bounds", + "Invalid Opcode", + "No Coprocessor", + + "Double Fault", + "Coprocessor Segment Overrun", + "Bad TSS", + "Segment Not Present", + "Stack Fault", + "General Protection Fault", + "Page Fault", + "Unknown Interrupt", + + "Coprocessor Fault", + "Alignment Check", + "Machine Check", + "Reserved", + "Reserved", + "Reserved", + "Reserved", + "Reserved", + + "Reserved", + "Reserved", + "Reserved", + "Reserved", + "Reserved", + "Reserved", + "Reserved", + "Reserved" +}; + +void isr_handler(registers_t r) { + kprint("received interrupt: "); + char s[3]; + int_to_ascii(r.int_no, s); + kprint(s); + kprint("\n"); + kprint(exception_messages[r.int_no]); + kprint("\n"); +} diff --git a/18-interrupts/cpu/isr.h b/18-interrupts/cpu/isr.h new file mode 100644 index 0000000..a493e52 --- /dev/null +++ b/18-interrupts/cpu/isr.h @@ -0,0 +1,51 @@ +#ifndef ISR_H +#define ISR_H + +#include "types.h" + +/* ISRs reserved for CPU exceptions */ +extern void isr0(); +extern void isr1(); +extern void isr2(); +extern void isr3(); +extern void isr4(); +extern void isr5(); +extern void isr6(); +extern void isr7(); +extern void isr8(); +extern void isr9(); +extern void isr10(); +extern void isr11(); +extern void isr12(); +extern void isr13(); +extern void isr14(); +extern void isr15(); +extern void isr16(); +extern void isr17(); +extern void isr18(); +extern void isr19(); +extern void isr20(); +extern void isr21(); +extern void isr22(); +extern void isr23(); +extern void isr24(); +extern void isr25(); +extern void isr26(); +extern void isr27(); +extern void isr28(); +extern void isr29(); +extern void isr30(); +extern void isr31(); + +/* Struct which aggregates many registers */ +typedef struct { + u32 ds; /* Data segment selector */ + u32 edi, esi, ebp, esp, ebx, edx, ecx, eax; /* Pushed by pusha. */ + u32 int_no, err_code; /* Interrupt number and error code (if applicable) */ + u32 eip, cs, eflags, useresp, ss; /* Pushed by the processor automatically */ +} registers_t; + +void isr_install(); +void isr_handler(registers_t r); + +#endif diff --git a/18-interrupts/cpu/types.h b/18-interrupts/cpu/types.h new file mode 100644 index 0000000..992a502 --- /dev/null +++ b/18-interrupts/cpu/types.h @@ -0,0 +1,16 @@ +#ifndef TYPES_H +#define TYPES_H + +/* Instead of using 'chars' to allocate non-character bytes, + * we will use these new type with no semantic meaning */ +typedef unsigned int u32; +typedef int s32; +typedef unsigned short u16; +typedef short s16; +typedef unsigned char u8; +typedef char s8; + +#define low_16(address) (u16)((address) & 0xFFFF) +#define high_16(address) (u16)(((address) >> 16) & 0xFFFF) + +#endif diff --git a/18-interrupts/drivers/ports.c b/18-interrupts/drivers/ports.c new file mode 100644 index 0000000..f015689 --- /dev/null +++ b/18-interrupts/drivers/ports.c @@ -0,0 +1,37 @@ +#include "ports.h" + +/** + * Read a byte from the specified port + */ +u8 port_byte_in (u16 port) { + u8 result; + /* Inline assembler syntax + * !! Notice how the source and destination registers are switched from NASM !! + * + * '"=a" (result)'; set '=' the C variable '(result)' to the value of register e'a'x + * '"d" (port)': map the C variable '(port)' into e'd'x register + * + * Inputs and outputs are separated by colons + */ + __asm__("in %%dx, %%al" : "=a" (result) : "d" (port)); + return result; +} + +void port_byte_out (u16 port, u8 data) { + /* Notice how here both registers are mapped to C variables and + * nothing is returned, thus, no equals '=' in the asm syntax + * However we see a comma since there are two variables in the input area + * and none in the 'return' area + */ + __asm__ __volatile__("out %%al, %%dx" : : "a" (data), "d" (port)); +} + +u16 port_word_in (u16 port) { + u16 result; + __asm__("in %%dx, %%ax" : "=a" (result) : "d" (port)); + return result; +} + +void port_word_out (u16 port, u16 data) { + __asm__ __volatile__("out %%ax, %%dx" : : "a" (data), "d" (port)); +} diff --git a/18-interrupts/drivers/ports.h b/18-interrupts/drivers/ports.h new file mode 100644 index 0000000..0b7fa2c --- /dev/null +++ b/18-interrupts/drivers/ports.h @@ -0,0 +1,11 @@ +#ifndef PORTS_H +#define PORTS_H + +#include "../cpu/types.h" + +unsigned char port_byte_in (u16 port); +void port_byte_out (u16 port, u8 data); +unsigned short port_word_in (u16 port); +void port_word_out (u16 port, u16 data); + +#endif diff --git a/18-interrupts/drivers/screen.c b/18-interrupts/drivers/screen.c new file mode 100644 index 0000000..02d4dd7 --- /dev/null +++ b/18-interrupts/drivers/screen.c @@ -0,0 +1,139 @@ +#include "screen.h" +#include "ports.h" +#include "../kernel/util.h" + +/* Declaration of private functions */ +int get_cursor_offset(); +void set_cursor_offset(int offset); +int print_char(char c, int col, int row, char attr); +int get_offset(int col, int row); +int get_offset_row(int offset); +int get_offset_col(int offset); + +/********************************************************** + * Public Kernel API functions * + **********************************************************/ + +/** + * Print a message on the specified location + * If col, row, are negative, we will use the current offset + */ +void kprint_at(char *message, int col, int row) { + /* Set cursor if col/row are negative */ + int offset; + if (col >= 0 && row >= 0) + offset = get_offset(col, row); + else { + offset = get_cursor_offset(); + row = get_offset_row(offset); + col = get_offset_col(offset); + } + + /* Loop through message and print it */ + int i = 0; + while (message[i] != 0) { + offset = print_char(message[i++], col, row, WHITE_ON_BLACK); + /* Compute row/col for next iteration */ + row = get_offset_row(offset); + col = get_offset_col(offset); + } +} + +void kprint(char *message) { + kprint_at(message, -1, -1); +} + + +/********************************************************** + * Private kernel functions * + **********************************************************/ + + +/** + * Innermost print function for our kernel, directly accesses the video memory + * + * If 'col' and 'row' are negative, we will print at current cursor location + * If 'attr' is zero it will use 'white on black' as default + * Returns the offset of the next character + * Sets the video cursor to the returned offset + */ +int print_char(char c, int col, int row, char attr) { + unsigned char *vidmem = (unsigned char*) VIDEO_ADDRESS; + if (!attr) attr = WHITE_ON_BLACK; + + /* Error control: print a red 'E' if the coords aren't right */ + if (col >= MAX_COLS || row >= MAX_ROWS) { + vidmem[2*(MAX_COLS)*(MAX_ROWS)-2] = 'E'; + vidmem[2*(MAX_COLS)*(MAX_ROWS)-1] = RED_ON_WHITE; + return get_offset(col, row); + } + + int offset; + if (col >= 0 && row >= 0) offset = get_offset(col, row); + else offset = get_cursor_offset(); + + if (c == '\n') { + row = get_offset_row(offset); + offset = get_offset(0, row+1); + } else { + vidmem[offset] = c; + vidmem[offset+1] = attr; + offset += 2; + } + + /* Check if the offset is over screen size and scroll */ + if (offset >= MAX_ROWS * MAX_COLS * 2) { + int i; + for (i = 1; i < MAX_ROWS; i++) + memory_copy(get_offset(0, i) + VIDEO_ADDRESS, + get_offset(0, i-1) + VIDEO_ADDRESS, + MAX_COLS * 2); + + /* Blank last line */ + char *last_line = get_offset(0, MAX_ROWS-1) + VIDEO_ADDRESS; + for (i = 0; i < MAX_COLS * 2; i++) last_line[i] = 0; + + offset -= 2 * MAX_COLS; + } + + set_cursor_offset(offset); + return offset; +} + +int get_cursor_offset() { + /* Use the VGA ports to get the current cursor position + * 1. Ask for high byte of the cursor offset (data 14) + * 2. Ask for low byte (data 15) + */ + port_byte_out(REG_SCREEN_CTRL, 14); + int offset = port_byte_in(REG_SCREEN_DATA) << 8; /* High byte: << 8 */ + port_byte_out(REG_SCREEN_CTRL, 15); + offset += port_byte_in(REG_SCREEN_DATA); + return offset * 2; /* Position * size of character cell */ +} + +void set_cursor_offset(int offset) { + /* Similar to get_cursor_offset, but instead of reading we write data */ + offset /= 2; + port_byte_out(REG_SCREEN_CTRL, 14); + port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset >> 8)); + port_byte_out(REG_SCREEN_CTRL, 15); + port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset & 0xff)); +} + +void clear_screen() { + int screen_size = MAX_COLS * MAX_ROWS; + int i; + char *screen = VIDEO_ADDRESS; + + for (i = 0; i < screen_size; i++) { + screen[i*2] = ' '; + screen[i*2+1] = WHITE_ON_BLACK; + } + set_cursor_offset(get_offset(0, 0)); +} + + +int get_offset(int col, int row) { return 2 * (row * MAX_COLS + col); } +int get_offset_row(int offset) { return offset / (2 * MAX_COLS); } +int get_offset_col(int offset) { return (offset - (get_offset_row(offset)*2*MAX_COLS))/2; } diff --git a/18-interrupts/drivers/screen.h b/18-interrupts/drivers/screen.h new file mode 100644 index 0000000..c175fd6 --- /dev/null +++ b/18-interrupts/drivers/screen.h @@ -0,0 +1,19 @@ +#ifndef SCREEN_H +#define SCREEN_H + +#define VIDEO_ADDRESS 0xb8000 +#define MAX_ROWS 25 +#define MAX_COLS 80 +#define WHITE_ON_BLACK 0x0f +#define RED_ON_WHITE 0xf4 + +/* Screen i/o ports */ +#define REG_SCREEN_CTRL 0x3d4 +#define REG_SCREEN_DATA 0x3d5 + +/* Public kernel API */ +void clear_screen(); +void kprint_at(char *message, int col, int row); +void kprint(char *message); + +#endif diff --git a/18-interrupts/kernel/kernel.c b/18-interrupts/kernel/kernel.c new file mode 100644 index 0000000..2afd8ed --- /dev/null +++ b/18-interrupts/kernel/kernel.c @@ -0,0 +1,11 @@ +#include "../drivers/screen.h" +#include "util.h" +#include "../cpu/isr.h" +#include "../cpu/idt.h" + +void main() { + isr_install(); + /* Test the interrupts */ + __asm__ __volatile__("int $2"); + __asm__ __volatile__("int $3"); +} diff --git a/18-interrupts/kernel/util.c b/18-interrupts/kernel/util.c new file mode 100644 index 0000000..d7c5da7 --- /dev/null +++ b/18-interrupts/kernel/util.c @@ -0,0 +1,30 @@ +#include "util.h" + +void memory_copy(char *source, char *dest, int nbytes) { + int i; + for (i = 0; i < nbytes; i++) { + *(dest + i) = *(source + i); + } +} + +void memory_set(u8 *dest, u8 val, u32 len) { + u8 *temp = (u8 *)dest; + for ( ; len != 0; len--) *temp++ = val; +} + +/** + * K&R implementation + */ +void int_to_ascii(int n, char str[]) { + int i, sign; + if ((sign = n) < 0) n = -n; + i = 0; + do { + str[i++] = n % 10 + '0'; + } while ((n /= 10) > 0); + + if (sign < 0) str[i++] = '-'; + str[i] = '\0'; + + /* TODO: implement "reverse" */ +} diff --git a/18-interrupts/kernel/util.h b/18-interrupts/kernel/util.h new file mode 100644 index 0000000..defc436 --- /dev/null +++ b/18-interrupts/kernel/util.h @@ -0,0 +1,5 @@ +#include "../cpu/types.h" + +void memory_copy(char *source, char *dest, int nbytes); +void memory_set(u8 *dest, u8 val, u32 len); +void int_to_ascii(int n, char str[]);