Add pic8259 Crate for Hardware Interrupt Handling

Mục tiêu: Integrate the pic8259 Rust crate into the kernel project by adding it as a dependency in the Cargo.toml file. This crate provides a safe abstraction layer for controlling the Programmable Interrupt Controller (PIC), which is essential for managing asynchronous hardware interrupts.


In the previous major step, you built an incredibly robust foundation for your kernel by implementing a system to handle CPU exceptions. You successfully set up an Interrupt Descriptor Table (IDT) and created handlers for both benign (Breakpoint) and critical (Double Fault) exceptions. You now have a kernel that can react gracefully to internal errors instead of silently crashing. This work centered on exceptions: synchronous events that are a direct result of the code the CPU is executing.

Now, we pivot to the other side of the interrupt coin: hardware interrupts. Unlike exceptions, these are asynchronous events triggered by external hardware devices. A key press, a tick from the system timer, or a packet arriving from a network card are all examples of hardware interrupts. They can happen at any time, completely independent of the code being executed.

To handle these external signals, the CPU needs help. It can’t listen to every single hardware device at once. This is where a crucial piece of legacy hardware comes into play: the Programmable Interrupt Controller (PIC), specifically the Intel 8259. The PIC acts as a manager or router for hardware interrupts. It gathers interrupt requests from multiple devices, prioritizes them, and then signals the CPU when an interrupt is ready to be processed.

Programming the PIC directly is a complex and error-prone task. It involves sending a precise sequence of bytes to specific I/O ports, a very low-level form of communication. Just as we used the x86_64 crate to safely abstract the complexities of the IDT, we will use a dedicated crate to provide a safe, high-level interface for the PIC. This brings us to our current task: adding this essential dependency to our project.

Introducing the pic8259 Crate

The pic8259 crate is a small, no_std compatible library that does one thing and does it well: it provides safe abstractions for controlling the 8259 PIC. By using this crate, we can avoid writing a lot of unsafe code involving I/O port operations and focus on the logic of handling the interrupts themselves. The crate will handle the correct initialization sequence and the commands for acknowledging interrupts, which we’ll need later.

Updating Your Dependencies

Adding this dependency is a simple modification to your Cargo.toml file. Cargo, Rust’s package manager, will handle the rest.

Open your Cargo.toml file and add the highlighted line to your [dependencies] section.

[dependencies]
spin = "0.5.2"
lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
x86_64 = "0.14.10"
pic8259 = "0.10.1"

This line tells Cargo that your project now depends on the pic8259 crate, specifying version 0.10.1. Using a specific version ensures that your build is reproducible and won’t be broken by future updates to the crate. The next time you build your project, Cargo will automatically find this crate on crates.io (the official Rust package repository), download it, and compile it along with your kernel.

Verifying the Addition

With the dependency added, you can confirm that everything is integrated correctly by running a build.

cargo build

You should see Cargo downloading and compiling the pic8259 crate in your terminal. The build should complete successfully without any errors.

If you run your kernel now with cargo run, you will see no change in behavior. If you left the double-fault test from the previous step, it will still trigger a double fault. If you removed it, it will boot and halt as before. This is the expected and correct outcome. We have added a new tool to our workshop but haven’t used it yet.

You have now successfully equipped your kernel with the necessary library to begin communicating with the hardware that manages interrupts.

What’s Next?

Our path to handling hardware interrupts requires one more piece of crucial knowledge. On the original IBM PC architecture (which our modern systems still emulate for backward compatibility), there wasn’t just one PIC; there were two, chained together in what’s known as a master/slave configuration. Understanding how these two chips work together is essential for initializing them correctly. Your next task will be to dive into the theory of this configuration to prepare for writing the initialization code.

Further Reading

To learn more about the PIC and the concepts introduced in this task, these resources are highly recommended:

  • OSDev.org Wiki: 8259 PIC: The definitive, deep-dive technical resource on how the 8259 PIC works at the hardware level. This is an essential read for any OS developer.
  • The pic8259 Crate Documentation: The official documentation on docs.rs. It’s a great reference for the functions and types the crate provides, which we will be using in the upcoming tasks.
  • I/O Ports: A short OSDev.org article explaining the concept of I/O ports, the low-level mechanism the pic8259 crate uses internally to communicate with the hardware.

