Now that we know what an interrupt is (if not, look back at the previous section of this guide), we’re in a position to create an interrupt vector table and let the CPU know where to find it. We can then fire off our first kernel interrupt (a breakpoint will do nicely as an example).
This article is part of a series on toy operating system development.
The interrupt vector table is a list of 256 addresses, one address for each interrupt. At least, it used to be, in real mode days, and it would live at address 0x0, at the beginning of memory. In protected mode, the idea is similar, but the execution more involved.
For one thing, and just to get this out of the way, the interrupt vector table doesn’t have to live at 0x0 anymore. It can be wherever the kernel wants it to be, and an excellent place is inside the kernels .bss segment: all zeroed out initially, and part of the kernel’s image. That way, the rest of the memory of the entire machine (save reserved memory from the memory map) is free.
Gates
The modern name for the interrupt table is the interrupt descriptor table (IDT). It contains, unsurprisingly, descriptors. For the most part these are addresses: a segment and offset, just like in real mode, but there’s also a set of flags.
Note: The IA32 architecture has descriptors in different places. There are desciptors for use in the global descriptor table (GDT), the interrupt descriptor table (IDT) and the local descriptor table (LDT). These descriptors are all 8 bytes in size, and their structures are vaguely similar, but no more than that. The LDT isn’t really used anymore in 386+ architectures, so that’s one less to worry about.
This is the structure of an interrupt gate descriptor:
| Offset | Size | Meaning |
|---|---|---|
| 0 | 2 | Offset, low word |
| 2 | 2 | Segment selector |
| 4 | 1 | Unused |
| 5 | 1 | Access byte |
| 6 | 2 | Offset, high word |
Here, the segment selector is an index into the global descriptor table, and the 32-bits offset is the offset in that segment, in bytes: this’ll point to an interrupt service routine inside our kernel image.
The access byte is structured like this:
| Bits | Name | Meaning |
|---|---|---|
| 7 | P | Present. Clear means gate is unusable - calling it raised “not present” fault |
| 6-5 | DPL | Descriptor privilege level (0-3) |
| 4 | S | System descriptor. Always 0 for a gate. |
| 3-0 | Type | Gate type |
A descriptor that’s marked as not present will raise a “not present” fault when called. Setting the entire descriptor to 8 bytes with a value of zero does the trick - suitable for interrupts that don’t exist.
Although the gate type has 4 bits and thus 16 possibilities, only three of them are of interest for a modern interrupt descriptor table. The reset are reserved or 16-bit variants for the 286 architecture:
| Type | Meaning |
|---|---|
| 0x5 | Task gate |
| 0xe | 32-bit interrupt gate |
| 0xf | 32-bit trap gate |