This is the first implementation task related to handling interrupts. Interrupts, as their name suggests, will interrupt the normal control flow of your kernel when certain events occur. We will use interrupts to receive signals from devices like the keyboard, but first we need to set up handling for standard interrupts that tell us when something goes wrong in a running program. You can find a table of these interrupts (which Intel calls Exceptions) in table 6-1 in our assigned reading from volume 3 of the Intel Software Developers Manual (page 3006 in the combined PDF).
To handle interrupts, we will create and load an Interrupt Descriptor Table (IDT). The interrupt descriptor table is an array of IDT Gate Descriptors, but we’ll just call them IDT Entries in this class.
Your task for our next class is to complete the implementation of the idt_setup() and idt_set_handler() functions below, which should create and install a 256-entry IDT.
Your IDT should set handlers for all of the named exceptions in table 6-1;
these handlers should print the error, the associated error code (if the exception provides one), and then call the halt() function to stop the kernel.
You’ll need a different handler function for each exception, although you can start with one generic handler to test your IDT.
Interrupt handlers are a lot like normal functions, but they use slightly different calling conventions, and will return differently (although our handlers will not return at this point). To request that the compiler generate an interrupt handler rather than a normal function, we use a special attribute. See the example handler below:
typedef struct interrupt_context {
uintptr_t ip;
uint64_t cs;
uint64_t flags;
uintptr_t sp;
uint64_t ss;
} __attribute__((packed)) interrupt_context_t;
__attribute__((interrupt))
void example_handler(interrupt_context_t* ctx) {
kprintf("example interrupt handler\n");
halt();
}
The interrupt_context_t struct tells the interrupt handler about what was happening when the interrupt occurred.
We’re not going to use it yet, but we’re required to accept it as a parameter for any interrupt handler.
Some interrupts provide error codes (see table 6-1 again). To write handlers for these interrupts, add an additional error code parameter:
__attribute__((interrupt))
void example_handler_ec(interrupt_context_t* ctx, uint64_t ec) {
kprintf("example interrupt handler (ec=%d)\n", ec);
halt();
}
Write at least one handler before moving on, although you will eventually need to write a different handler for each standard exception.
Now you need to create an IDT to arrange for your handler(s) to be called when an interrupt occurs.
We’ll do that in the idt_setup() function, which you should call from your kernel’s _start function.
Make sure you do this after term_setup so you are able to use kprintf while you are setting up the IDT.
The idt_setup() function will use the idt_set_handler() helper function to install handlers for individual interrupts.
The starter code below includes partial implementations and some instructions for idt_setup() and idt_set_handler().
// Every interrupt handler must specify a code selector. We'll use entry 5 (5*8=0x28), which
// is where our bootloader set up a usable code selector for 64-bit mode.
#define IDT_CODE_SELECTOR 0x28
// IDT entry types
#define IDT_TYPE_INTERRUPT 0xE
#define IDT_TYPE_TRAP 0xF
// A struct the matches the layout of an IDT entry
typedef struct idt_entry {
uint16_t offset_0;
uint16_t selector;
uint8_t ist : 3;
uint8_t _unused_0 : 5;
uint8_t type : 4;
uint8_t _unused_1 : 1;
uint8_t dpl : 2;
uint8_t present : 1;
uint16_t offset_1;
uint32_t offset_2;
uint32_t _unused_2;
} __attribute__((packed)) idt_entry_t;
// Make an IDT
idt_entry_t idt[256];
/**
* Set an interrupt handler for the given interrupt number.
*
* \param index The interrupt number to handle
* \param fn A pointer to the interrupt handler function
* \param type The type of interrupt handler being installed.
* Pass IDT_TYPE_INTERRUPT or IDT_TYPE_TRAP from above.
*/
void idt_set_handler(uint8_t index, void* fn, uint8_t type) {
// Fill in all fields of idt[index]
// Make sure you fill in:
// handler (all three parts, which requires some bit masking/shifting)
// type (using the parameter passed to this function)
// p=1 (the entry is present)
// dpl=0 (run the handler in kernel mode)
// ist=0 (we aren't using an interrupt stack table, so just pass 0)
// selector=IDT_CODE_SELECTOR
}
// This struct is used to load an IDT once we've set it up
typedef struct idt_record {
uint16_t size;
void* base;
} __attribute__((packed)) idt_record_t;
/**
* Initialize an interrupt descriptor table, set handlers for standard exceptions, and install
* the IDT.
*/
void idt_setup() {
// Step 1: Zero out the IDT, probably using memset (which you'll have to implement)
// Write me!
// Step 2: Use idt_set_handler() to set handlers for the standard exceptions (1--21)
// Write me!
// Step 3: Install the IDT
idt_record_t record = {
.size = sizeof(idt),
.base = idt
};
__asm__("lidt %0" :: "m"(record));
}
One of the faults we are going to handle is a Page Fault, which occurs any time our kernel makes an invalid memory access.
One easy way to make invalid memory accesses is to dereference low-valued pointers (like NULL, which is defined to be 0).
However, our kernel is actually booted with a page mapped at address 0x0.
We need to add a boot header tag to request that this page be unmapped.
Add the following tag to boot.c:
static struct stivale2_tag unmap_null_hdr_tag = {
.identifier = STIVALE2_HEADER_TAG_UNMAP_NULL_ID,
.next = 0
};
The bootloader will find this tag by walking the linked list of header tags, so you will need to link this tag into the existing list of header tags.
Change the next field of the terminal header tag to be (uintptr_t)&unmap_null_hdr_tag instead of 0.
At this point you should be able to trigger a page fault with the following code in _start:
int* p = (int*)0x1;
*p = 123;
If you’ve set up your IDT correctly you should see the error from your page fault handler, along with an error code of 2. If the IDT is not set up correctly then your kernel will just reboot when the page fault occurs.
If you change the above example to set p to NULL there’s a good chance you’ll get an invalid opcode exception instead of a page fault.
That happens because the compiler is smart enough to recognize that you are intentionally causing a fault, so it will generate the ud assembly instruction, which is always guaranteed to generate an undefined opcode exception.
You can also generate interrupts of any number with the inline assembly below.
Add this to your _start function and change 3 to whatever number you want to test.
Note that invoking interrupts this way will not report an error code so you will see some strange values for ec when you cause an interrupt that expects an error code.
__asm__("int $3");