Research the master/slave configuration of the dual PIC 8259 setup.

Mục tiêu:


Excellent! In the previous task, you equipped our kernel with the pic8259 crate, giving us the necessary software tools to communicate with the hardware that manages external interrupts. However, before we can use these tools to write any code, we must first understand the architecture of the hardware we are about to control.

This brings us to a crucial piece of computer architecture history that is still relevant today: the master/slave configuration of the dual 8259 Programmable Interrupt Controllers (PICs). This is not a code-writing task; it’s a knowledge-gathering one. A deep understanding of this concept is essential to correctly initialize the hardware and prevent maddeningly difficult bugs.

The Problem: Not Enough Interrupt Lines

The original IBM PC was designed around the Intel 8088 CPU and included a single Intel 8259A PIC chip. This chip is a marvel of engineering that acts as an interrupt router. It has eight input lines, known as Interrupt Request (IRQ) lines, labeled IRQ 0 through IRQ 7. Various hardware devices (like the timer, keyboard, and floppy drive) were connected to these lines. When a device needed the CPU’s attention, it would raise its IRQ line. The PIC would then signal the CPU, which would stop what it was doing to handle the request.

This worked well, but it quickly became apparent that eight IRQ lines were not enough. As more hardware was added—sound cards, mice, hard drive controllers—the system ran out of available interrupt lines.

The Solution: Chaining PICs in a Master/Slave Configuration

Instead of a complete redesign, a clever and backward-compatible solution was devised for the IBM PC/AT architecture: add a second PIC chip and chain them together. This setup is known as a master/slave or cascaded configuration.

Here’s how it works:

  • The Master PIC: This is the primary controller. Its interrupt output pin is connected directly to the CPU’s single INTR (Interrupt Request) pin. It manages its original eight lines, IRQ 0 through IRQ 7.
  • The Slave PIC: This is a secondary controller. Its interrupt output is not connected to the CPU. Instead, it is connected to one of the Master PIC’s input lines—specifically, IRQ 2. It provides an additional eight interrupt lines, IRQ 8 through IRQ 15.

This creates a two-tiered hierarchy:

  1. When a device connected to the Slave PIC (e.g., a PS/2 mouse on IRQ 12) needs attention, it signals the Slave.
  2. The Slave PIC, in turn, signals its interrupt output, which raises the IRQ 2 line on the Master PIC.
  3. The Master PIC sees an interrupt on its IRQ 2 line and signals the CPU.
  4. The CPU acknowledges the interrupt and asks the Master PIC for the interrupt vector number.
  5. The Master PIC knows the interrupt came from the Slave (because it was on IRQ 2), so it communicates with the Slave, which then provides the correct vector number for its device (the one for IRQ 12).
  6. The CPU receives the final vector number and jumps to the appropriate handler in the Interrupt Descriptor Table (IDT).

This clever arrangement expanded the number of available interrupt lines from 8 to 15 (as IRQ 2 on the master is now used for the cascade).

The Critical Flaw: Default Vector Conflicts

Here we arrive at the most important part of this research. By default, after a system boot, the PICs are configured in a way that is completely incompatible with a modern protected-mode operating system.

  • Default Master PIC Mapping: Maps IRQ 0-7 to interrupt vectors 8-15.
  • Default Slave PIC Mapping: Maps IRQ 8-15 to interrupt vectors 8-15.

This presents two catastrophic problems:

  1. Direct Conflict: Both PICs are trying to use the same vector range, which is unworkable.
  2. Overlap with CPU Exceptions: As you learned in the previous step, the CPU reserves vectors 0-31 for its own internal exceptions. The PICs’ default range (8-15) directly overlaps with critical CPU exceptions, including the Double Fault (vector 8), General Protection Fault (vector 13), and Page Fault (vector 14).

If we left this configuration as is, the first time the hardware timer (IRQ 0) fired, the Master PIC would send vector 8 to the CPU. The CPU would then mistakenly execute our double_fault_handler, assuming a critical system error had occurred when, in fact, it was just a normal timer tick. This would immediately and incorrectly halt our kernel.

