Skip to content

CPU Architecture and Interrupts

TilekarOS manages the x86 processor using two fundamental structures: the Global Descriptor Table (GDT) and the Interrupt Descriptor Table (IDT).

1. Global Descriptor Table (GDT)

Source Files: gdt.c, gdt.asm

The GDT defines the memory segments used by the OS. TilekarOS uses a Flat Memory Model, where each segment starts at address 0 and covers the entire 4GB of memory.

Code Preview: gdt.c
#include "local_config.h"
#include <stdint.h>

/**
 * @brief single 8-byte GDT entry.
 *
 * https://wiki.osdev.org/Global_Descriptor_Table
 */
typedef struct GDTEntry {
  uint16_t limit_low;     // The lower 16 bits of the limit.
  uint16_t base_low;      // The lower 16 bits of the base.
  uint8_t base_middle;    // The next 8 bits of the base (16-23).
  uint8_t access;         // Access flags (see gdt_access_t).
  uint8_t flags_limit_hi; // Granularity, 32/16-bit, and high 4 bits of limit
                          // (16-19).
  uint8_t base_high;      // The last 8 bits of the base (24-31).
} __attribute__((packed)) GDTEntry;

/**
 * @brief 32-bit Task State Segment (TSS)
 *
 * Official Intel format (Vol. 3, Table 7-2).
 * Used for privilege-level stack switching and hardware task management.
 *
 * Each 16-bit field followed by a reserved 16-bit field ensures exact
 * alignment to the hardware-defined byte offsets (_resX).

 * https://wiki.osdev.org/Task_State_Segment
 */
typedef struct __attribute__((packed)) {
  uint16_t backlink, _res0;      // Previous Task Link (if hardware task switch)
  uint32_t esp0;                 // Stack Pointer (Ring 0)
  uint16_t ss0, _res1;           // Stack Segment (Ring 0)
  uint32_t esp1;                 // Stack Pointer (Ring 1)
  uint16_t ss1, _res2;           // Stack Segment (Ring 1)
  uint32_t esp2;                 // Stack Pointer (Ring 2)
  uint16_t ss2, _res3;           // Stack Segment (Ring 2)
  uint32_t cr3;                  // Page Directory Base Register
  uint32_t eip;                  // Instruction Pointer
  uint32_t eflags;               // Flags Register
  uint32_t eax, ecx, edx, ebx;   // General-Purpose Register
  uint32_t esp;                  // Stack Pointer
  uint32_t ebp;                  // Base Pointer
  uint32_t esi;                  // Source Index
  uint32_t edi;                  // Destination Index
  uint16_t es, _res4;            // Extra Segment Selector
  uint16_t cs, _res5;            // Code Segment Selector
  uint16_t ss, _res6;            // Stack Segment Selector
  uint16_t ds, _res7;            // Data Segment Selector
  uint16_t fs, _res8;            // FS Segment Selector
  uint16_t gs, _res9;            // GS Segment Selector
  uint16_t ldt_selector, _res10; // Local Descriptor Table Selector
  uint16_t t, iomap_base;        // Trap flag and I/O Map Base Address
} TSSEntry;

/**
 * @brief Represents the GDT Descriptor (GDTR payload).
 *
 * This structure is loaded into the GDTR register using the `lgdt` instruction.
 *
 * https://wiki.osdev.org/Global_Descriptor_Table#GDTR
 */
typedef struct {
  uint16_t limit;    // sizeof(GDT) - 1
  GDTEntry *address; // Linear address of the GDT.
} __attribute__((packed)) GDTDescriptor;

