Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CRISTOP-bot/cris-os-v2/llms.txt

Use this file to discover all available pages before exploring further.

Protected mode on i386 requires a valid Global Descriptor Table (GDT) before the CPU can execute code reliably, and a populated Interrupt Descriptor Table (IDT) before any interrupt or exception can be handled without a triple fault. CrisOS v2 initializes the GDT, IDT, and 8259 Programmable Interrupt Controller (PIC) as three of the first actions in kmain, in that exact order, ensuring that exceptions are caught and hardware interrupts are controllable before any driver or subsystem is started.

GDT — Global Descriptor Table

Purpose

The GDT defines memory segments for the CPU. CrisOS v2 uses a flat memory model: all segments span the full 4 GB address space (base = 0, limit = 0xFFFFF with 4 KB granularity), so segmentation is effectively bypassed and all protection is done by the segment type and privilege level fields.
void gdt_init(void);
void gdt_load(unsigned int gdtr);

Implementation

gdt_init() populates five GDT entries and calls gdt_load() to install them:
void gdt_init(void)
{
    gdt_set_entry(0, 0, 0, 0, 0);              /* null descriptor  */
    gdt_set_entry(1, 0, 0xFFFFF, 0x9A, 0xCF);  /* code ring 0      */
    gdt_set_entry(2, 0, 0xFFFFF, 0x92, 0xCF);  /* data ring 0      */
    gdt_set_entry(3, 0, 0xFFFFF, 0xFA, 0xCF);  /* code ring 3      */
    gdt_set_entry(4, 0, 0xFFFFF, 0xF2, 0xCF);  /* data ring 3      */

    gdt_ptr_t gp;
    gp.limit = sizeof(gdt_entry_t) * GDT_ENTRIES - 1;
    gp.base  = (uint32_t)&gdt_entries;

    gdt_load((unsigned int)&gp);
    console_print("[ OK ] GDT initialized (flat mode)\n");
}
gdt_load() is implemented in assembly (src/asm_utils.S). It receives the address of the gdt_ptr_t structure, executes lgdt, reloads all data segment registers with selector 0x10 (entry 2 — ring-0 data), and performs a far jump to selector 0x08 (entry 1 — ring-0 code) to flush the instruction pipeline:
gdt_load:
    movl 4(%esp), %eax
    lgdt (%eax)
    movw $0x10, %ax
    movw %ax, %ds
    movw %ax, %es
    movw %ax, %fs
    movw %ax, %gs
    movw %ax, %ss
    ljmp $0x08, $gdt_flush
gdt_flush:
    ret
SelectorIndexPrivilegeTypeDescription
0x000NullRequired null descriptor
0x081Ring 0CodeKernel executable segment
0x102Ring 0DataKernel read/write segment
0x183Ring 3CodeUser executable segment
0x204Ring 3DataUser read/write segment

IDT — Interrupt Descriptor Table

Header

void idt_init(void);

Implementation

idt_init() registers 32 CPU exception handlers (isr0isr31) and 16 hardware IRQ handlers (irq0irq15) into the 256-entry IDT table, using code segment selector 0x08 and interrupt-gate flags 0x8E:
void idt_init(void)
{
    if (idt_installed)
        return;

    unsigned short code_sel = 0x08;
    unsigned char  flags    = 0x8E;   /* present, ring-0, 32-bit interrupt gate */

    for (int i = 0; i < 32; ++i)
        idt_set_entry(i, ex_handlers[i], code_sel, flags);

    for (int i = 0; i < 16; ++i)
        idt_set_entry(32 + i, irq_handlers[i], code_sel, flags);

    idt_pointer_t idtp;
    idtp.limit = (unsigned short)(sizeof(idt_entry_t) * 256 - 1);
    idtp.base  = (unsigned int)&idt;

    asm volatile("lidt (%0)" : : "p" (&idtp));

    idt_installed = true;
    console_print("[ OK ] IDT installed (32 exceptions + 16 IRQs)\n");
}
Exception handlers are generated by the isr_no_err and isr_err macros in src/asm_utils.S. Each stub pushes a dummy error code (for exceptions that do not generate one) and the interrupt number, then jumps to a common stub that saves all registers and calls the C exception_handler():
isr_common_stub:
    pusha
    push %ds
    push %es
    push %fs
    push %gs
    mov $0x10, %ax      /* load kernel data selector */
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs
    mov %esp, %eax
    push %eax
    call exception_handler
    add $4, %esp
    pop %gs
    pop %fs
    pop %es
    pop %ds
    popa
    add $8, %esp        /* discard err code + int number */
    iret
IRQ stubs follow the same pattern but call irq_handler() through irq_common_stub.

IRQ Dispatch

The C irq_handler() demultiplexes incoming IRQs by number and calls the appropriate driver handler:
void irq_handler(struct isr_regs *r)
{
    unsigned char irq = (unsigned char)(r->num - 32);

    switch (irq) {
    case 0:   timer_handler();       break;  /* PIT timer   */
    case 1:   (void)inb(0x60);       break;  /* keyboard ACK (polled) */
    case 12:  mouse_handler();       break;  /* PS/2 mouse  */
    default:  break;
    }

    pic_eoi(irq);
}