The Solution: Remapping the PICs

The “P” in PIC stands for Programmable. We can, and absolutely must, send a sequence of commands to both PICs to reconfigure them. This process is called remapping. Our goal is to tell the PICs to map their interrupts to a new, safe range of vectors that does not conflict with the CPU exceptions. A common and safe choice is to map IRQs 0-15 to vectors 32-47.

This remapping is the primary goal of the PIC initialization code we are about to write. The pic8259 crate we added will provide safe functions to perform this complex initialization sequence.

You now have the theoretical knowledge required to proceed. You understand why there are two PICs, how they work together, and, most importantly, the critical need to remap their interrupt vectors to avoid conflicts with CPU exceptions.

In the next task, we will apply this knowledge by defining the new starting vector offsets for the master and slave PICs in our code, taking the first concrete step toward writing our initialization function.

Further Reading

To solidify your understanding, it is highly recommended to review these resources. They provide the low-level details that our abstraction crate will be handling for us.

Define PIC Interrupt Vector Offsets

Mục tiêu: Define public constants in Rust for the new Master and Slave PIC interrupt vector offsets. This is the first step in remapping the PICs to avoid conflicts with CPU exceptions.


In the previous task, you did the essential research to understand the “why” behind Programmable Interrupt Controller (PIC) configuration. You learned about the legacy master/slave setup and, most critically, discovered the dangerous overlap between the PICs’ default interrupt vectors (8-15) and the CPU’s reserved exception vectors (0-31). This knowledge is the foundation for everything we do in this step.

Now, it’s time to translate that theoretical understanding into the first lines of code. Our current task is to formally define the new, safe interrupt vector offsets that we will use to remap the PICs. This act of defining constants is the first concrete step toward building our PIC initialization routine.

From Theory to Constants

The goal of remapping is to tell each PIC a new starting vector number, or offset, for its range of interrupts. * The Master PIC handles hardware Interrupt Request lines (IRQs) 0 through 7. We will tell it to map these IRQs to the vector range MASTER_OFFSET through MASTER_OFFSET + 7. * The Slave PIC handles IRQs 8 through 15. We will tell it to map these IRQs to the vector range SLAVE_OFFSET through SLAVE_OFFSET + 7.

Based on our research, the first 32 interrupt vectors (0-31) are reserved by the CPU for exceptions. Therefore, the first safe, available vector is 32. This is the standard and recommended offset for the Master PIC. Consequently, the Slave PIC’s offset must immediately follow the Master’s range. Since the Master uses 8 vectors (32-39), the Slave’s offset will be 32 + 8 = 40.

Let’s codify this decision by adding two constants to our interrupts module. These constants will serve as the single source of truth for our interrupt vector mapping, preventing “magic numbers” in our code and making it much more readable and maintainable.

Open your src/interrupts.rs file and add the following two pub const definitions at the top of the file.

// In src/interrupts.rs

pub const PIC_1_OFFSET: u8 = 32;
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;

use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
// ... rest of the file

Let’s analyze these new lines in detail:

  • pub const PIC_1_OFFSET: u8 = 32;

    • pub: We declare these constants as public (pub) because we will need to access them from outside this module later. Specifically, when we add handlers for the keyboard and timer to our IDT, we will need to know their exact vector numbers, which will be calculated using these offsets.
    • const: This keyword defines a value that is known at compile time and is inlined wherever it’s used. This is perfect for fixed values like our offsets.
    • PIC_1_OFFSET: A clear, descriptive name for the Master PIC’s offset.
    • u8: The type is an 8-bit unsigned integer. This is the ideal type because interrupt vectors are numbers in the range 0-255, which fits perfectly within a u8.
    • 32: The value we chose, safely positioned immediately after the 32 CPU exception vectors.
  • pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;

    • This defines the offset for the Slave PIC.
    • Instead of hardcoding 40, we calculate it based on PIC_1_OFFSET. This is a fantastic practice. If we ever decided to change the master offset (for instance, to 48), the slave offset would automatically update to 56, preventing a whole class of potential bugs.

Verifying Our Progress

This task was purely about definition. We’ve added crucial constants to our code but haven’t used them yet. As such, if you run your kernel now, you should see no change in its behavior.

