Before we move on with our kernel development, we must talk about an important concept in operating systems: interrupts. When the CPU executes a program in memory, it will take an instruction, execute it, take the next instruction, execute that, and continues without ever stopping. It isn’t distracted by anything that happens in the wider world.
Sometimes, though, it’s necessary to tell the CPU to stop executing a program, just for a moment, and direct its attention elsewhere. Perhaps some piece of hardware needs servicing: a user pressed a button on the keyboard or moved the mouse; a network request has completed; a disk read operation failed. It’s also possible that we want the CPU to execute more than one program at a time, switching between them every few microseconds. To do that, we set an alarm clock that rings, interrupting the CPU from its work.
In this section, we’ll look at what interrupts are.
This article is part of a series on toy operating system development.
The dedicated CPU
Once you power on the CPU, it is a machine of sheer single mindedness. It operates in a simple loop: read instruction from memory, decode instruction, execute instruction, move pointer to next bit of memory, repeat. It never stops doing this; you’ll have to pull the plug to get it to stop - or feed it an instruction it doesn’t know what to do with. Short of these, nothing will distract it from its work.
Sometimes, though, the world around the CPU needs its attention.
- The CPU is surrounded by hardware that it has no knowledge about. The CPU doesn’t know what a keyboard is, or a mouse, or a disk drive. When you press a key on the keyboard, then this has no meaning for the CPU; it continues executing instructions. What’s needed is for the CPU to notice that something in its environment happened, and needs attention.
- Operating systems execute multiple processes at the same time. They do this by having the CPU execute some instructions from process A, then stop it, and set it work on process B. The CPU is moved from process to process every few microseconds - a concept known as scheduling. What’s needed is a way to set an alarm clock; when it rings, the CPU has to move to the next process. The process switching happens so frequently that to the user it appears that the CPU is executing all processes simultaneously.
- It’s possible for the CPU to encounter an error. It may, for example, try to divide by zero. That’s not the CPU’s fault; it is the program that it’s executing that made it attempt the division. Dividing by zero is impossible, and sends the CPU screaming. But who does it scream to? Itself, actually: it needs a way to stop attempting to divide and execute some other code that allows it to recover from the error.
Interrupting the CPU
The way to get the CPU’s attention is by interrupting it. When something of note happens to a piece of hardware, that hardware talks to a special chip known as the Programmable Interrupt Controller (PIC), sending it an Interrupt Request (IRQ). The PIC then communicates this request to the CPU. Receiving this interrupt, the CPU then stops doing what is was doing, acknowledges receipt to the PIC which in turns sends a vector (a number: one value for the keyboard, another value for the mouse, and so on). The CPU then executes a special bit of code to service the interrupt, appropriately known as an interrupt service routine. When it’s done, the CPU goes back to what it was doing before it was interrupted.