typedef enum {
  // --- Segment Type (bits 0-3) + S bit (bit 4) ---
  // System Segments (S=0)
  GDT_TYPE_TSS_32_AVAIL = 0x09, // 32-bit TSS (Available)
  GDT_TYPE_TSS_32_BUSY = 0x09,  // 32-bit TSS (Busy)
  GDT_TYPE_TSS_16_AVAIL = 0x09, // 32-bit TSS (Available)
  GDT_TYPE_TSS_16_BUSY = 0x09,  // 32-bit TSS (Busy)
  GDT_TYPE_LDT = 0x2,
  GDT_ACCESS_DESCRIPTOR_TSS = 0x00,

  GDT_ACCESS_CODE_READABLE = 0x02,
  GDT_ACCESS_DATA_WRITEABLE = 0x02,

  GDT_ACCESS_CODE_CONFORMING = 0x04,
  GDT_ACCESS_DATA_DIRECTION_NORMAL = 0x00,
  GDT_ACCESS_DATA_DIRECTION_DOWN = 0x04,

  GDT_ACCESS_DATA_SEGMENT = 0x10,
  GDT_ACCESS_CODE_SEGMENT = 0x18,

  // Privilege Level (DPL, bits 5-6)
  GDT_ACCESS_RING0 = 0x00,
  GDT_ACCESS_RING1 = 0x20,
  GDT_ACCESS_RING2 = 0x40,
  GDT_ACCESS_RING3 = 0x60,

  // Present (P, bit 7)
  GDT_ACCESS_PRESENT = 0x80,

} GDT_ACCESS;

typedef enum {
  // Size (D/B bit, bit 2)
  GDT_FLAG_64BIT = 0x20,
  GDT_FLAG_32BIT = 0x40,
  GDT_FLAG_16BIT = 0x00,

  // Granularity (G bit, bit 3)
  GDT_FLAG_GRANULARITY_1B = 0x00,
  GDT_FLAG_GRANULARITY_4K = 0x80,
} GDT_FLAGS;

// Helper macros
#define GDT_PACK_LIMIT_LOW(limit) ((limit) & 0xFFFF)
#define GDT_PACK_BASE_LOW(base) ((base) & 0xFFFF)
#define GDT_PACK_BASE_MIDDLE(base) (((base) >> 16) & 0xFF)
#define GDT_PACK_FLAGS_LIMIT_HI(limit, flags)                                  \
  ((((limit) >> 16) & 0x0F) | ((flags) & 0xF0))
#define GDT_PACK_BASE_HIGH(base) (((base) >> 24) & 0xFF)

/**
 * @brief Macro to create a GDT_ENTRY initializer.
 * Simplifies the creation of GDT entries at compile time.
 */
#define GDT_ENTRY(base, limit, access, flags)                                  \
  {GDT_PACK_LIMIT_LOW(limit),                                                  \
   GDT_PACK_BASE_LOW(base),                                                    \
   GDT_PACK_BASE_MIDDLE(base),                                                 \
   (access),                                                                   \
   GDT_PACK_FLAGS_LIMIT_HI(limit, flags),                                      \
   GDT_PACK_BASE_HIGH(base)}


/**
 * @brief The Task State Segment (TSS) entry.
 *
 * We only need one for the whole system, to define the Ring 0 stack
 * that the CPU will switch to upon an interrupt or exception.
 * This matches the `static TSSEntry tss_entry` from the original code.
 */
TSSEntry tss_entry = {
    // Set kernel stack segment (GDT_KERNEL_DS_OFFSET = Kernel Data) and pointer.
    // ESP0 will be set by the kernel during context switches or task creation.
    .ss0 = GDT_KERNEL_DS_OFFSET,
    .esp0 = 0,

    // Set segment selectors for user mode (RPL=3).
    // GDT_KERNEL_CS_OFFSET = Kernel Code, GDT_KERNEL_DS_OFFSET = Kernel Data. | 0x3 sets RPL to Ring 3.
    .cs = GDT_KERNEL_CS_OFFSET | 0x3,
    .ds = GDT_KERNEL_DS_OFFSET | 0x3,
    .ss = GDT_KERNEL_DS_OFFSET | 0x3,
    .es = GDT_KERNEL_DS_OFFSET | 0x3,
    .fs = GDT_KERNEL_DS_OFFSET | 0x3,
    .gs = GDT_KERNEL_DS_OFFSET | 0x3,
    .iomap_base = sizeof(TSSEntry),
};