cargo run

A successful compilation is the sign of success for this task. It confirms that you have correctly defined the constants that will guide our PIC initialization logic.

You have now laid the very first stone in the foundation of our hardware interrupt handling system. With these clear, well-defined offsets in place, we are perfectly prepared for the next task: creating the initialization function that will use the pic8259 crate and these constants to safely configure and remap both PICs.

Further Reading

Configure and Initialize the PIC 8259

Mục tiêu: Create a global, mutex-guarded handle for the chained PICs and write an initialization function that uses the pic8259 crate to remap hardware interrupts to safe vector offsets.


With your crucial research complete and the new, safe vector offsets (PIC_1_OFFSET and PIC_2_OFFSET) defined, you have the “what” and the “why” of PIC remapping. You know what offsets to use and why it’s so critical to move them away from the CPU’s reserved exception range.

Now, we move on to the “how.” Our current task is to write the initialization function that takes our defined offsets and uses the pic8259 crate to send the correct command sequence to the hardware. This will involve creating a global, synchronized handle to the PICs and then calling the crate’s high-level initialization routine.

A Global Handle to Hardware: The PICS Static

Just like our VGA buffer and our Interrupt Descriptor Table (IDT), the pair of PICs is a unique, global hardware resource. We need a single, globally accessible instance to represent them. Following the robust pattern we’ve already established, we will use a spin::Mutex to ensure safe, synchronized access and lazy_static! to handle the one-time initialization.

The constructor for the ChainedPics struct from our pic8259 crate is unsafe. This is an important signal from the crate’s author. It tells us that creating this object has the potential to cause undefined behavior if not done correctly. Specifically, it performs I/O port operations that could interfere with other devices if we weren’t careful. Since our kernel is the only software running and in complete control of the hardware, we can confidently create this instance within an unsafe block, knowing that our use case is correct.

Let’s add this static definition to your src/interrupts.rs file.

// In src/interrupts.rs

pub const PIC_1_OFFSET: u8 = 32;
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;

// --- Add the new `use` statements ---
use pic8259::ChainedPics;
use spin;

use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
use lazy_static::lazy_static;
use crate::println;

// --- Add the new `lazy_static!` block for the PICS ---
/// A static, mutex-guarded instance of the two chained PICs.
/// The offsets are the constants we defined above, telling the PICs
/// to map hardware interrupts to vectors 32-47.
pub static PICS: spin::Mutex<ChainedPics> =
    spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });

// ... rest of the file ...

Let’s break down this new static variable: * use pic8259::ChainedPics; and use spin;: We import the necessary types. ChainedPics is the struct from the pic8259 crate that represents the master/slave configuration. * pub static PICS: spin::Mutex<ChainedPics>: We define a pub (public) static variable named PICS. Its type is a Mutex wrapping a ChainedPics instance. We don’t need lazy_static! here because ChainedPics::new is a const function, meaning it can be evaluated at compile time. This is more efficient. * spin::Mutex::new(...): We create a new spinlock to protect access to the PICs. * unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) }: This is the core of the initialization. * The unsafe block is required because we are creating an object that will directly manipulate hardware I/O ports. We are asserting to the compiler that this is safe to do in our bare-metal environment. * We call ChainedPics::new(), passing it the two offset constants we defined in the previous task. This configures the instance with the knowledge of where we want the interrupts to be mapped, but it does not yet send any commands to the hardware.

Creating the Initialization Function

Now that we have a handle to the PICs, we can write the function that performs the actual hardware initialization. The pic8259 crate provides a simple initialize() method that handles the entire complex sequence of sending Initialization Command Words (ICWs) to both the master and slave PICs.

It makes sense to integrate this PIC initialization into our existing interrupt setup logic. Let’s refactor our init_idt function into a more general init function that handles all interrupt-related setup.

Modify your src/interrupts.rs file to rename init_idt to init and add the PIC initialization call.

// In src/interrupts.rs

// ... (constants, use statements, PICS static, IDT lazy_static) ...

/// Initializes both the IDT and the PICs.
pub fn init() {
    // Initialize the PICs. This remaps them to our chosen offsets.
    // The `initialize` function performs the full, complex initialization sequence.
    unsafe {
        PICS.lock().initialize();
    }
    // Load our custom IDT.
    IDT.load();
}

