Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Kernel-Level Timer Interrupts in x86 Protected Mode

Tech May 8 3

Hardware Prerequisites

Operating systems interacting with hardware timers rely on the 8253 Programmable Interval Timer (PIT) and the 8259A Programmable Interrupt Controller (PIC). The 8253 chip decrements a counter register on every clock pulse. Once it reaches zero, it asserts a signal on its output pin to trigger an interrupt. This process is controlled by a gate signal that permits counting only when enabled.

The 8259 PIC organizes external hardware interrupts into vectors. It distinguishes between internal exceptions and external requests. While it supports eight request lines natively, cascading two controllers allows for up to sixty-four interrupt levels. Internal state is managed via three registers: the Interrupt Request Register (IRR), the In-Service Register (ISR), and the Interrupt Mask Register (IMR).

Interrupt Descriptor Table (IDT)

In 32-bit protected mode, the CPU consults the IDT to locate interrupt handlers. Each entry consists of an 8-byte descriptor mapping a vector number to a handler address. These descriptors include a segment selector (pointing to the GDT code segment), offset addresses, and attribute bits determining access privileges. Although trap gates and task gates exist, this implementation utilizes interrupt gates for standard asynchronous handling.

Critical fields within the descriptor include:

  • Selector: Defines the privilege level and GDT entry.
  • Offset: Points to the start of the handler function.
  • Attributes: Includes the DPL (Descriptor Privilege Level), presence bit, and type identifier.

Port I/O Operations

Accessing hardware ports requires inline assembly to manipulate the CPU's eax and edx registers directly.

static inline void io_port_write(uint16_t port, uint8_t value) {
    __asm__ __volatile__("outb %0, %1"
        : 
        : "a"(value), "Nd"(port));
}

PIC Initialization Sequence

Before enabling specific IRQs, the Master and Slave PICs must be initialized. This involves setting command words (ICWs) to configure edge/level triggering, master/slave cascading, and vector base addresses. Subsequently, mask registers (IMRs) are updated to allow or block specific channels.

// Initialize both controllers with ICW1
io_port_write(0x20, 0x11); // Master
io_port_write(0xA0, 0x11); // Slave

// Set vector offsets (remap to avoid conflict with CPU exceptions)
io_port_write(0x21, 0x20); // Master base: 0x20
io_port_write(0xA1, 0x28); // Slave base: 0x28

// Configure cascading wires
io_port_write(0x21, 0x04); // Master knows Slave is on IRQ2
io_port_write(0xA1, 0x02); // Slave identifies as connected to Master

// Final initialization word
io_port_write(0x21, 0x01); // EOI mode enabled, 8086 mode
io_port_write(0xA1, 0x01);

// Update Masks: Enable IRQ0, disable others
io_port_write(0x21, 0xFE); // Allow IRQ0 on Master
io_port_write(0xA1, 0xFF); // Disable all on Slave

Timer Configuration

The 8253 PIT uses an external oscillator typically running at 1,193,180 Hz. To generate an interrupt every 10 milliseconds (100Hz frqeuency), we calculate a divisor. We select Channel 0 and configure it for Mode 3 (square wave generation).

const int target_freq = 100;
int divider = 1193180 / target_freq; 

// Control Word: Channel 0, Read/Write LSB then MSB, Mode 3
io_port_write(0x43, 0x36);

// Write Low Byte
io_port_write(0x40, (uint8_t)divider);

// Write High Byte
io_port_write(0x40, (uint8_t)(divider >> 8));

Constructing IDT Entries

We define a packed structure for IDT descriptors to ensure strict alignment. The table is populated to link the timer vector (0x20) to the C function pointer.

struct idt_entry {
    uint16_t offset_low;
    uint16_t selector;
    uint8_t  attributes;
    uint8_t  reserved;
    uint16_t offset_high;
} __attribute__((packed));

struct idt_entry idt_table[256];

void setup_timer_idt(void (*func)(void)) {
    // Fill offset
    idt_table[0x20].offset_low = (uint16_t)((uintptr_t)func & 0xFFFF);
    idt_table[0x20].offset_high = (uint16_t)((uintptr_t)func >> 16);

    // Select kernel code segment
    idt_table[0x20].selector = KERNEL_CODE_SEG;

    // Attributes: Present (Bit 7), Ring 0 (Bits 5-4=00), Type 14 (Interrupt Gate)
    idt_table[0x20].attributes = 0x8E00; 
    idt_table[0x20].reserved = 0;
}

Assembly Handler Implementation

The interrupt service routine must preserve the processor state using the stack and signal the completion of the interrupt service to the controller before returning.

.global timer_interrupt_handler

timer_interrupt_handler:
    pusha                     
    mov $0x20, %al
    outb %al, $0x20          # Send End-of-Interrupt
    popa                      
    iret                      

This sequence ensures that general-purpose registers remain valid after the context switch and that the PIC releases the interrupt request line correctly.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.