This Intel 8259 Programmable Interrupt Controller can be yours for $10.95
Note: in modern computers the 8259 PIC has given way to the APIC (“Advanced”): a local APIC on each CPU core, plus one or more I/O APICs for the devices. PCIe devices go further still, posting their interrupts as writes to memory (while PICs use a controller pin). For the theory in this post, none of this makes any difference.
The second use case - an alarm clock that interrupts the CPU so that it can tend to the next process that’s scheduled - works in a similar way. The kernel programs a special chip to fire off an interrupt every few microseconds, the CPU gets interrupted, and the interrupt service routine is written to call the kernel again, which will move some registers around so that the CPU will start executing the next process.
There are a number of errors that can happen during program execution: divide-by-zero is the typical example, but other things include an invalid opcode that the CPU doesn’t know what to do with, a stack overflow, or reading or writing to a region of memory where a process isn’t allowed. These will need dealing with by an operating system kernel: retry, or halt the offending process. On this occasion it is the CPU that fires off an interrupt, interrupting itself, so that an interrupt service routine (installed by the kernel) can run in order to put things right.
There’s a fourth use case still: a user program needing to talk to the kernel, in order to request a service (a software interrupt). The kernel knows how to speak to hardware on the program’s behalf, for example, and it can execute code at a higher privilege level than the user program. In previous chapters, we’ve already used the INT assembly instruction to call the BIOS. We’ve asked it to print a character to the screen, wait for a keypress, and produce a memory map. The difference is that we’ve moved to protected mode, and talking to the BIOS is out the window - we’ll have to provide all of these services ourselves, but more on that later.
The interrupt service routine
Whenever the CPU gets interrupted, then, it will execute an interrupt service routine. As we’ve seen, this can be the result of:
- Hardware interruption of the CPU (this includes the scheduling alarm clock)
- A program execution error
- A program executing the
INTinstruction
Each interrupt has a number. That number is an index into a table sitting somewhere in memory. The table is known as an interrupt vector table: for each interrupt, it contains the memory address of its interrupt service routine.
Note: the shape of the interrupt vector table has changed over the years, but the idea behind it hasn’t. A real-mode CPU has 256 entries of four bytes each, sitting at address 0x0, and each one is just a segment + offset. A protected-mode CPU has 256 gate descriptors instead, each containing more than just an address. Additionally, the modern table can live anywhere in memory, and we use
LIDTto tell the CPU where (which we’ve done before).
The interrupt service routine is a piece of code which can do whatever it wants, subject to two constraints.
The routine must leave the machine exactly as it found it: every register it touches has to be saved and put back, because the interrupted program doesn’t kow ut was interrupted and will carry on using those registers as though nothing had happened.
An interrupt arrives between two arbitrary instructions, so an ISR may well have interrupted code that was halfway through updating a data structure. It cannot assume that anything it looks at is in a consistent state, and it must never wait for a lock that the interrupted code might already be holding - that code is not running, and will not release the lock until the ISR returns. A routine which can safely be entered while an earlier invocation of it is still in progress is called reentrant.
An ISR is special in one other way: while an ordinary subroutine returns with RET, an ISR uses IRET. This is because the CPU, when it gets interrupted, leaves a return address on the stack automatically and pushes the flags; IRET pops them off again. This is important because the interrupted program’s carry and zero flags have to survive, as does the interrupt-enable state.
The return address is a little bit special, and is different for three kinds of interrupts:
- For a fault, the saved address points at the offending instruction, so that it can be retried once the condition has been fixed. A page fault is the standard example: map the missing page,
IRET, and the instruction now succeeds. - For a trap, the saved address points after the instruction. Breakpoints, overflow, and the software
INTinstruction all behave this way. - For an abort, there is no reliable address at all. A double fault is an abort: there is nothing sensible left to return to.
The barest (and not useful) implementation for interrupts is therefore a table of pointers, all the same, and at that pointer lives the IRET instruction. Or HLT, in case of an error.
Note: some of the error interrupts push an extra word onto the stack describing what went wrong, so this word must be removed before
IRETcan return to the correct place.
Interrupt masking
There’s a detail worth mentioning at this point: the CPU has special instructions allowing it to ignore interrupts. This is known as interrupt masking, and it’s the equivalent of the CPU inserting a finger in each ear and going “LA-LA-LA” until the masking is turned off. The CLI instruction turns off interrupts, and STI turns them back on again; instructions we have already used in the boot loader.
Interrupts are also masked automatically while an interrupt service routine is running, otherwise an interrupt could interrupt an interrupt and most of the time, we won’t have that. In protected mode this turns out to be a property of the table entry rather than of interrupts in general: an entry can be marked either to mask interrupts on the way in or to leave them alone.
Not everything can be masked, though. Some events are too serious to be ignored at the CPU’s convenience (imminent power loss, or a memory parity error) and these arrive as a non-maskable interrupt, which CLI cannot switch off. When one of those turns up, something is wrong with the machine rather than with the program it’s running.
Summary
In this article, we’ve explored what interrupts are: a way for either software, hardware, or a program error to make the CPU stop executing program code and jump to an interrupt service routine, returning to its original job when it’s done. When we first enter protected mode, there aren’t any interrupt service routines yet, so the CPU has no idea what to do when an interrupt is fired. In the next article, we’ll see how to set up an interrupt vector table.