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 exceptions 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 or software 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.
Pure C Based Interrupts
If you wish to avoid using assembly at all costs it is possible to handle interrupts purely in a high level language like C although it is not always the best way to handle things it can simplify things for making them not having to setup assembly stuff it can cause headaches and other issues if done incorrectly
Creating the structures
These structures are for the IDT itself to be filled in and loaded
struct __attribute__((packed)) IDTEntry {
uint16_t offset_low;
uint16_t selector;
uint8_t ist;
uint8_t type_attr;
uint16_t offset_mid;
uint32_t offset_high;
uint32_t zero;
}
struct __attribute__((packed)) IDTR {
uint16_t limit;
uint64_t base;
};
static struct IDTEntry idt[256];
static struct IDTR idtr;
struct interrupt_frame {
uintptr_t ip;
uintptr_t cs;
uintptr_t flags;
uintptr_t sp;
uintptr_t ss;
};
Filling Entries
This function lets you setup an IDT entry with a C function as the handler
void set_idt_entry(int index, int ist, int attr, void (*handler)()) {
uint64_t addr = (uint64_t)handler;
idt[index] = (struct IDTEntry){
.offset_low = addr & 0xFFFF,
.selector = 0x08,
.ist = ist,
.type_attr = attr,
.offset_mid = (addr >> 16) & 0xFFFF,
.offset_high = (addr >> 32),
.zero = 0
};
}
/*
0x20 : ISR number (In this case being IRQ0 (the PIT) when the master PIC is remapped to 0x20)
0 : The IST (The stack to be switched to based on the TSS, in most cases it should be 0)
0x8E : The attribute value which for kernel only interrupts should be 0x8E
(void (*)())pit_isr : The C function to be called whe the interupt is triggered
*/
set_idt_entry(0x20, 0, 0x8E, (void (*)())pit_isr);
Making C Interrupt Handlers
When making interrupts in pure C you need to use a special attribute __attribute__((interrupt)) for it to work and it will need to use a specific function signature being
void errorless_isr(void* frame) for interrupts that don't provide an error code and void double_fault_isr(struct interrupt_frame* frame, uint64_t error) for ones that do
As an example this interrupt could be assigned to the PIT (Covered Later)
volatile int pitInteruptsTriggered = 0;
__attribute__((interrupt))
void pit_isr(__attribute__((unused)) void* frame) {
pitInteruptsTriggered++;
outb(0x20,0x20);
}
Here the frame is unused so it is marked as such to tell the compiler and volatile is used to tell the compiler that the variable might be modified by an interrupt or another thread
and lets it know to not store it in a register or do other optimizations to it so it works properly when referring to it externally.
You would want a header file for this function that marks that variable as external so other code can get the value and then every time the interrupt is triggered the value will increment and other code will see that happening so you can track time based on the PIT
Loading The IDT
The following code will set up the data structure to use with the lidt instruction and then execute it to load your IDT.
This should be run after loading your entries into the IDT
idtr.limit = sizeof(idt) - 1;
idtr.base = (uint64_t)&idt;
asm volatile ("lidt %0" : : "m"(idtr));
asm volatile ("sti");
Assembly Based Interrupts (Recommended)
Assembly based interrupts which is the recommended way to do these gives you more control on how the C code is called so you can get more exact information like all of the registers and other things verses the limited set of data the CPU will give by default
Creating the structures
These structures are for the IDT itself to be filled in and loaded
struct __attribute__((packed)) IDTEntry {
uint16_t offset_low;
uint16_t selector;
uint8_t ist;
uint8_t type_attr;
uint16_t offset_mid;
uint32_t offset_high;
uint32_t zero;
}
struct __attribute__((packed)) IDTR {
uint16_t limit;
uint64_t base;
};
static struct IDTEntry idt[256];
static struct IDTR idtr;
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;
Assembly Stubs
When using assembly based interrupts you will assign IDT entries to the according stub made in assembly that will call a common stub to prepare data to then call a C function to get the interrupt number and then call the final proper handler for that interrupt.
The following assembly code will implement the macros to make stubs and the common stub that will call the C code
Make sure to have a C header file to refer to the things that you need too.
; 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 |
; |------------------------------------------------------------|
; add more as needed for other ISRs
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
; PIT 32 = 0x20 in hex
ISR_NOERR 32, 32
Filling entries
This function lets you setup an IDT entry with a function as the handler
void set_idt_entry(int index, int ist, int attr, void (*handler)()) {
uint64_t addr = (uint64_t)handler;
idt[index] = (struct IDTEntry){
.offset_low = addr & 0xFFFF,
.selector = 0x08,
.ist = ist,
.type_attr = attr,
.offset_mid = (addr >> 16) & 0xFFFF,
.offset_high = (addr >> 32),
.zero = 0
};
}
/*
0x20 : ISR number (In this case being IRQ0 (the PIT) when the master PIC is remapped to 0x20)
0 : The IST (The stack to be switched to based on the TSS, in most cases it should be 0)
0x8E : The attribute value which for kernel only interrupts should be 0x8E
(void (*)())do_isr32 : The C function to be called when the interrupt is triggered
*/
set_idt_entry(0x20, 0, 0x8E, (void (*)())do_isr32);
Writing the ISR handler
You can now make the void isr_handler (registers_t *regs) function in C that the assembly stub will call.
This function will need to check the ISR number and dispatch to the proper handler.
It should look similar to this
void isr_handler (registers_t *regs) {
// add more cases for other ISRs as needed
switch (regs->int_no) {
case 0x08:
double_fault_isr();
break;
case 0x20:
pit_isr();
break;
default:
generic_isr(regs->int_no, regs->err_code);
break;
}
}
Loading The IDT
The following code will setup the data structure to use with the lidt instruction and then execute it to load your IDT.
This should be run after loading your entries into the IDT