GDTEntry gdt[] = {
    // NULL descriptor
    [0] = GDT_ENTRY(0, 0, 0, 0),

    // Kernel 32-bit code segment
    [GDT_KERNEL_CS_INDEX] = GDT_ENTRY(0, 0xFFFFF,
              GDT_ACCESS_PRESENT | GDT_ACCESS_RING0 | GDT_ACCESS_CODE_SEGMENT |
                  GDT_ACCESS_CODE_READABLE,
              GDT_FLAG_32BIT | GDT_FLAG_GRANULARITY_4K),

    // Kernel 32-bit data segment
    [GDT_KERNEL_DS_INDEX] = GDT_ENTRY(0, 0xFFFFF,
              GDT_ACCESS_PRESENT | GDT_ACCESS_RING0 | GDT_ACCESS_DATA_SEGMENT |
                  GDT_ACCESS_DATA_WRITEABLE,
              GDT_FLAG_32BIT | GDT_FLAG_GRANULARITY_4K),

    // User 32-bit code segment
    [GDT_USER_CS_INDEX] = GDT_ENTRY(0, 0xFFFFF,
              GDT_ACCESS_PRESENT | GDT_ACCESS_RING3 | GDT_ACCESS_CODE_SEGMENT |
                  GDT_ACCESS_CODE_READABLE,
              GDT_FLAG_32BIT | GDT_FLAG_GRANULARITY_4K),

    // User 32-bit data segment
    [GDT_USER_DS_INDEX] = GDT_ENTRY(0, 0xFFFFF,
              GDT_ACCESS_PRESENT | GDT_ACCESS_RING3 | GDT_ACCESS_DATA_SEGMENT |
                  GDT_ACCESS_DATA_WRITEABLE,
              GDT_FLAG_32BIT | GDT_FLAG_GRANULARITY_4K),

    // Task State Segment
    [GDT_TSS_INDEX] = {/* Set by `gdt_install_tss` in `init_gdt` */},
};

GDTDescriptor gdt_discriptor = {sizeof(gdt) - 1, gdt};

// Assembly functions
// These functions are expected to be defined in an assembly file.

/**
 * @brief Loads the GDT descriptor into the GDTR register.
 * @param descriptor Pointer to the GDT descriptor.
 */
extern void gdt_load_register(GDTDescriptor *descriptor);

/**
 * @brief Loads the Task State Segment selector into the TR register.
 */
extern void tss_load_register(void);

/**
 * @brief Creates and installs the TSS descriptor in the GDT.
 *
 * This function sets the GDT entry at GDT_TSS_INDEX to point to
 * the global `tss_entry`.
 *
 * This function is only used by `gdt_init` in this file,
 * so it is marked `static`.
 */
static void gdt_install_tss(void) {
    uint32_t base = (uint32_t)&tss_entry;
    uint32_t limit = sizeof(tss_entry) - 1;

    // Access byte: Present, Ring 3, System, 32-bit TSS
    // P=1, DPL=3, S=0, Type=9 (32-bit TSS)
    uint8_t access = GDT_ACCESS_PRESENT | GDT_ACCESS_RING3 | GDT_TYPE_TSS_32_AVAIL;

    // Flags: 1-byte granularity. 32-bit flag is ignored for TSS.
    GDTEntry tss_descriptor = GDT_ENTRY(base, limit, access, GDT_FLAG_GRANULARITY_1B);
    gdt[GDT_TSS_INDEX] = tss_descriptor;
}

/**
 * @brief Setups the GDT and TSS.
 */
void init_gdt() {
  gdt_load_register(&gdt_discriptor);
  gdt_install_tss();
  tss_load_register();
}

void tss_set_kernel_stack(uint32_t stack_base) {
    tss_entry.esp0 = stack_base;
}

Segment Layout

Selector Usage Base Limit DPL
0x08 Kernel Code 0x0 4 GB 0 (Ring 0)
0x10 Kernel Data 0x0 4 GB 0 (Ring 0)
0x18 User Code 0x0 4 GB 3 (Ring 3)
0x20 User Data 0x0 4 GB 3 (Ring 3)
0x28 TSS &tss sizeof(TSS) 3 (Ring 3)

Segment Selectors Configuration:

See local_config.h for the detailed index and offset definitions.