// ... (handler functions) ...

And, of course, we must update the call site in src/main.rs.

// In src/main.rs

// ...

#[no_mangle]
pub extern "C" fn _start() -> ! {
    println!("Hello, World!");

    // Initialize our interrupt handling systems (PICs and IDT).
    crate::interrupts::init(); // <-- Update this function call

    // ... rest of the function ...
}

Let’s review the changes to the init function: * pub fn init(): We’ve renamed the function to reflect its broader responsibility. It’s now the single entry point for setting up all interrupt hardware. * unsafe { PICS.lock().initialize(); }: * We lock our global PICS mutex to get exclusive access. * We call the initialize() method. This is the moment the magic happens. The crate now sends a sequence of commands to I/O ports 0x20, 0x21, 0xA0, and 0xA1 to configure the PICs. It uses the offsets we provided earlier to remap IRQs 0-15 to vectors 32-47. * The initialize method is also unsafe because it can cause race conditions if interrupts are enabled while it’s running. We will enable them in a later step, so this is safe for now. * IDT.load(): After the PICs are configured, we proceed to load our IDT as before.

What to Expect

You have now written the code that correctly configures and remaps the legacy interrupt controllers. However, if you run the kernel now, you will see absolutely no change in behavior.

cargo run

The kernel will boot, print its message, and halt (or double fault if you left the test code in). This is the correct and expected outcome. A successful compilation and run is the sign of success for this task.

We have prepared the interrupt router (the PICs) to forward hardware interrupts to the correct vectors, but we haven’t yet told the CPU to actually listen for these interrupts. The CPU’s master interrupt flag is still disabled. That is the final piece of the puzzle.

In the very next task, we will execute the special sti (“Set Interrupt Flag”) instruction to enable interrupts on the CPU, officially opening the floodgates for hardware events.

Further Reading

Understanding PIC Initialization and Remapping

Mục tiêu: A deep dive into the initialize() function for the 8259 PIC. This task explains the four-step process using Initialization Command Words (ICWs) to remap hardware interrupts, resolving conflicts with CPU exceptions and setting up the master/slave configuration.


You have made excellent progress. In the last task, you assembled the complete init function, which now stands ready to configure all our interrupt-related hardware. You defined the global PICS static and, most importantly, you added the crucial line: PICS.lock().initialize();.

While you have already written the code, this task is dedicated to deeply understanding what that single, powerful line of code accomplishes. It’s the moment we translate all the theory from our research into direct hardware commands. We are now officially remapping the Programmable Interrupt Controller (PIC) interrupts.

The Problem Revisited: A Dangerous Collision Course

Let’s quickly recap the critical problem we’re solving. By default, after the system boots, the dual 8259 PICs are configured to map hardware interrupts (IRQs 0-15) to interrupt vectors 8-15. This is a catastrophic conflict, as this range is reserved by the CPU for critical exceptions like the Double Fault (vector 8). Without remapping, the first hardware interrupt from the system timer (IRQ 0) would be mistaken for a double fault, incorrectly halting our kernel.

Our init function in src/interrupts.rs is designed to prevent this. Let’s look at it again, focusing on the line that does the work:

// In src/interrupts.rs

// ... (constants, use statements, PICS static, IDT lazy_static) ...

/// Initializes both the IDT and the PICs.
pub fn init() {
    // This `unsafe` block contains the hardware interaction.
    unsafe {
        // THIS IS THE LINE THAT REMAPS THE PICS
        // It takes our chosen offsets (32 and 40) and uses them to
        // configure the master and slave PICs.
        PICS.lock().initialize();
    }
    // Load our custom IDT.
    IDT.load();
}

// ... (handler functions) ...

The call to initialize() is the culmination of our efforts. It seems simple, but it hides a complex, multi-step communication protocol with the PIC hardware. The pic8259 crate is now performing a carefully choreographed sequence on our behalf, sending a series of bytes called Initialization Command Words (ICWs) to the PICs’ command and data I/O ports.

Unpacking initialize(): The Four Command Words

