x86_64 IDT
The Interrupt Descriptor Table (IDT) is a data structure on the x86_64 and x86 architectures. It is used for associating interrupt requests with interrupt routines. The IDT consists of 256 vectors and contains three types of interrupt routines:
- Exceptions: events generated by the CPU. They have a fixed mapping from the first and up to 32 interrupt vectors. Exceptions are mostly run-time errors in the program's code, and could be as trivial as a divide by zero or completely non fixable during runtime like a segmentation fault, forcing you to kill the offending process.
- Hardware interrupt/IRQ: events generated by hardware. The mapping depends on how the PIC is programmed. These are routines for things like a timer, the keyboard, or even a disk.
- Software interrupt: events generated by the software. These are defined by the operating system. Linux usually maps syscalls to vector 0x80, however, you can map them anywhere you want as long as they don't conflict with exception or hardware mappings.
In real mode, the IDT is called the Interrupt Vector Table (IVT). The BIOS also provides simple interrupts for interacting with the hardware. It can also provide a memory map, which is the primary way for BIOS bootloaders to detect memory and pass it onto the operating system.
First of all, what is an interrupt?
An interrupt is generated by the hardware using the int instruction, with the argument being the interrupt vector. The interrupt itself is an event which immediately forces the CPU to stop whatever it is doing, and do something else instead. Most modern operating systems use interrupts for preemptive scheduling (Scheduling that is not cooperative; makes task switches happen regardless whether the current task wants it or not. The easiest way to implement it is by having scheduling happen during the timer interrupt), error handling, and system calls, which require changing CPU rings from ring 3 to ring 0, and interrupts do just that by default.
In this page, we will cover implementation of a simple IDT (containing only exception vectors).
Creating the structures
First of all, we need a structure. This is very similar to the GDT structure we implemented earlier. The structures should be packed, for reasons explained in the GDT page:
/* An IDT entry, we're going to have
* 256 of these.
*/
struct idt_entry
{
uint16_t base_low;
uint16_t sel;
uint8_t always0;
uint8_t flags;
uint16_t base_mid;
uint32_t base_high;
uint32_t zero;
} __attribute__ ((packed));
struct idt_ptr
{
uint16_t limit;
uint64_t base;
} __attribute__ ((packed));
/* We have to create the real structures
* with them
*/
struct idt_entry idt[256];
struct idt_ptr idtptr;
/* CPU register table to be used by the ISR handler */
typedef struct
{
uint64_t r15, r14, r13, r12, r11, r10, r9, r8;
uint64_t rbp, rdi, rsi, rdx, rcx, rbx, rax;
uint64_t int_no, err_code;
uint64_t rip, cs, rflags, rsp, ss;
} registers_t;
Filling entries
Again, this is very similar to the GDT code earlier:
/*
* Fill an entry.
* 3. Params:
* num - Index of the IDT entry (0 to 255)
* base - The 64-bit address of the interrupt handler function.
* sel - The segment selector for the code segment that contains
* the handler (usually the kernel code segment).
* flags - Flags:
* - Bit 7: Present (1 if handler is valid)
* - Bits 6-5: Descriptor privilege level (DPL)
* - Bit 4: Storage Segment (always 0 for interrupt gates)
* - Bits 3-0: Gate type (0xE for 64-bit interrupt gate)
* for exception handlers, could be called like this:
* fill_idt_entry (0, (uint64_t)isr0_stub, 0x08, 0x8E);
*/
void fill_idt_entry (int num, uint64_t base, uint16_t sel, uint8_t flags)
{
idt[num].base_low = (uint16_t)(base & 0xFFFF);
idt[num].sel = sel;
idt[num].always0 = 0; /* reserved, must be 0 */
idt[num].flags = flags;
idt[num].base_mid = (uint16_t)((base >> 16) & 0xFFFF);
idt[num].base_high = (uint32_t)((base >> 32) & 0xFFFFFFFF);
idt[num].zero = 0; /* reserved, must be 0 */
}
Writing the ISR handler
There are a few things a proper exception handler should do: 14. Provide a dump of the CPU registers, which is extremely useful for debugging 15. Provide an error code, that would be pushed automatically by the CPU 16. Check if the exception occurred in ring 0 or ring 3. If in ring 0, exceptions are mostly unrecoverable. However, if the exception occurred in ring 3 all you have to do is just kill the user process, and go on with your day. 17. If it is a somewhat recoverable exception (for example, a page fault) decode the error code and try to fix the error.
This page will only cover a basic implementation, which should provide an error code and halt the CPU.
First, we'll need to define the exception messages used:
const char *exception_msgs[] = {
"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"
};
Implementing the interrupt handler yourself should be pretty simple. The assembly stub pushes the CPU registers, which your function can fetch by declaring itself with void isr_handler (registers_t *regs). You should check the interrupt number in regs->int_noand compare it to interrupt names provided in the exception_msgs[] table, then print out the result. If you recovered from the fault, you should acknowledge the interrupt with outb(0x20, 0x20), or if you didn't, you should halt the CPU.
An example assembly stub would be:
; Your exception handler here
extern isr_handler
; ===============================================
; Macros
; ===============================================
; no error code (push a 0)
%macro ISR_NOERR 2
global do_isr%1
do_isr%1:
cli ; disable interrupts
push 0
push %2
jmp isr_common_stub
%endmacro
; error code
%macro ISR_ERR 2
global do_isr%1
do_isr%1:
cli ;disable interrupts
push %2
jmp isr_common_stub
%endmacro
; ===============================================
; isr_common_stub
; ===============================================
isr_common_stub:
; This is the main stub everyone
; jumps to.
; Push registers
push rax
push rbx
push rcx
push rdx
push rsi
push rdi
push rbp
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
; move the registers_t struct on
; the stack to arguments which
; our ISR handler will use
mov rdi, rsp
; call the C handler
call isr_handler
; pop registers from the stack
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rbp
pop rdi
pop rsi
pop rdx
pop rcx
pop rbx
pop rax
; remove error code and num
add rsp, 16
iretq
; Map ISR macros to functions
; Each ISR can be referenced from C code as:
; ---------------------------------------------------------------
; | C Symbol | Vector | Description |
; |------------------------------------------------------------|
; | do_isr0() | 0 | Divide Error |
; | do_isr1() | 1 | Debug Exception |
; | do_isr2() | 2 | Non-Maskable Interrupt |
; | do_isr3() | 3 | Breakpoint |
; | do_isr4() | 4 | Overflow |
; | do_isr5() | 5 | Bound Range Exceeded |
; | do_isr6() | 6 | Invalid Opcode |
; | do_isr7() | 7 | Device Not Available |
; | do_isr8() | 8 | Double Fault |
; | do_isr9() | 9 | Coprocessor Segment Overrun |
; | do_isr10() | 10 | Invalid TSS |
; | do_isr11() | 11 | Segment Not Present |
; | do_isr12() | 12 | Stack Fault |
; | do_isr13() | 13 | General Protection Fault |
; | do_isr14() | 14 | Page Fault |
; | do_isr15() | 15 | Reserved |
; | do_isr16() | 16 | x87 Floating-Point Exception |
; | do_isr17() | 17 | Alignment Check |
; | do_isr18() | 18 | Machine Check |
; |------------------------------------------------------------|
ISR_NOERR 0, 0
ISR_NOERR 1, 1
ISR_NOERR 2, 2
ISR_NOERR 3, 3
ISR_NOERR 4, 4
ISR_NOERR 5, 5
ISR_NOERR 6, 6
ISR_NOERR 7, 7
ISR_ERR 8, 8
ISR_NOERR 9, 9
ISR_ERR 10, 10
ISR_ERR 11, 11
ISR_ERR 12, 12
ISR_ERR 13, 13
ISR_ERR 14, 14
ISR_NOERR 15, 15
ISR_NOERR 16, 16
ISR_ERR 17, 17
ISR_NOERR 18, 18
Loading the IDT
With all the things provided, it should be pretty easy to figure this out on your own. Loading an IDT is extremely similar to loading a GDT.