Before the advent of interrupts in processors, the mechanisms for handling events and managing the flow of operations were significantly cumbersome.
Pre-Interrupt Era:
1️⃣ Polling Method:
One of the primary techniques used before interrupts was polling.
In this method, the CPU would repeatedly check the status of an I/O device at regular intervals to see it it needed attention. This means the CPU was continuously executing a loop to check the status of each peripheral device, wasting valuable processing time that could be used for other tasks.
; Pseudo-code for polling
start:
load status from device
compare status with ready
if not ready, jump to start
; Device is ready, process the data
process data
jump to start
2️⃣ Busy-Waiting:
Similar to polling, busy-waiting involves the CPU waiting in a loop, doing nothing but checking the status of an operation. The CPU stays in a tight loop until the event occurs. This was inefficient because the CPU couldn't perform any other tasks while waiting.
; Pseudo-code for busy-waiting
start:
if device is not ready, jump to start
; Device is ready, process the data
process data
jump to start
3️⃣ Hardware Signaling
Hardware Signals: Some early systems used hardware signals or flags that a program could check to determine the state of an I/O device. The program would manually check these signals at various points during its execution.
Drawbacks of Pre-Interrupt Methods:
- Inefficiency: The CPU spent a significant amount of time in idle loops, checking the status of devices rather than performing useful work.
- Poor Utilization: System resources were poorly utilized because the CPU could not switch to other tasks while waiting for I/O operations to complete.
- Complex Programming: Programs had to be written to manually check the status of devices and handle them appropriately, which increased the complexity of the code.
Introduction of Interrupts
Early computers did not have interrupts. They used polling and busy-waiting to handle input/output operations, which was inefficient and consumed valuable CPU time.
- The
UNIVAC (1951)
is one of the earliest computer known to implement a form of interrupts. It used a simple form of interrupts to handle I/O operations. - The introduction of microprocessors like the Intel 4004 (1971) and the Intel 8080 (1974) included interrupt handling capabilities, making it easier to build more responsive and efficient systems.
- The advent of personal computers in the late 1970s and 1980s, such as the Apple II (1977) and IBM PC (1981), utilized interrupts for managing peripherals and multitasking.
The introduction of interrupts revolutionized how processors handled events and managed resources:
- Improved Efficiency: Interrupts allow the CPU to perform other tasks while waiting for I/O operations to complete, significantly improving overall system efficiency.
- Enhanced Responsiveness: Systems can respond immediately to external events, leading to better performance in real-time applications.
- Asynchronous Event Handling: Interrupts allowed the CPU to be notified immediately when an I/O device was ready, freeing the CPU to perform other tasks instead of waiting.
- Interrupt Service Routines (ISRs): Special functions that were executed in response to specific interrupts, allowing efficient handling of events.
- Multi-tasking: Interrupts enable preemptive multitasking, allowing operating systems to switch between tasks based on priority and ensuring that critical tasks are addressed promptly.
- Error Handling: Interrupts facilitate robust error handling mechanisms, allowing systems to respond to and recover from exceptional conditions effectively.
Now, let's get familiar with the term interrupt
in computing:
An interrupt is a signal sent to the processor by hardware or software to indicate an event that needs immediate attention. It temporarily halts the current instructions, saves the state, and executes a function to handle the event.
Interrupts are sent by a special Hardware.
There are three types of interrupts:
- Hardware Interrupts: Generated by external devices such as keyboards, mice, and hard drives, hardware interrupts reduce the need for the processor to waste time in polling loops waiting for events.
- Software Interrupts: Initiated by software, these interrupts manage system calls, allowing programs to request services from the operating system.
- Exceptions: Triggered by CPU when it encounters an error or unusual event during program execution, exceptions include issues like division by zero or page fault. They signal conditions that the program cannot handle alone and require special processing.
Exceptions:
Exceptions in x86 are internal events that disrupt the normal flow of a program’s execution. They are typically triggered by the CPU when it encounters an error condition, such as a division by zero, an invalid opcode, or a page fault. Unlike external interrupts, which are generated by hardware devices, exceptions are generated by the processor itself.
Types of Exceptions in x86
In the x86 architecture, exceptions are classified into three main types:
- Faults: A fault is an exception that occurs during the execution of an instruction, but before the instruction is completed. The CPU can often correct the error, allowing the instruction to be re-executed. Examples include page faults and general protection faults.
- Traps: A trap is an exception that occurs after an instruction has been executed. Traps are often used for debugging, as they allow the OS to monitor or intervene after specific instructions. An example is the breakpoint exception.
- Aborts: An abort is a severe exception that indicates a critical error that cannot be corrected. Aborts are usually caused by hardware failures or serious inconsistencies in the system, such as a double fault.
x86 Exception Table
Vector | Exception Name | Type | Description |
---|---|---|---|
0x00 | Divide Error | Fault | Occurs when a division by zero or overflow occurs in a DIV or IDIV instruction. |
0x01 | Debug Exception | Fault/Trap | Triggered by conditions such as single-step execution or hardware breakpoints. |
0x02 | Non-Maskable Interrupt (NMI) | Interrupt | A high-priority interrupt that cannot be masked by the CPU, typically used for critical events. |
0x03 | Breakpoint | Trap | Triggered by the INT 3 instruction, used for debugging purposes. |
0x04 | Overflow | Trap | Triggered by the INTO instruction when the overflow flag (OF) is set. |
0x05 | Bound Range Exceeded | Fault | Occurs when the BOUND instruction detects an operand outside the defined bounds. |
0x06 | Invalid Opcode | Fault | Triggered when the CPU encounters an undefined or illegal opcode. |
0x07 | Device Not Available | Fault | Occurs when a floating-point operation is attempted, but the FPU is not available or disabled. |
0x08 | Double Fault | Abort | Occurs when a second exception is encountered while the CPU is processing a previous exception. |
0x09 | Coprocessor Segment Overrun | Fault | Occurs on some older processors when a floating-point instruction causes a memory overrun. |
0x0A | Invalid TSS | Fault | Triggered when the Task State Segment (TSS) is invalid during a task switch. |
0x0B | Segment Not Present | Fault | Occurs when the CPU references a segment that is not present in memory. |
0x0C | Stack-Segment Fault | Fault | Triggered by errors related to the stack, such as stack overflows or invalid stack segment references. |
0x0D | General Protection Fault (GPF) | Fault | A catch-all exception for protection violations, such as accessing restricted memory. |
0x0E | Page Fault | Fault | Occurs when a memory access violates paging rules, such as accessing a non-present page. |
0x0F | (Reserved) | - | Reserved by Intel for future use. |
0x10 | x87 Floating-Point Error | Fault | Triggered by errors in x87 floating-point operations, such as division by zero. |
0x11 | Alignment Check | Fault | Occurs when unaligned memory accesses are detected (enabled only in certain modes). |
0x12 | Machine Check | Abort | Triggered by serious hardware errors, such as memory corruption or CPU issues. |
0x13 | SIMD Floating-Point Exception | Fault | Triggered by errors in SIMD floating-point operations (SSE instructions). |
0x14-0x1F | (Reserved) | - | Reserved by Intel for future use. |
0x20-0xFF | User-Defined Interrupts | - | These vectors are typically used for user-defined interrupts and can be programmed in the IDT. |
⚡ Terms in Interrupt
- Interrupt Service Routine (ISR): A special function that is executed in response to an interrupt. ISRs are designed to handle specific interrupt signals and perform necessary tasks.
- Interrupt Vector Table: A table that holds the addresses of ISRs. When an interrupt occurs, the CPU uses this table to determine which ISR to execute. Each type of interrupt is assigned a specific entry in the table, which the CPU uses to determine which ISR to execute.
- Vector Number: Each interrupt type has a unique vector number that acts as an index into the IVT.
- Address Lookup: When an interrupt occurs, the CPU uses the vector number to look up the ISR address in the IVT.
Interrupt Handling Process
- Interrupt Request
- Generation: An interrupt is generated by a hardware device or software event.
- Hardware: Devices like keyboards, network cards, and timers generate interrupts when they need CPU attention.
- Software: Programs can trigger interrupts using specific instructions (e.g., system calls).
- Generation: An interrupt is generated by a hardware device or software event.
- Interrupt Acknowledgement:
- Detection: The CPU checks for pending interrupts at specific points during its execution cycle.
- Modern CPUs have dedicated hardware, like an Interrupt Controller, to manage and prioritize interrupts.
- Detection: The CPU checks for pending interrupts at specific points during its execution cycle.
- Interrupt Processing:
- Interrupt Handling:
- Saving Context: The CPU saves the current execution context (e.g., program counter, registers) to preserve the state of the current task.
- Disable Lower-Priority Interrupts: Some systems disable lower-priority interrupts to prevent them from interfering with the current ISR.
- Jump to ISR: The CPU fetches the address of the ISR from the Interrupt Vector Table (IVT) and jumps to the ISR.
- Interrupt Handling:
- Executing ISR:
- ISR Execution: The ISR performs the necessary tasks to handle the interrupt. This could involve reading data from an I/O device, updating system states, or handling errors.
- Restoring Context:
- Restore Context: Once the ISR is complete, the CPU restores the saved context, allowing the interrupted task to resume execution as if it was never interrupted.
Example Sequence
- Interrupt Generation:
- A network card receives a packet and generates an interrupt.
- Interrupt Detection:
- The CPU, during its execution cycle, checks the interrupt lines and detects the network card's interrupt.
- Acknowledgment:
- The CPU acknowledges the interrupt and signals the network card to stop sending the interrupt signal.
- Context Saving:
- The CPU saves the current task's context (e.g., register values, program counter).
- ISR Lookup:
- The CPU uses the interrupt vector number to find the ISR address in the IVT.
- ISR Execution:
- The ISR for the network card interrupt reads the packet data and stores it in memory.
- Context Restoration:
- After the ISR completes, the CPU restores the saved context and resumes the interrupted task.
Interrupts in Real Mode
In real mode, interrupts are handled in a straightforward manner, with a focus on simplicity and direct access to hardware resources. Real mode is an operating mode of x86-compatible CPUs that provides direct access to memory and hardware, without the complex features of protected mode such as memory protection and multitasking.
Interrupt Vector Table (IVT):
- The IVT is a table located at the beginning of memory (address 0x0000:0000).
- It consists of 256 entries, each entry being 4 bytes long.
- Each entry contains the segment address of an Interrupt Service Routine (ISR).
- The size of the IVT is 1024 bytes (256 entries * 4 bytes per entry).
🌩️ Interrupt Descriptor Table
The Interrupt Descriptor Table (IDT) is another data structure in the x86 architecture that defines how the processor handles interrupts and exceptions.
The Interrupt Descriptor Table (IDT) is a data structure utilized by the x86 architecture to manage interrupt and exception handling. The IDT allows the processor to determine the appropriate response to various interrupts and exceptions by referencing specific handlers defined within the table.
Each entry in IDT is called an
interrupt descriptor
, which contains information about the interrupt service routine (ISR
) that should be executed when a specific interrupt occurs.
History
The concept of the IDT was introduced with the Intel 80286 processor, which was the first in the x86 family to support protected mode. This mode provided advanced features like hardware-based multitasking, memory protection, and a new method of handling interrupts and exceptions. The 80286 used a similar structure called the Interrupt Vector Table (IVT) in real mode, but the IDT was specifically designed for protected mode with enhanced capabilities.
With the introduction of the Intel 80386 and later processors, the IDT was further refined to support 32-bit protected mode and additional interrupt handling features, making it a cornerstone of modern operating systems.
IRQ
An Interrupt Request (IRQ) is a hardware signal sent to the processor to gain its attention for handling specific events. These events could be anything that requires the processor's immediate attention, such as input from peripherals like keyboards, mice, disk controllers, or other hardware devices.
How IRQs Work
- Hardware Interrupt: When a hardware device needs the processor's attention, it sends an interrupt request (IRQ) to the interrupt controller.
- Interrupt Controller: The interrupt controller prioritizes the interrupts and sends them to the CPU. In x86 systems, this is typically managed by the Programmable Interrupt Controller (PIC) or the more modern Advanced Programmable Interrupt Controller (APIC).
- CPU Interrupt Handling: Upon receiving an IRQ, the CPU temporarily halts its current activities, saves the state of the currently executing task, and executes an Interrupt Service Routine (ISR) specific to the interrupt.
- ISR Execution: The ISR handles the interrupt (e.g., reading input data from a device). After handling the interrupt, the ISR sends an End of Interrupt (EOI) signal to the interrupt controller.
- Return to Normal Operation: The CPU restores the previously saved state and resumes the interrupted task.
Common IRQ Numbers
On the x86 architecture, the original PIC (the Intel 8259A) supports 16 IRQ lines, which are typically mapped as follows:
IRQ Number | Description |
---|---|
0 | Timer |
1 | Keyboard |
2 | Cascade for IRQs 8-15 |
3 | COM2 (serial port) |
4 | COM1 (serial port) |
5 | LPT2 (parallel port) or sound card |
6 | Floppy disk controller |
7 | LPT1 (parallel port) |
8 | Real-time clock (RTC) |
9 | Free (available for peripherals) |
10 | Free (available for peripherals) |
11 | Free (available for peripherals) |
12 | PS/2 mouse |
13 | FPU (floating point unit) or coprocessor |
14 | Primary ATA channel (hard drive) |
15 | Secondary ATA channel (hard drive) |
Interrupts in Protected Mode
In protected mode, we can't use the Interrupt Vector Table (IVT) of real mode. We have to set up and inform the processor about the interrupts in protected mode. This is done using a special register and a special table.
The table in which we store the interrupt number and address of corresponding interrupt handler address is known as IDT (Interrupt Descriptor Table).
We populate this table with the entries of our interrupts.
After populating the IDT, we have to inform the processor about the IDT, for this purpose processor have a special register IDTR
, which stores the address and size of the table. We flush the table to this register with the use of a special instruction known as the LIDT
(which stands for Load Interrupt Descriptor Table)
Setting Up the IDT
🌩️ Structure of the IDT
The IDT is an array of 256 entries
, where each entry (also known as a gate) defines how a specific interrupt or exception should be handled. Each entry in the IDT is 8 bytes
long and contains the following fields:
Bits | Description |
---|---|
0-15 | Offset bits 0-15 (lower 16 bits of the ISR address) |
16-31 | Segment selector (points to a code segment in the GDT or LDT) |
32-39 | Reserved (should be set to 0) |
40-47 | Type and attributes |
48-63 | Offset bits 16-31 (higher 16 bits of the ISR address) |
Detailed Breakdown:
- Offset (0-15 and 48-63 bits): This is the address of the interrupt service routine (ISR) or simply the interrupt handler function. Since the address is split into two parts, the first 16 bits are stored in bits 0-15, and the next 16 bits are stored in bits 48-63.
- Segment Selector (16-31 bits): This is the code segment selector that the CPU should load into the CS (Code Segment) register before jumping to the ISR. It usually points to a segment in the Global Descriptor Table (GDT) or Local Descriptor Table (LDT).
- Reserved (32-39 bits): These bits are reserved and should be set to 0.
- Type and Attributes (40-47 bits): This field defines the type of interrupt gate and various attributes. It includes:
- Gate Type: Specifies whether the entry is an interrupt gate, trap gate, or task gate.
- DPL (Descriptor Privilege Level): Defines the privilege level required for calling the interrupt. DPL can range from 0 (most privileged) to 3 (least privileged).
- P (Present bit): Indicates whether the interrupt descriptor is present (1) or not (0).
struct IDTEntry {
uint16_t offset_low; // Lower 16 bits of the ISR's address
uint16_t selector; // Segment selector
uint8_t zero; // Reserved, set to 0
uint8_t type_attr; // Type and attributes
uint16_t offset_high; // Upper 16 bits of the ISR's address
} __attribute__((packed));
#define IDT_SIZE 256
struct IDTEntry idt[IDT_SIZE]; // Define an IDT with 256 entries
Note:
__attribute__((packed))
is used to ensure that the compiler does not add any padding between the fields of the structure.
Types of Gates
- Interrupt Gate (Type 0xE): When an interrupt gate is used, the processor automatically disables further interrupts by clearing the IF flag in the EFLAGS register upon entering the interrupt handler.
- Trap Gate (Type 0xF): When a trap gate is used, interrupts remain enabled. This is typically used for software exceptions where subsequent interrupts should still be allowed.
- Task Gate (Type 0x5): This gate type is used to perform a task switch by referencing a Task State Segment (TSS). Task gates are less common in modern operating systems but are still part of the architecture.
Initializing IDT
As we already defined the structure of the IDT entry. Next step comes in is to fill the IDT structure with entries. The IDT in x86 could accommodate only 256 entries.
void set_idt_entry(int n, uint32_t isr_address, uint16_t selector, uint8_t type_attr) {
idt[n].offset_low = isr_address & 0xFFFF;
idt[n].selector = selector;
idt[n].zero = 0;
idt[n].type_attr = type_attr;
idt[n].offset_high = (isr_address >> 16) & 0xFFFF;
}
// Example: Setting IDT entries for the first 32 ISRs (CPU exceptions)
for (int i = 0; i < 32; i++) {
set_idt_entry(i, (uint32_t)isr_addresses[i], 0x08, 0x8E); // Assuming code segment selector is 0x08 and type_attr for 32-bit interrupt gate is 0x8E
}
In this example, isr_addresses
is an array of ISR function addresses:
extern void isr0();
extern void isr1();
// ...
extern void isr31();
void* isr_addresses[] = {
isr0, isr1, /* ... */ isr31
};
Now since, our table is loaded fully and it is somewhere in the memory, next we need a way to tell the processor about the table. Processor facilitate us with a special register known as the LIDTR
(Load Interrupt Descriptor Table Register). It is set by the special assembly instruction lidt
.
Defining the IDT Pointer Structure
Now, we define a structure that will store the base address (starting address) of the IDT and its limit (size). It would be of size 48
bits very much similar to GDT Pointer
structure.
16 bits
for the size.32 bits
for the base address.
struct IDTPointer {
uint16_t limit; // Size of the IDT in bytes - 1
uint32_t base; // Base address of the IDT
} __attribute__((packed));
struct IDTPointer idt_ptr;
Fill the IDT Pointer Structure
Next we store the size of IDT base address and its size into the IDT Pointer
.
idt_ptr.limit = (sizeof(struct IDTEntry) * IDT_SIZE) - 1;
idt_ptr.base = (uint32_t)&idt;
Load the IDT Pointer to the IDTR
The last but the not least step is to load the IDT Pointer
structure into the IDTR
.
__asm__ __volatile__("lidt (%0)" : : "r" (&idt_ptr));
Uses of the IDT
- Hardware Interrupts: Hardware devices, such as keyboards, mice, and network cards, generate interrupts to signal the CPU for attention. Each device is assigned a specific interrupt number, and the corresponding IDT entry points to the handler function.
- Software Interrupts: Operating systems and applications can generate software interrupts using instructions like
INT
. These interrupts are used for system calls and other software-driven events. - Exceptions: CPU-generated exceptions, such as divide-by-zero or invalid opcode, use specific entries in the IDT to handle these conditions and provide appropriate responses.
PIC
The Programmable Interrupt Controller (PIC) is a device used to manage hardware interrupts in a computer system. It allows the CPU to handle interrupts efficiently by prioritizing and dispatching them to the appropriate interrupt service routines (ISRs).
The x86 architecture includes two PIC
chips:
- Master PIC (PIC1): Handles interrupts from IRQ 0 to IRQ 7.
- Slave PIC (PIC2): Handles interrupts from IRQ 8 to IRQ 15 and is connected to IRQ 2 of the master PIC.
What is PIC?
The PIC is a hardware component that:
- Receives Interrupt Requests (IRQs): It takes input from various hardware devices that generate interrupts.
- Prioritizes Interrupts: It determines the priority of each interrupt, allowing higher-priority interrupts to be processed before lower-priority ones.
- Sends Interrupts to the CPU: It sends an interrupt signal to the CPU when an interrupt is ready to be handled.
- Manages Interrupts: It can mask (ignore) certain interrupts and remap interrupt numbers to avoid conflicts with CPU exceptions.
Why is PIC Important?
- Interrupt Handling: It enables the CPU to handle multiple interrupt sources efficiently by prioritizing them.
- Conflict Avoidance: It remaps interrupts to ensure they do not conflict with CPU exceptions (e.g., remapping IRQ0-15 to avoid overlapping with CPU exception numbers 0-31).
- Flexibility: It allows masking and prioritizing of interrupts, providing more control over the system's interrupt handling.
- Legacy Support: Despite newer systems using Advanced Programmable Interrupt Controllers (APICs), the PIC is fundamental for understanding how basic interrupt handling works.
Initial Setup and Remapping
In a typical x86 system, the Intel 8259A PIC is used. There are usually two PICs: a master and a slave, each handling 8 IRQ lines, for a total of 16 IRQs. The master PIC handles IRQ0-7, and the slave PIC handles IRQ8-15.
Why Do We Need to Remap the PIC?
The x86 architecture uses an Interrupt Vector Table (IVT) to map interrupt requests (IRQs) to specific interrupt service routines (ISRs).
The IVT has 256 entries (0 to 255), each corresponding to a specific interrupt vector.
1 Conflicts with CPU Exceptions:
By default, the PIC maps IRQs 0-7
to interrupt vectors 0x08-0x0F
, and IRQs 8-15
to vectors 0x70-0x77
. However, in the x86 architecture, the CPU reserves the first 32 interrupt vectors (0-31
) for processor exceptions (e.g., divide by zero, page fault and other important conditions).
- If the PIC uses vectors
0x08 to 0x0F
, it could conflict with these CPU-reserved exceptions. Thus PICIRQs 0x08-0x0F
overlaps with the reserved exceptions.
Thus we can conclude that, When the PIC is initialized, it uses a set of default interrupt vector offsets.
- IRQ 0-7 (from the master PIC) maps to interrupt vectors 0x08-0x0F.
- IRQ 8-15 (from the slave PIC) maps to interrupt vectors 0x70-0x77.
These vectors are crucial because they determine how the CPU interprets and handles interrupts.
2 Clear Separation:
Remapping ensures a clear separation between hardware interrupts and CPU exceptions. By moving the IRQ vectors to a different range, you can easily distinguish between hardware-generated interrupts and CPU-generated exceptions.
3 Consistent Handling:
With remapped IRQs, you can set up consistent and non-overlapping entries in the Interrupt Descriptor Table (IDT), making it easier to manage and handle interrupts in the operating system.
Avoiding Conflicts:
- To avoid such conflicts, the PIC is remapped to use different interrupt vectors that do not overlap with the CPU-reserved range.
- Typically, the PIC is remapped to use interrupt vectors starting at 0x20 (32) for IRQ 0 to 7, and 0x28 (40) for IRQ 8 to 15.
- This ensures that hardware interrupts are handled without interfering with CPU exceptions
How to Remap the PIC
Remapping the PIC involves reassigning the interrupt vector offsets for both the master and slave PICs. Here’s a step-by-step guide on how to achieve this:
1 Start the Initialization Sequence:
- Send the Initialization Command Word 1 (ICW1) to both the master and slave PICs. This command initiates the PICs and Prepares them for the remapping process.
outb(0x20, 0x11); // Start initialization sequence for Master PIC
outb(0xA0, 0x11); // Start initialization sequence for Slave PIC
2 Set New Interrupt Vector Offsets:
- Specify the new base offsets for the interrupt vectors. Commonly, the master PIC is assigned
0x20
(IRQ 0-7 will map to 0x20-0x27) and the slave PIC is assigned0x28
(IRQ 8-15 will map to 0x28-0x2F).
outb(0x21, 0x20); // Remap Master (Primary) PIC to 0x20-0x28
outb(0xA1, 0x28); // Remap Slave (Slave) PIC to 0x28-0x30
3 Inform PICs of their Configuration:
- Specify the cascade relationship between the master and slave PICs. This tells the master PIC that the slave PIC is connected to its IRQ2 line.
outb(0x21, 0x04); // Tell Master PIC there is a Slave PIC at IRQ2
outb(0xA1, 0x02); // Tell Slave PIC its cascade identity
4 Set PICs to 8086/88 Mode:
- Send the final command to set the PICs to 8086/88 mode, which is the operating mode used by most x86 operating systems.
outb(0x21, 0x01); // Set Master PIC to 8086/88 mode
outb(0xA1, 0x01); // Set Slave PIC to 8086/88 mode
5 Mask all IRQs in PIC
The PIC has a set of registers called Interrupt Mask Registers (IMRs), one for the master PIC and one for the slave PIC. Each bit in these registers corresponds to an IRQ line. If a bit is set to 1
, the corresponding IRQ is masked, meaning the PIC will not forward interrupts form the line to the CPU. If a bit is set to 0
, the corresponding IRQ is unmasked, allowing interrupts on that line to sent to the CPU.
- After remapping, either restore the original interrupt masks to ensure that only the intended interrupts are enabled or we can enable/disable all the IRQ lines for the moment.
outb(0x21, 0xFF); // Mask all IRQ in Master PIC
outb(0xA1, 0xFF); // Mask all IRQ in Slave PIC