GDT Entry Structure

The GDT defines the characteristics of the various memory segments.

packet-beta
    0-15: "Limit (0-15)"
    16-31: "Base (0-15)"
    32-39: "Base (16-23)"
    40-47: "Access Byte"
    48-51: "Limit (16-19)"
    52-55: "Flags"
    56-63: "Base (24-31)"
  • Base: The starting linear address of the segment (0x0 for Flat Model).
  • Limit: The size of the segment.
  • Access Byte: Defines presence, privilege level (Ring 0 vs 3), and type (Code vs Data).
  • Flags: Granularity (4KiB blocks vs 1 Byte) and Operand size (32-bit vs 16-bit).

Task State Segment (TSS)

The TSS is essential for Privilege Level Switching. When an interrupt occurs while the CPU is in Ring 3, the hardware automatically switches to the Ring 0 stack pointer (esp0) stored in the TSS.

Why we need TSS

Even though TilekarOS uses software-based multitasking, a single hardware TSS is required to handle the transition from User Mode back to Kernel Mode during interrupts or system calls.


2. Interrupt Descriptor Table (IDT)

Source Files: idt.c, idt.asm

The IDT tells the CPU which code to run when an interrupt or exception occurs.

Code Preview: idt.c
#include "local_config.h"
#include <kernel/tty.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "idt.h"
#include "utils.h"
#include "task.h"

/*
 * IDT Entry Structure
 * Defines the format of a single interrupt gate as required by the x86 CPU.
 * Used to tell the CPU where to jump when an interrupt occurs.
 */
typedef struct
{
    uint16_t isr_low;               // The lower 16 bits of the ISR's address
    uint16_t kernel_cs;             // The GDT Code Segment selector (tells CPU which segment the ISR lives in)
    uint8_t reserved;               // Must be set to zero (reserved by Intel)
    uint8_t attributes;             // Type and attributes (Present, DPL, Gate Type)
    uint16_t isr_high;              // The higher 16 bits of the ISR's address
} __attribute__((packed)) IDTEntry; // 'packed' prevents compiler padding, ensuring exact hardware alignment

/*
 * IDT Flags
 * Constants to set the 'attributes' byte in the IDTEntry.
 */
enum IDT_FLAGS
{
    IDT_FLAG_PRESENT = 0x80,   // Bit 7: 1 = Interrupt is present
    IDT_FLAG_RING0 = 0x00,     // Bits 5-6: Privilege Level 0 (Kernel)
    IDT_FLAG_RING1 = 0x20,     // Bits 5-6: Privilege Level 1
    IDT_FLAG_RING2 = 0x40,     // Bits 5-6: Privilege Level 2
    IDT_FLAG_RING3 = 0x60,     // Bits 5-6: Privilege Level 3 (User)
    IDT_FLAG_TASK_GATE = 0x05, // Gate Type: Task Gate
    IDT_FLAG_INT_GATE = 0x0E,  // Gate Type: 32-bit Interrupt Gate
    IDT_FLAG_TRAP_GATE = 0x0F, // Gate Type: 32-bit Trap Gate
};

/*
 * IDT Register (IDTR)
 * The structure pointer required by the 'lidt' assembly instruction.
 */
typedef struct
{
    uint16_t limit; // Size of the IDT in bytes - 1
    uint32_t base;  // Linear address where the IDT array starts
} __attribute__((packed)) IDTR;

// The actual Interrupt Descriptor Table array (256 entries).
// Aligned to 0x10 for potential performance optimization on some CPUs.
__attribute__((aligned(0x10))) static IDTEntry idt[256];

// The IDTR structure telling the CPU the size of our table.
static IDTR idtr = {
    .limit = sizeof(idt) - 1,
};

// Assembly helper to load the IDT pointer (lidt instruction)
extern void idt_flush(uint32_t);


/*
 * External Assembly Interrupt Service Routines (ISRs)
 * These are defined in an assembly file (e.g., interrupt.asm).
 * They prepare the stack and call the C handlers.
 */