PIC — 8259 Programmable Interrupt Controller

Why Remapping Is Necessary

On reset the 8259 PIC maps IRQs 0–7 to CPU interrupt vectors 0x080x0F and IRQs 8–15 to 0x700x77. Vectors 0x000x1F are reserved for CPU exceptions in protected mode, so those default IRQ vectors would collide with exception handlers. pic_init() remaps both PICs before any IRQ can fire.

Header

void pic_init(void);
void pic_mask(unsigned char mask_master, unsigned char mask_slave);
void pic_eoi(unsigned char irq);

pic_init() — Remapping Sequence

void pic_init(void)
{
    /* save existing masks */
    unsigned char m1 = inb(PIC_MASTER_DATA);
    unsigned char m2 = inb(PIC_SLAVE_DATA);

    /* ICW1: begin initialization */
    outb(PIC_MASTER_CMD,  ICW1_INIT | ICW1_ICW4);
    outb(PIC_SLAVE_CMD,   ICW1_INIT | ICW1_ICW4);

    /* ICW2: vector base — master → 0x20, slave → 0x28 */
    outb(PIC_MASTER_DATA, 0x20);   /* IRQ0–7  → INT 0x20–0x27 */
    outb(PIC_SLAVE_DATA,  0x28);   /* IRQ8–15 → INT 0x28–0x2F */

    /* ICW3: cascade wiring */
    outb(PIC_MASTER_DATA, 4);      /* slave on IRQ2 */
    outb(PIC_SLAVE_DATA,  2);      /* slave cascade identity */

    /* ICW4: 8086 mode */
    outb(PIC_MASTER_DATA, ICW4_8086);
    outb(PIC_SLAVE_DATA,  ICW4_8086);

    /* restore saved masks */
    outb(PIC_MASTER_DATA, m1);
    outb(PIC_SLAVE_DATA,  m2);
}
After remapping, the interrupt vector layout is:
IRQ RangeCPU Vector RangeSource
IRQ 0–70x200x27Master PIC (timer, keyboard, …)
IRQ 8–150x280x2FSlave PIC (RTC, mouse, …)

IRQ Masking

pic_mask() writes directly to the PIC data ports. A 1 bit masks (disables) the corresponding IRQ; a 0 bit enables it.
void pic_mask(unsigned char mask_master, unsigned char mask_slave)
{
    outb(PIC_MASTER_DATA, mask_master);
    outb(PIC_SLAVE_DATA,  mask_slave);
}
CrisOS v2 calls pic_mask twice during initialization:
1

After pic_init() — keyboard only

pic_mask(0xFD, 0xFF);
Master mask 0xFD = 11111101b — only bit 1 (IRQ1, keyboard) is cleared (enabled). All other master IRQs and all slave IRQs (0xFF) are masked. This minimal mask is safe before the mouse driver is ready.
2

After mouse_init() — keyboard + mouse

pic_mask(0xFC, 0xEF);
Master mask 0xFC = 11111100b — bits 0 and 1 cleared, enabling IRQ0 (PIT timer) and IRQ1 (keyboard). Slave mask 0xEF = 11101111b — bit 4 cleared, enabling IRQ12 (PS/2 mouse). The PIT timer IRQ0 is now also unmasked so timer_handler() can increment timer_ticks.

PIT Timer — timer_init(100)

The 8253/8254 Programmable Interval Timer is configured by timer_init() in drivers/timer.c:
#define TIMER_CMD  0x43
#define TIMER_DATA 0x40

static void timer_set_freq(unsigned int frequency)
{
    unsigned int divisor = 1193180 / frequency;
    outb(TIMER_CMD,  0x36);                             /* channel 0, mode 3, binary */
    outb(TIMER_DATA, (unsigned char)(divisor & 0xFF));  /* low byte  */
    outb(TIMER_DATA, (unsigned char)((divisor >> 8) & 0xFF)); /* high byte */
}

void timer_init(unsigned int frequency)
{
    timer_ticks = 0;
    timer_set_freq(frequency);
}
The PIT’s input clock runs at 1 193 180 Hz. To achieve a target frequency the divisor is:
divisor = 1 193 180 / frequency
        = 1 193 180 / 100
        = 11 931   (≈ 100.006 Hz actual)
The command byte 0x36 selects channel 0, access mode “lobyte/hibyte”, operating mode 3 (square wave), and binary counting. Each time the counter reaches zero it fires IRQ0, which irq_handler() routes to timer_handler(), incrementing the global timer_ticks counter.
CrisOS v2 operates in polling mode for most input. Keyboard interrupts are enabled (IRQ1 is unmasked), but the shell’s keyboard_readline function spins waiting for the interrupt flag rather than using an interrupt-driven ring buffer. This means CPU time is consumed while waiting for user input, which is acceptable for a single-tasking educational kernel.

Build docs developers (and LLMs) love