x86 Interrupts

If you've ever made a website in JavaScript and made something happen when you clicked a button, you'd know about event-driven programming (or at least would have used it before). Event-driven programming is a programming paradigm in which the flow of a program is determined by external events. This paradigm will also be used when developing our operating system, and it comes in the form of interrupts.

An interrupt is an event that causes the processor to temporarily stop its current execution and transfer control to an interrupt service routine. The processor then returns to the code it was executing before the interrupt.

Many interrupts exist, and we've actually used one before; this was when we were loading the kernel into memory from the disk; this involved a disk operation; there are also video-related interrupts, etc. A hardware interrupt can occur when you press a key on your keyboard; these will then be handled in a certain way by the operating system that consults correctly with the device drivers.

Either hardware or software can cause an interrupt. The keyboard is an example of a hardware source of interrupts, while software can explicitly cause an interrupt using an instruction such as INT.

Software interrupts have an interrupt number, which specifies which interrupt vector or IDT entry is used. The one used in our bootloader was 10h, when we used int 10h the processor used interrupt vector 10h. A kernel can also provide services that application software can request using software interrupts; this can be stuff like manipulating file systems. These services are called "system calls."

As well as interrupts, exceptions can also occur as another type of event that temporarily stops the processor similarly to an interrupt. The difference, however, is that exceptions are generated by the processor when certain conditions occur while executing an instruction, like for example when an error happens.

The interrupt descriptor table

In x86, there is another table called the "interrupt descriptor table" (IDT). The IDT tells the processor how to reach the interrupt handler for a specific interrupt vector. Entries in the IDT are called "gate descriptors." In 32-bit protected mode, each gate descriptor is 8 bytes, the same size as descriptors in the GDT. The base address of the IDT is stored in a register called the IDTR (Interrupt descriptor table register).

Gate descriptors in the IDT can be 1 of three types. The task gate, the interrupt gate and trap gate. Focusing on the interrupt and trap gate, a diagram of them can be seen here:

Trap and Interrupt descriptor

A gate descriptor contains the information needed to reach the interrupt handler's code. You can see bytes 2 and 3 in both contain a segment selector, which is the selector of the handler's code. The offset of the handler's entry point within the code segment given by the segment selector; as we can see, this is divided into parts like descriptors in the GDT are.

The least significant nibble of byte 4 is reserved, and the most significant nibble of byte 4 contains the gate type and other attributes. When the present flag (P flag) is 0, the gate is not present, and 1 means the inverse. The DPL specifies the privilege level required to use the gate from software.

The D flag specifies the operand size use when entering the handler. When D = 1, the handler uses a 32-bit operand size, and when D = 0, it uses a 16-bit operand size. 32 bits should always be used in protected mode. There's also the gate type field, which is right next to the D flag; when this is 0, it is an interrupt gate; when it is 1, it is a trap gate, which can be seen in the respective diagrams.

The difference between interrupt and trap gates is that when an interrupt gate is used, the processor clears the interrupt flag, disabling normal hardware interrupts until the flag is set again. There are exceptions though; one of these is an interrupt known as "non-maskable interrupts" (NMI) will pause execution of an interrupt even if it is caused by an interrupt gate. NMIs can be generated by hardware for events that cannot be stopped by the interrupt flag.

Service routines defined by a trap gate and can be interrupted by hardware interrupts, whereas interrupt gates disable hardware interrupts.

Hardware interrupts can also be disabled by code using the cli (clear interrupt flag) instruction. They can be enabled again using the sti (set interrupt flag) instruction. Both of these manipulate the interrupt flag, a part of EFLAGS.

It's also worth knowing that the interrupt number is simply the index of the interrupt entry itself, and in protected mode, interrupt vectors 0-31 are reserved for processor-defined exceptions and other purposes. The remaining vectors can be assigned by the operating system or hardware interrupt controllers.

The IDT register

We have the ability to tell the processor where the IDT resides in memory; this is done by the lidt (load IDT) instruction; this works similarly to the lgdt instruction: it loads the IDT pseudo-descriptor from the operand into the IDTR register, which is then used to locate the IDT. The structure of the IDTR is exactly the same as the GDTR, so we would use this in the same way as we used lgdt.

And that covers just about all the theory we need to know for now, what a relief, I bet you're happy to begin coding again because I certainly am.