// --- CPU Exceptions (0-31) ---
extern void isr0();  // Divide by Zero
extern void isr1();  // Debug
extern void isr2();  // NMI
extern void isr3();  // Breakpoint
extern void isr4();  // Overflow
extern void isr5();  // Bound Range Exceeded
extern void isr6();  // Invalid Opcode
extern void isr7();  // Device Not Available
extern void isr8();  // Double Fault
extern void isr9();  // Coprocessor Segment Overrun
extern void isr10(); // Invalid TSS
extern void isr11(); // Segment Not Present
extern void isr12(); // Stack-Segment Fault
extern void isr13(); // General Protection Fault
extern void isr14(); // Page Fault
extern void isr15(); // Reserved
extern void isr16(); // x87 Floating Point Exception
extern void isr17(); // Alignment Check
extern void isr18(); // Machine Check
extern void isr19(); // SIMD Floating Point Exception
extern void isr20(); // Virtualization Exception
extern void isr21(); // Reserved
extern void isr22(); // Reserved
extern void isr23(); // Reserved
extern void isr24(); // Reserved
extern void isr25(); // Reserved
extern void isr26(); // Reserved
extern void isr27(); // Reserved
extern void isr28(); // Reserved
extern void isr29(); // Reserved
extern void isr30(); // Security Exception
extern void isr31(); // Reserved

// --- Software Interrupts / System Calls ---
extern void syscall_stub(); // Often used for Syscalls (0x80)
extern void isr177(); // Alternative Syscall entry

// --- Hardware Interrupts (IRQs) ---
// Mapped from PIC hardware lines to IDT entries 32-47
extern void irq0();  // Timer
extern void irq1();  // Keyboard
extern void irq2();  // Cascade (internal for PIC2)
extern void irq3();  // COM2
extern void irq4();  // COM1
extern void irq5();  // LPT2
extern void irq6();  // Floppy Disk
extern void irq7();  // LPT1
extern void irq8();  // CMOS real-time clock
extern void irq9();  // Legacy SCSI / NIC
extern void irq10(); // SCSI / NIC
extern void irq11(); // SCSI / NIC
extern void irq12(); // PS/2 Mouse
extern void irq13(); // FPU
extern void irq14(); // Primary ATA Hard Disk
extern void irq15(); // Secondary ATA Hard Disk

/*
 * Helper: Set IDT Gate
 * Fills an IDT entry with the address of the handler and settings.
 */
void set_idt_gate(uint8_t num, uint32_t base, uint16_t sel, uint8_t flags)
{
    idt[num].isr_low = base & 0xFFFF;          // Low 16 bits of address
    idt[num].isr_high = (base >> 16) & 0xFFFF; // High 16 bits of address
    idt[num].kernel_cs = sel;                  // Code Segment Selector
    idt[num].reserved = 0;                     // Always 0
    idt[num].attributes = flags;               // Flags (Present, Ring, Type)
};

/*
 * Initialize IDT
 * Sets up the table, remaps the PIC, and loads the IDT into the CPU.
 */