The remapping process requires sending four distinct command words to each of the two PICs. Here is what the initialize() method is doing under the hood:

1. ICW1: Begin Initialization

The very first byte sent to each PIC is ICW1. This command tells the PICs to enter initialization mode and listen for the next three command words. It also informs them about the system configuration—specifically, that they are operating in a “cascaded” or master/slave setup.

2. ICW2: The Vector Offset (The Remap!)

This is the most critical step for our current task. The second byte, ICW2, is where we specify the new vector offset. The pic8259 crate takes the constants you defined earlier and sends them to the hardware: * It sends PIC_1_OFFSET (which is 32) to the Master PIC. The master now knows that IRQs 0-7 should be mapped to interrupt vectors 32-39. * It sends PIC_2_OFFSET (which is 40) to the Slave PIC. The slave now knows that IRQs 8-15 should be mapped to interrupt vectors 40-47.

With this single step, the dangerous collision between hardware interrupts and CPU exceptions is completely resolved.

3. ICW3: Master/Slave Identity

The third command, ICW3, establishes the relationship between the two PICs. * The Master PIC is told that it has a slave connected on its IRQ 2 pin. * The Slave PIC is told its identity—that it is the slave connected to the master’s IRQ 2. This ensures that when an interrupt arrives from a device on the slave (e.g., IRQ 12), the master knows to query the slave for the correct vector number.

4. ICW4: Additional Configuration

The final command, ICW4, provides some additional environmental information, such as telling the PICs we are in “80x86 mode.”

The Power of Abstraction

This four-step sequence for both PICs involves a series of unsafe writes to specific hardware I/O ports. It is a fragile process where the order and timing of commands are critical. The pic8259 crate provides an invaluable abstraction, hiding this complexity behind a single, safe initialize() method. It ensures the remapping is performed correctly every time, allowing us to focus on the higher-level logic of our kernel.

Current State and What’s Next

You have now successfully written and understood the code that configures the hardware interrupt router. The PICs are remapped, and any incoming hardware interrupt will now be correctly forwarded to a safe vector number in the 32-47 range.

If you run your kernel now, there will be no visible change. A successful build is the sign of success for this task. We have correctly configured the hardware, but we haven’t yet told the CPU that it should start paying attention to signals from the PIC. The CPU’s global interrupt enable flag is still turned off by default.

In the final task of this step, we will complete the circuit. You will write the code to execute the sti (“Set Interrupt Flag”) instruction, which finally tells the CPU to start listening for and processing hardware interrupts.

Further Reading

To truly appreciate the abstraction you’re using, it’s enlightening to see the low-level details of what the initialize() method is doing.

  • OSDev.org Wiki: PIC Initialization: This page shows the exact bit-level layout of the ICW commands and the raw I/O port writes required to perform the initialization sequence manually.
  • The initialize function’s source code: For the truly curious, you can look at the source code of the pic8259 crate to see how it implements this logic in Rust. This is a great way to learn how to write low-level hardware abstractions.

Enable Hardware Interrupts on the CPU

Mục tiêu: Enable maskable hardware interrupts on the CPU by setting the interrupt flag (IF) in the RFLAGS register. This is the final step after initializing the PIC and IDT, allowing the kernel to start processing external hardware events.


You have perfectly navigated the intricate world of the Programmable Interrupt Controller (PIC). By creating the init function and calling its initialize() method, you have successfully remapped the hardware interrupt vectors to a safe range (32-47), averting a dangerous collision with the CPU’s critical exception vectors. The entire interrupt routing system is now configured and ready.

However, there is one final, crucial switch to flip. The CPU itself has a master control switch for all maskable hardware interrupts. By default, after a system boot, this switch is in the “off” position. The CPU is effectively “deaf” to any signals the PIC might send. Our current task is to flip this switch to “on,” enabling the CPU to finally listen for and respond to events from the outside world.

The CPU’s Master Switch: The Interrupt Flag

Deep within the CPU’s RFLAGS register lies a single bit known as the Interrupt Flag (IF). This flag acts as a global gatekeeper for all maskable hardware interrupts (the kind routed by the PIC).

  • When IF is 0 (cleared): The CPU will ignore any interrupt signals it receives from the PIC. The hardware can raise its interrupt lines all it wants, but the CPU will not stop its current work to handle them.
  • When IF is 1 (set): The CPU is “listening.” If the PIC signals an interrupt, the CPU will immediately pause its current execution, look up the appropriate handler in our now-active Interrupt Descriptor Table (IDT), and transfer control to it.

