What are Interrupts?
An interrupt is a signal sent by a hardware device or a software instruction that temporarily halts the CPU's current activities and transfers control to a special piece of code known as an interrupt handler. Interrupts are used to handle events that require immediate attention, such as I/O operations, timer events, and hardware errors.
An interrupt is like a function call to the operating system, but it's initiated by hardware or software rather than by the program itself. When an interrupt occurs, the CPU transfers control to a specific interrupt handler, which is a piece of code responsible for handling the interrupt. This handler can then perform any necessary actions, such as servicing an I/O request or responding to a hardware event.
Types of Interrupts
There are several types of interrupts, including:
1 Hardware Interrupts: These interrupts are generated by hardware devices such as keyboards, mice, disks, and network interfaces to signal events that require the CPU's attention. Hardware interrupts are further divided into:
- Maskable Interrupts: These interrupts can be disabled or enabled by the CPU. Examples include I/O interrupts and timer interrupts.
- Non-maskable Interrupts (NMI): These interrupts cannot be disabled by the CPU and are typically used for critical system events such as hardware errors.
2 Software Interrupts: These interrupts are generated by software instructions and are used to request specific services from the operating system. Software interrupts are often used for system calls and other privileged operations.
How Interrupts Work
When an interrupt occurs, the CPU performs the following steps:
1 Save the Current State: The CPU saves the current state of the program by pushing the values of the program counter (PC), flags register, and other relevant registers onto the stack.
2 Lookup the Interrupt Handler: The CPU looks up the address of the interrupt handler in the Interrupt Descriptor Table (IDT), which contains a list of interrupt vectors and their corresponding handler addresses.
3 Transfer Control to the Interrupt Handler: The CPU transfers control to the interrupt handler by setting the program counter (PC) to the address of the interrupt handler.
4 Execute the Interrupt Handler: The interrupt handler executes, performing whatever actions are necessary to handle the interrupt.
5 Restore the Previous State: After the interrupt handler completes execution, the CPU restores the previous state of the program by popping the saved values from the stack.
6 Resume Execution: The CPU resumes execution of the interrupted program.
Calling an Interrupt in Real Mode
Up to now, we have called interrupts in the real mode using BIOS.
; Print 'A' to the screen using BIOS interrupt 0x10
mov ah, 0x0E ; Function code for teletype output
mov al, 'A' ; Character to print
mov bh, 0 ; Page number (set to 0 for text mode)
mov bl, 0x07 ; Text attribute (white on black)
int 0x10 ; Call interrupt 0x10
In the BIOS, interrupt 0x10
serves multiple functions, each indicated by a value in register AH before calling the interrupt. For instance, function 0x0E
writes a character to the screen ("video teletype output"). This function requires two additional arguments: the character to be written in register AL and the attribute (text color) in register BX
.
When interrupt 0x10
is called, the CPU transfers control to the 0x10
interrupt handler in the BIOS. This handler typically contains a switch statement to execute the appropriate function based on the value of register AH
.
Interrupt calls may return values, which are placed in specific registers according to the function's specification. For example, function 0x08
reads a character and its attribute from the screen at the current cursor position. The resulting character is placed in register AL
, and the color in register AH
. Different interrupts may use different registers and return values, following their respective function specifications. Essentially, making an interrupt call is much like calling a function.
Interrupts in Protected Mode
In protected mode, interrupts function similarly to real mode. However, there are several differences in how they are handled by the CPU and the system.
In protected mode, the Interrupt Descriptor Table (IDT) replaces the interrupt vector table. Entries in the IDT no longer contain segmented real-mode addresses but selectors, similar to those in the Global Descriptor Table (GDT). Unlike real mode, there is no default table provided by the BIOS. In protected mode, the responsibility of setting up interrupt handlers falls on the operating system kernel. Therefore, we need to provide our own Interrupt Descriptor Table (IDT).
When an interrupt occurs in protected mode, the CPU does not simply transfer control to the interrupt handler code. Instead, it changes the privilege level to match the level specified in the interrupt descriptor for that interrupt. This is because user processes run with a lower privilege level, and interrupts are used to communicate with the kernel, which runs at a higher privilege level. After the interrupt is serviced, the CPU lowers the privilege level again and returns control to the user process code.
Steps in Protected Mode
In addition to user code and hardware, the CPU itself can also trigger interrupts.
CPU Interrupts
Errors can occur while programs are running. In real mode, if a program wrote to memory it shouldn't, the computer would likely crash. However, in protected mode, the CPU protects memory and steps in when errors occur.
When a user program makes a mistake, the CPU detects it and triggers an interrupt. Then, it's up to the kernel to handle the situation. Usually, the kernel will stop the problematic user program. (If the issue is within the kernel itself, the outcome is uncertain.)
Memory access errors aren't the only problems that can happen. For instance, dividing by zero or calling an interrupt without a handler can also cause issues.
Some common CPU interrupts include:
- Divide Error (0): Occurs when the DIV or IDIV instructions are executed with a divisor of 0.
- Debug Exception (1): Reserved for debugging purposes.
- NMI (Non-Maskable Interrupt) (2): Non-maskable interrupt, cannot be ignored or disabled.
- Breakpoint (3): Occurs when the INT3 instruction is executed.
- Overflow (4): Occurs when the INTO instruction is executed and the overflow flag is set.
- BOUND Range Exceeded (5): Occurs when the BOUND instruction is executed and the operand exceeds the specified bounds.
- Invalid Opcode (6): Occurs when the CPU encounters an invalid or undefined opcode.
- Device Not Available (7): Occurs when the LOCK prefix is used with an instruction that cannot be locked.
- Double Fault (8): Occurs when an exception occurs during the handling of another exception.
- Coprocessor Segment Overrun (9): Occurs when a floating-point instruction is executed while the EM (emulation) bit in the CR0 register is set.
- Invalid TSS (10): Occurs when the task state segment (TSS) specified in a task switch is not valid.
- Segment Not Present (11): Occurs when a segment is referenced that is not present in memory.
- Stack-Segment Fault (12): Occurs when the SS register is loaded with an invalid segment selector.
- General Protection Fault (13): Occurs when a memory access violates the protection mechanism of the CPU.
- Page Fault (14): Occurs when a page table entry is not present in memory or the access violates the paging protection mechanism.
- Reserved (15): Reserved by Intel.
- x87 FPU Floating-Point Error (16): Occurs when an x87 floating-point error is detected.
- Alignment Check (17): Occurs when misaligned memory access is detected.
- Machine Check (18): Occurs when the CPU detects an internal error.
- SIMD Floating-Point Exception (19): Occurs when a SIMD floating-point exception is detected.
What is the IDT?
The IDT is a data structure used by the CPU to determine how to handle various interrupts, exceptions, and traps. It is similar in concept to the Global Descriptor Table (GDT), which is used for managing memory segmentation.
The IDT contains a list of descriptors, each of which provides information about how to handle a specific interrupt or exception. Each entry in the IDT is 8 bytes long and contains information such as the address of the interrupt handler, the privilege level at which the interrupt handler should run, and other attributes.
Why is the IDT important?
The IDT is crucial for managing interrupts, exceptions, and traps in an operating system. When an interrupt or exception occurs, the CPU looks up the appropriate entry in the IDT to find the address of the corresponding interrupt handler.
For example, when a divide-by-zero error occurs, the CPU consults the IDT to find the address of the divide-by-zero interrupt handler. The CPU then transfers control to the interrupt handler, allowing the operating system to handle the error.
Interrupt Descriptor Table (IDT) Structure
In x86 architecture, the Interrupt Descriptor Table (IDT) is a data structure used to manage interrupt handlers. It is similar to the Global Descriptor Table (GDT), but it contains interrupt gate descriptors instead of segment descriptors.
The IDT is an array of 8-byte entries, each describing an interrupt or exception handler. Each entry in the IDT contains the following fields:
- Offset: The offset field contains the address of the interrupt handler.
- Selector: The selector field contains the segment selector for the interrupt handler.
- Type: The type field contains flags that specify the type of interrupt or exception and the privilege level at which the interrupt handler should run.
Offset | Size | Name | Description |
0 | 2 bytes | Offset (low) | Low 2 bytes of interrupt handler offset |
2 | 2 bytes | Selector | A code segment selector in the GDT |
4 | 1 byte | Zero | Zero - unused |
5 | 1 byte | Type/attributes | Type and attributes of the descriptor |
6 | 2 bytes | Offset (high) | High 2 bytes of interrupt handler offset |
Noticed, A descriptor includes a selector from the GDT, so the IDT depends on the GDT. In the GDT, we must define a code segment (which we did in previous chapters) where the interrupt handlers will be stored. The interrupt descriptor then includes an offset to where the handler code begins.
The descriptor also defines an type/attribute byte with a number of flags:
Bits | Code | Description |
0…3 | Gate type | IDT gate type |
4 | Storage Segment | Must be set to 0 for interrupt gates. |
5..6 | Descriptor privilege level | Gate call protection. This defines the minimum privilege level the calling code must have. This way, user code may not be able to call some interrupts. |
7 | Present | For unused interrupts, this is set to 0. Normally, it's 1. |
Implementing the IDT
We don’t actually need to implement any interrupt handlers in our second-stage boot loader. We will need to reserve enough memory to hold 256 interrupts (which is 2,048 bytes) and fill it all with zeroes. Then we tell the CPU where this table is, because only then will it allow us to switch to protected mode. The real IDT will defined by our kernel – and from C code, so much nicer – once it’s running.
In order to let the CPU know where the IDT is, we use an IDT pointer. This is the exact same structure we used for the GDT pointer:
Field | Size | Description |
Size | 2 bytes | Number of bytes (not entries) in the interrupt descriptor table, NOT minus one |
Offset | 4 bytes | Linear address of interrupt descriptor table |