Demilade Sonuga's blogAll posts
The Interrupt Descriptor Table I
In the previous post, we took a brief look at the pieces of x86's interrupt system and how they fit together. We looked at the IDT and saw how it is the mechanism used to determine what to do when an interrupt occurs. At computer start-up, the UEFI firmware sets up its own IDT which it uses to handle its timer services. Now that we've exited the Boot Services, we can set up our own IDT.
Hence, we start our foray into the interrupt world with the IDT. As we begin this new chapter of handling intel's structures, we must be careful to go bit by bit to avoid getting overwhelmed with too much detail because to get the IDT into our code, we have to look at it (and a few other things) in a lot more detail (and the details are a lot), then model it in our code.
Remember that the IDT is just an array of function pointers, and the first 32 entries must be handlers for exceptions. Before we can proceed, we first need to know what the IDT looks like:
|0||Divide Error (Division by zero)|
|1||Debug (Used by debuggers)|
|5||BOUND Range Exceeded|
|7||Device Not Available|
|9||CoProcessor Segment Overrun|
|11||Segment Not Present|
|12||Stack Segment Fault|
|19||SIMD Floating-Point Exception|
|21||Control Protection Exception|
We're not going to have to deal with most of these errors, but they still have to be on the table.
Now, that we've seen the entries that have to be in the table, we need to know what exactly these entries look like so we can pin them down in the code.
A single entry in the IDT looks like this:
|0..=15||First 16 bits of handler pointer|
|48..=63||Next 16 bits of handler pointer|
|64..=95||Upper 32 bits of handler pointer|
In other words, a single IDT entry is just a 128-bit value whose chunks of bits have a specific meaning. I said previously that the IDT is an array of function pointers, but that's not very accurate. It's actually an array of function pointers along with a few more pieces of some required information.
To understand the bits column of the table, you have to view a binary number as an array of binary digits whose index starts from the right. For example, consider the 8-bit binary representation of the number 5: 00000101. You can number the bits from 0 to 7 starting from the right so that bit 0 is 1, bit 1 is 0, bit 2 is 1, and so on. Bits 0..=2 is 101. Bit 7 is the leftmost bit. Likewise, in any n-bit number, the rightmost bit is bit 0, the bit numbers increase to the left and the leftmost bit is bit n-1. So, when I say "the first bit", what I mean is "the first bit from the right". Lower means from the right and upper means from the left. For example, the lower 4 bits of 00000101 are 0101 and the upper 4 bits are 0000.
The function pointers themselves are broken into 3 pieces. The first 16 bits, the next 16 bits and the upper 32 bits. So, if the function pointer is 0b00000000000000000000000000000000_1010101010101010_1111111111111111 (64 bits because x86-64 is a 64-bit machine), then bits 0..=15 of the entry will be 1111111111111111, bits 48..=63 will be 1010101010101010 and bits 64..=95 will be all 0s.
The segment selector specified up there is something we'll get to shortly.
The Options field itself is just a 16-bit value with sections of its bits having specific meanings:
|0..=2||An offset into the Interrupt Stack Table|
|13..=14||CPU privilege levels|
The first 3 bits represent an offset into a structure called the Interrupt Stack Table. The exact meaning of this is something we'll get to later.
The entry type field tells whether or not the entry is handling exceptions. Bit 12 is just a 0 with no meaning.
CPU privilege levels, or CPU rings, are a security mechanism used to stop executing programs from accessing computer resources they're not meant to be able to access. We're not going to need to pay attention to this because our game is essentially its own OS and should be able to access all resources the computer has to offer but if you want to learn more about the CPU rings, check here.
Bit 15, the present bit, must be 1 for the IDT entry to be considered valid.
As for the segment selector mentioned earlier, this is an index into another structure similar to the IDT called the Global Descriptor Table (GDT). This structure must have been set up before we can start putting entries into the IDT.
The Interrupt Stack Table is a structure stored in another structure called the Task State Segment (TSS). This structure is not needed for most things, but we'll still need it.
So, before we can set up an IDT, we first need to set up the GDT and the TSS. And, as we'll see shortly, the TSS requires the GDT to be set up.
Our algorithm for getting interrupts into our code now looks like this:
- Understand, model and set up the GDT.
- Understand, model and set up the TSS.
- Get on with the IDT.
We'll begin with step 1 in the next post.
- The IDT is just an array of function pointers along with some required information.
- The IDT requires that the GDT and, in our case, a TSS be set up.
We'll be getting into the GDT
- Exceptions and Interrupts: Intel Architecture Software Developer's Manual, section 6.5.1, table 6-1.
You can download the Intel Architecture Software Developer Manual here