The x86_64 architecture provides two special, privileged assembly instructions to manipulate this flag: * sti (Set Interrupt Flag): Sets the IF bit to 1, enabling interrupts. * cli (Clear Interrupt Flag): Clears the IF bit to 0, disabling interrupts.

These instructions are privileged, meaning they can only be executed in kernel mode (Ring 0). This is a vital security feature that prevents user-space applications from disabling interrupts and monopolizing the CPU.

Enabling Interrupts in Rust

Just as we’ve seen with other special instructions, the x86_64 crate provides a safe, idiomatic wrapper for this operation, sparing us from writing unsafe inline assembly. The function is x86_64::instructions::interrupts::enable().

The timing of this call is absolutely critical. We must only enable interrupts after our IDT and PICs have been fully initialized. If we were to enable interrupts first, the very first hardware interrupt (likely from the system timer) would arrive before we have any handlers registered, leading to an immediate double fault. Our current initialization order—init() first, then enable interrupts—is the correct and safe sequence.

Let’s add the code to enable interrupts in our _start function.

// In src/main.rs

// ...

#[no_mangle]
pub extern "C" fn _start() -> ! {
    println!("Hello, World!");

    // Initialize our interrupt handling systems (PICs and IDT).
    crate::interrupts::init();

    // Enable hardware interrupts.
    // After this line, the CPU will start listening for signals
    // from the PICs and dispatching them to our IDT handlers.
    x86_64::instructions::interrupts::enable();

    // The system should not crash here. Instead, it should wait for the
    // next interrupt to arrive.
    println!("It did not crash!");
    loop {}
}

The Expected (and Successful) Crash

With interrupts now enabled, what do we expect to happen when we run the kernel?

  1. The kernel boots and our init() function runs, successfully remapping the PICs. IRQ 0 (the hardware timer) is now mapped to interrupt vector 32.
  2. The interrupts::enable() function executes the sti instruction. The CPU is now listening.
  3. The kernel continues and prints “It did not crash!”.
  4. Within a few milliseconds, the hardware timer “ticks,” firing IRQ 0.
  5. The Master PIC receives this signal and sends interrupt vector 32 to the CPU.
  6. The CPU pauses execution and looks at index 32 of our active IDT.
  7. It finds that the entry is empty. We have not yet defined a handler for the timer interrupt.
  8. This failure to invoke a handler for a hardware interrupt is a critical error, causing the CPU to trigger a Double Fault (exception 8).
  9. The CPU then looks at index 8 of our IDT, finds our trusty double_fault_handler, and executes it.

Therefore, the expected output is our double fault message. This might seem like a failure, but it is a resounding success! It proves that the entire chain is working perfectly: the hardware timer fired, the PIC correctly routed it to vector 32, the CPU listened and tried to find a handler, and our exception-handling safety net caught the resulting error.

Let’s run the code and confirm.

cargo run

You should see output similar to this in the QEMU window:

Hello, World!
It did not crash!
panicked at 'EXCEPTION: DOUBLE FAULT
InterruptStackFrame {
    instruction_pointer: VirtAddr(0x...),
    code_segment: 8,
    cpu_flags: 0x...,
    stack_pointer: VirtAddr(0x...),
    stack_segment: 0,
}', src/interrupts.rs:NN:5

Congratulations! You have successfully configured and enabled hardware interrupts. Your kernel is now fully capable of responding to external hardware events.

What’s Next?

You have now completed all the foundational work for handling hardware interrupts. You’ve proven that the timer interrupt is being delivered to the CPU. The next logical step, and the focus of the next major section of this project, is to stop this from causing a double fault. You will write a proper handler function for the timer interrupt, add it to the IDT at vector 32, and make it do something useful, like printing a character to the screen on every tick. This will be your first step into the world of asynchronous, event-driven programming in your kernel.

Further Reading

To learn more about the concepts covered in this final task, please review these excellent resources: