Skip to content

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"
};
Now, we'll write the ISR handler. You can do it in two ways: 1. Have a minimal assembly stub that pushes registers and calls the needed C handler, and after that pops the registers off the stack and performs an iretq. 2. Use a compiler-specific interrupt attribute with your C function, which is currently not covered on this page. Both ways are correct and you yourself should decide what to use.

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.