void init_idt()
{
    // 1. Set the base address in the IDT register pointer
    idtr.base = (uint32_t)&idt[0];

    // 2. Clear the IDT memory area
    memset    (&idt, 0, sizeof(idt));

    /*
     * 3. Remap the Programmable Interrupt Controller (PIC).
     * By default, IRQs 0-7 map to interrupts 0-7, which conflicts with
     * CPU exceptions (like "Division by Zero"). We move them to 32-47.
     */

    // ICW1: Start initialization sequence (in cascade mode)
    out_port_b(0x20, 0x11); // Master PIC
    out_port_b(0xA0, 0x11); // Slave PIC

    // ICW2: Vector Offsets (Remapping happens here)
    out_port_b(0x21, 0x20); // Master PIC vector offset start = 32 (0x20)
    out_port_b(0xA1, 0x28); // Slave PIC vector offset start = 40 (0x28)

    // ICW3: Cascade identity
    out_port_b(0x21, 0x04); // Tell Master that Slave is at IRQ2 (0000 0100)
    out_port_b(0xA1, 0x02); // Tell Slave its cascade identity (0000 0010)

    // ICW4: 8086 mode
    out_port_b(0x21, 0x01);
    out_port_b(0xA1, 0x01);

    // OCW1: Unmask all interrupts (allow them to happen)
    out_port_b(0x21, 0x0);
    out_port_b(0xA1, 0x0);

    /*
     * 4. Populate the IDT
     * Connect the assembly stub functions to the IDT slots.
     * Note: GDT_KERNEL_CS_OFFSET is usually 0x08 (Kernel Code Segment).
     */

    // --- Exception Gates (0-31) ---
    set_idt_gate( 0, (uint32_t)isr0 , GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate( 1, (uint32_t)isr1 , GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate( 2, (uint32_t)isr2 , GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate( 3, (uint32_t)isr3 , GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING3 | IDT_FLAG_TRAP_GATE);
    set_idt_gate( 4, (uint32_t)isr4 , GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING3 | IDT_FLAG_TRAP_GATE);
    set_idt_gate( 5, (uint32_t)isr5 , GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate( 6, (uint32_t)isr6 , GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate( 7, (uint32_t)isr7 , GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate( 8, (uint32_t)isr8 , GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate( 9, (uint32_t)isr9 , GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(10, (uint32_t)isr10, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(11, (uint32_t)isr11, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(12, (uint32_t)isr12, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(13, (uint32_t)isr13, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING3 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(14, (uint32_t)isr14, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(15, (uint32_t)isr15, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(16, (uint32_t)isr16, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(17, (uint32_t)isr17, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(18, (uint32_t)isr18, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(19, (uint32_t)isr19, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(20, (uint32_t)isr20, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(21, (uint32_t)isr21, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(22, (uint32_t)isr22, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(23, (uint32_t)isr23, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(24, (uint32_t)isr24, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(25, (uint32_t)isr25, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(26, (uint32_t)isr26, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(27, (uint32_t)isr27, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(28, (uint32_t)isr28, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(29, (uint32_t)isr29, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(30, (uint32_t)isr30, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(31, (uint32_t)isr31, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_TRAP_GATE);

    // --- Hardware Interrupt Gates (IRQs 32-47) ---
    set_idt_gate(32, (uint32_t)irq0, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(33, (uint32_t)irq1, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(34, (uint32_t)irq2, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(35, (uint32_t)irq3, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(36, (uint32_t)irq4, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(37, (uint32_t)irq5, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(38, (uint32_t)irq6, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(39, (uint32_t)irq7, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(40, (uint32_t)irq8, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(41, (uint32_t)irq9, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(42, (uint32_t)irq10, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(43, (uint32_t)irq11, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(44, (uint32_t)irq12, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(45, (uint32_t)irq13, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(46, (uint32_t)irq14, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);
    set_idt_gate(47, (uint32_t)irq15, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING0 | IDT_FLAG_INT_GATE);

    // --- System Call Gates ---
    set_idt_gate(128, (uint32_t)syscall_stub, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING3 | IDT_FLAG_TRAP_GATE);
    set_idt_gate(177, (uint32_t)isr177, GDT_KERNEL_CS_OFFSET, IDT_FLAG_PRESENT | IDT_FLAG_RING3 | IDT_FLAG_TRAP_GATE);

    // 5. Finally, load the IDT using the ASM command 'lidt'
    idt_flush((uint32_t)&idtr);
}

// Human-readable strings describing the first 32 exceptions
const char *exception_messages[] = {
    [0] = "Division By Zero",
    [1] = "Debug",
    [2] = "Non Maskable Interrupt",
    [3] = "Breakpoint",
    [4] = "Into Detected Overflow",
    [5] = "Out of Bounds",
    [6] = "Invalid Opcode",
    [7] = "No Coprocessor",
    [8] = "Double fault",
    [9] = "Coprocessor Segment Overrun",
    [10] = "Bad TSS",
    [11] = "Segment not present",
    [12] = "Stack fault",
    [13] = "General protection fault",
    [14] = "Page fault",
    [15] = "Unknown Interrupt",
    [16] = "Coprocessor Fault",
    [17] = "Alignment Fault",
    [18] = "Machine Check",
    [19] = "Reserved",
    [20] = "Reserved",
    [21] = "Reserved",
    [22] = "Reserved",
    [23] = "Reserved",
    [24] = "Reserved",
    [25] = "Reserved",
    [26] = "Reserved",
    [27] = "Reserved",
    [28] = "Reserved",
    [29] = "Reserved",
    [30] = "Reserved",
    [31] = "Reserved"};

/*
 * ISR Handler
 * This is the main handler for CPU Exceptions (0-31).
 * It is called by the assembly stub when a fault occurs.
 */
void isr_handler(InteruptReg *regs)
{
    // If it is a known CPU exception
    if (regs->intr_num < 32)
    {
        uint32_t pid = current_task ? current_task->id : 0;
        printf("\nEXCEPTION: %s (ID: %d)\n", exception_messages[regs->intr_num], regs->intr_num);
        printf("FAULTING TASK PID: %d\n", pid);

        if (regs->intr_num == 14) { // Page Fault
            uint32_t cr2;
            asm volatile("mov %%cr2, %0" : "=r"(cr2));
            printf("FAULTING VIRTUAL ADDRESS: 0x%x\n", cr2);
        }

        // Check if exception occurred in user land (Ring 3)
        // or if it's a kernel task other than PID 0
        if ((regs->csm & 0x3) == 3) {
            printf("TERMINATING USER PROCESS (PID %d) due to exception.\n", pid);
            task_exit();
        } else if (pid != 0) {
            printf("TERMINATING KERNEL TASK (PID %d) due to exception.\n", pid);
            task_exit();
        } else {
            printf("CRITICAL EXCEPTION IN KERNEL MAIN (PID 0). SYSTEM HALTED.\n");
            // Hang the system to prevent further damage
            for (;;);
        }
    }
}

// Array of function pointers to handle custom IRQ handlers (e.g., keyboard_handler)
void *irq_routines[16] = {0};

/*
 * Install IRQ Handler
 * Allows other parts of the kernel (like the keyboard driver) to register a function
 * to be called when a specific IRQ fires.
 */
void irq_install_handler(int irq, void (*handler)(InteruptReg *r))
{
    irq_routines[irq] = handler;
}

/*
 * Uninstall IRQ Handler
 * Removes the custom handler for an IRQ.
 */
void irq_uninstall_handler(int irq)
{
    irq_routines[irq] = NULL;
}

void pic_send_eoi(uint32_t intr_num) {
    if (intr_num >= 40) {
        out_port_b(0xA0, 0x20); // EOI to Slave PIC
    }
    out_port_b(0x20, 0x20); // Always send EOI to Master PIC
}

/*
 * IRQ Handler
 * This is the main handler for Hardware Interrupts (32-47).
 * It finds the registered handler function and manages the PIC.
 */
void irq_handler(InteruptReg *regs)
{
    // Function pointer for the specific handler
    void (*handler)(InteruptReg *regs) = irq_routines[regs->intr_num - 32];

    // If a custom handler exists, execute it.
    // The handler is responsible for sending EOI (either manually or via context switch).
    if (handler) {
        handler(regs);
    }

    pic_send_eoi(regs->intr_num);
}

Interrupt Dispatch Pipeline

When an interrupt (e.g., IRQ 1 - Keyboard) occurs, the hardware stops current execution and jumps to a specific assembly stub.

sequenceDiagram
    participant HW as Hardware (Keyboard)
    participant CPU
    participant IDT as IDT Entry
    participant ASM as ASM Wrapper (irq_common)
    participant C as C Handler (irq_handler)

    HW->>CPU: Signals IRQ 1
    CPU->>IDT: Looks up Vector 33
    IDT->>ASM: Jump to irq1 stub
    ASM->>ASM: Save registers (pushad)
    ASM->>C: call irq_handler(regs)
    C->>C: Execute Keyboard Driver
    C->>ASM: Return
    ASM->>ASM: Restore registers (popad)
    ASM->>CPU: iret

IDT Configuration

The IDT is an array of 256 8-byte entries.

Vector Range Usage Description
0 - 31 CPU Exceptions Faults, Traps, and Aborts (e.g., Page Fault, GPF).
32 - 47 Hardware IRQs Remapped PIC interrupts. IRQ0=32 (Timer), IRQ1=33 (Keyboard).
48 - 127 Reserved Available for custom use.
128 (0x80) Syscall Typical Linux-style system call entry point.
177 Syscall Alternate system call entry point.

PIC Remapping

The 8259 PIC is remapped so that hardware interrupts (IRQs) start at vector 32 (0x20), avoiding conflicts with CPU exceptions (0-31).


3. Kernel Utility Functions

Source File: utils.h

TilekarOS provides low-level functions for direct hardware interaction:

  • out_port_b(port, value): Writes a byte to an I/O port.
  • in_port_b(port): Reads a byte from an I/O port.
  • interrupt_save() / interrupt_restore(): Atomically disable/enable interrupts using cli and sti, preserving the current state of the EFLAGS register.
Code Preview: utils.h
#ifndef ARCH_I386_UTILS_H
#define ARCH_I386_UTILS_H

#include <stdint.h>

/*
 * Interrupt Registers
 * A snapshot of the CPU state pushed onto the stack by the assembly stubs
 * before calling the C handler. Used to analyze crashes or handle inputs.
 */
typedef struct
{
    uint32_t gs, fs, es, ds;                         // Pushed by segment registers
    uint32_t edi, esi, ebp, esp, ebx, edx, ecx, eax; // Pushed by pushad
    uint32_t intr_num, err_cod;                      // Interrupt number and error code (if applicable)
    uint32_t eip, csm, eflags, useresp, ss;          // Pushed by the processor automatically
} InteruptReg;

/**
 * out_port_b - Writes a byte to an I/O port.
 * @port: The I/O port address.
 * @value: The value to write.
 */
void out_port_b(uint16_t port, uint8_t value);

/**
 * out_port_w - Writes a word to an I/O port.
 * @port: The I/O port address.
 * @value: The value to write.
 */
void out_port_w(uint16_t port, uint16_t value);

/**
 * out_port_l - Writes a double word (32-bit) to an I/O port.
 * @port: The I/O port address.
 * @value: The value to write.
 */
void out_port_l(uint16_t port, uint32_t value);

/**
 * in_port_b - Reads a byte from an I/O port.
 * @port: The I/O port address.
 *
 * Return: The byte read from the port.
 */
uint8_t in_port_b(uint16_t port);

/**
 * in_port_w - Reads a word from an I/O port.
 * @port: The I/O port address.
 *
 * Return: The word read from the port.
 */
uint16_t in_port_w(uint16_t port);

/**
 * in_port_l - Reads a double word (32-bit) from an I/O port.
 * @port: The I/O port address.
 *
 * Return: The dword read from the port.
 */
uint32_t in_port_l(uint16_t port);

#define CEIL_DIV(a,b) (((a + b) - 1)/b)

/**
 * interrupt_save - Saves the current EFLAGS and disables interrupts.
 * Return: The saved EFLAGS value.
 */
static inline uint32_t interrupt_save(void) {
    uint32_t flags;
    asm volatile("pushfl\n\tpopl %0\n\tcli" : "=r"(flags));
    return flags;
}

#include <stdbool.h>

/**
 * interrupt_restore - Restores the EFLAGS (and thus interrupt state).
 * @flags: The EFLAGS value to restore.
 */
static inline void interrupt_restore(uint32_t flags) {
    asm volatile("pushl %0\n\tpopfl" : : "r"(flags) : "cc", "memory");
}

#endif // ARCH_I386_UTILS_H

4. Test/Example: Triggering a Soft Exception

You can test the IDT by triggering a manual software interrupt or an exception:

void test_interrupts() {
    // 1. Division by zero (Should trigger ISR 0)
    volatile int x = 5 / 0; 

    // 2. Breakpoint (Should trigger ISR 3)
    asm volatile("int3");

    // 3. System Call (Should trigger ISR 128 / 0x80)
    asm volatile("int $0x80" : : "a"(0)); // SYS_EXIT
}

References