8.0 KiB
Exceptions and Interrupts
Sometimes, things go wrong. When they do, we want to fail gracefully - or even recover. That's the point of exceptions.
Interrupt Service Routines
The x86 interrupt handling method is, for historical reasons I assume, messy. The x86_64 architecture saw a slight improvement in that the stack pointer and segment are always pushed, even if the cpu was running in ring 0 when the interrupt happened. Still, though, some exceptions push an error code, and others do not. And no data is provided to determine which interrupt occurred, besides which interrupt service routine was called.
If all interrupts pushed a dummy error code and an identifying number, a single ISR would be enough, and the rest could be done in software.
Anyway. Let's play with the cards we're dealt.
The most common way of solving this discrepancy is by having a number of short ISRs in the form
isr1:
push 0 //; Dummy error code
push 1 //; Interrupt number
jmp isr_common //; The rest is the same for all interrupts
You may want up to 256 ISRs, so let's do some finger warmup exercises!
Or rather yet, let's generate the ISRs automatically. With python!
src/kernel/cpu/isr.S.py
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from __future__ import print_function
num_isr = 256
pushes_error = [8, 10, 11, 12, 13, 14, 17]
print('''
.intel_syntax noprefix
.extern isr_common
''')
print('// Interrupt Service Routines')
for i in range(num_isr):
print('''isr{0}:
cli
{1}
push {0}
jmp isr_common '''.format(i,
'push 0' if i not in pushes_error else 'nop'))
print('')
print('''
// Vector table
.section .data
.global isr_table
isr_table:''')
for i in range(num_isr):
print(' .quad isr{}'.format(i))
This outputs an assembly file with 256 ISRs like the one above, except numbers
8, 10, 11, 12, 13, 14 and 17, which has an nop
instruction instead of pushing
a bogus error code.
It's written for python 2 because that's what's included in the alpine version the build docker image is based on - despite it being 2018. The encoding is utf-8, and I import the print function from __future__, because it's 2018.
It also makes a table with pointers to each ISR, which makes it easy to set up the Interrupt Descriptor Table later:
src/kernel/cpu/interrupts.c
...
struct idt
{
uint16_t base_l;
uint16_t cs;
uint8_t ist;
uint8_t flags;
uint16_t base_m;
uint32t base_h;
uint32_t _;
}__attribute__((packed)) idt[NUM_INTERRUPTS];
extern uintptr_t isr_table[]
void interrupt_init()
{
memset(idt, 0, sizeof(idt));
for(int i=0; i < NUM_INTERRUPTS; i++)
{
idt[i].base_l = isr_table[i] & 0xFFFF;
idt[i].base_m = (isr_table[i] >> 16) & 0xFFFF;
idt[i].base_h = (isr_table[i] >> 32) & 0xFFFFFFFF;
idt[i].cs = 0x8;
idt[i].ist = 0;
idt[i].flags = IDT_PRESENT | IDT_DPL0 | IDT_INTERRUPT;
}
...
isr_common
pushes all registers to the stack (one by one, there's no pusha
instruction in x86_64) and passes controll to a c interrupt handler. Note that
for x86_64 the arguments to a function is not primarily passed on the stack,
but in registers. So the last thing it does before calling the c function is
move the stack pointer into rdi
. In case the handler returns, isr_common
restores the stack pointer from rax
- which is the function return value,
pops all values again, and performs an iretq
instruction, which is pretty
much a backwards interrupt.
src/kernel/cpu/isr_common.S
...
isr_common:
push r15
push r14
...
push rbx
push rax
mov rdi, rsp
call int_handler
mov rdi, rax
isr_return:
mov rsp, rdi
pop rax
pop rbx
...
pop r14
pop r15
add rsp, 0x10
iretq
But what's the deal with passing rax
to rsp
via rdi
? Doing it this way
will allow us to call isr_return
as a function, with a faked interrupt stack.
We'll use this later to get into user mode.
Building isr.S.py
But back to the ISRs. In order to build this, we need some changes in the kernel makefile. First of all, the lines
SRC := $(wildcard **/*.[cS])
OBJ := $(patsubst %, %.o, $(basename $(SRC)))
need to be updated to allow more file extensions:
SRC := $(wildcard **/*.[cS]*)
OBJ := $(patsubst %, %.o, $(basename $(basename $(SRC))))
We also need a special rule to generate .o files from .S.py:
src/kernel/Makefile
%.o: %.S.py
python $^ | $(COMPILE.S) $(DEPFLAGS) -x assembler-with-cpp - -o $@
In theory, it should be enough with a rule of the form
%.S: %.S.py
python $^ > $@
However, this generates the dependency tree .o <- .s <- .S <- .py rather than
.o <- .S <- .py, which uses as
to compile, and causes some other trouble as
well with intermediate files that are removed once, but not if you run make
again, and stuff...
Some of this can be solved with an .INTERMEDIATE:
rule, but that's not very
elegant. The big problem's probably with me rather than make.
The Interrupt Handler
The c interrupt handler routine is a simple thing. Its default modus operandi is to print an error message and hang.
However, before doing this, it checks a table of other interrupt handlers, and if one exists for the current interrupt, it passes execution over to that.
src/kernel/cpu/interrupts.c
registers *int_handler(registers *r)
{
if(int_handlers[r->int_no])
return int_handlers[r->int_no](r);
debug("Unhandled interrupt occurred\n");
debug("Interrupt number: %d Error code: %d\n", r->int_no, r->err_code);
debug_print_registers(r);
PANIC("Unhandled interrupt occurred");
for(;;);
}
Final Note
For tidyness sake, I wrapped the call to interrupt_init
inside a function
called cpu_init
, which in turn is called from kmain
. For now, that's all it
is, but it will soon grow more important.
Bonus: Debugging Interrupts
There's a small problem with the way interrupts are handled by the processor; they don't follow the calling convention.
This means that when an interrupt occurs, and the debugger breaks in the
PANIC
macro, it has lost all context, and we can't see what happened.
But wait. The entire context is saved. It was pushed to the stack and passed to the interrupt handler. And by using gdbs ability to set the value of registers in qemu, we can bring it back into scope.
I put the following function in toolchain/gdbinit
define restore_env
set $name = $arg0
python
registers = {r: gdb.parse_and_eval('$name->' + r) for r in
['rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi', 'rbp', 'rsp', 'r8', 'r9', 'r10',
'r11', 'r12', 'r13', 'r14', 'r15', 'rip']}
for r in registers.items():
gdb.parse_and_eval('$%s=%s' % r)
gdb.execute('frame 0')
end
end
And it's used like this:
(gdb) c
Continuing.
Thread 1 hit Breakpoint 2, int_handler (r=0xffffff8000019f10) at cpu/interrupts.c:74
74 PANIC("Unhandled interrupt occurred");
(gdb) restore_env r
#0 0xffffff8000010caa in divide_two_numbers (divisor=0, dividend=0) at boot/kmain.c:18
18 return dividend/divisor;
(gdb) bt
#0 0xffffff8000010caa in divide_two_numbers (divisor=0, dividend=0) at boot/kmain.c:18
#1 0xffffff8000010dbd in kmain (multiboot_magic=920085129, multiboot_data=0x105fa0) at boot/kmain.c:33
#2 0xffffff8000010efd in .reload_cs () at boot/boot.S:96
#3 0x0000000000000007 in ?? ()
#4 0x0000000000000730 in ?? ()
#5 0x0000000000000000 in ?? ()
(gdb) list
13 for(;;);
14 }
15
16 int divide_two_numbers(int divisor, int dividend)
17 {
18 return dividend/divisor;
19 }
20
21 void kmain(uint64_t multiboot_magic, void *multiboot_data)
22 {
(gdb) p divisor
$1 = 0
(gdb) p divident
$2 = 5
(gdb) frame 1
#1 0xffffff8000010dbd in kmain (multiboot_magic=920085129, multiboot_data=0x105fa0) at boot/kmain.c:33
33 divide_two_numbers(0,5); // Calculate 0/5 and discard the results
(gdb)
By restoring the processor to the state stored in r
, we can debug from where
the interrupt occurred as normal. By backtracing and inspecting variables we
find that whoever wrote line 33 in kmain.c
got the divisor and divident mixed
up, which resulted in a divide by zero exception.