From 47e83cf7957b6b606d963576af8b060ad393f263 Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:17:35 +0200 Subject: [PATCH 01/10] Nadir serial COM1 polling driver --- include/serial.h | 35 ++++++++++++++++++++ kernel/arch/x86_64/serial.c | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 include/serial.h create mode 100644 kernel/arch/x86_64/serial.c diff --git a/include/serial.h b/include/serial.h new file mode 100644 index 0000000..784a92a --- /dev/null +++ b/include/serial.h @@ -0,0 +1,35 @@ +/* Nadir serial log: COM1 (16550 UART) polling driver. + * + * Runs in 64-bit long mode at Ring 0 (CPL 0); uses `inb`/`outb` which fault + * with #GP if executed above CPL 0, so this driver is kernel-only. + * Ports: COM1 0x3F8-0x3FF (data, int-enable, divisor latch, FIFO, line + * control, modem control, line status). QEMU/SeaBIOS provides COM1; the + * VGA console stays primary and serial is the secondary log. + * Fault behavior: port I/O cannot fault at CPL 0. The TX poll spins on + * LSR bit 5 and hangs only if the UART hardware is absent or wedged — + * call serial_ok() first to check LSR sanity. + * + * Preconditions: call serial_init() once before any put. serial_puts() + * requires a valid NUL-terminated string (NULL is ignored, not crashed + * on). Failure modes: on absent hardware, init writes go nowhere and + * serial_ok() returns 0; TX polls may then spin forever. + */ + +#ifndef NADIR_SERIAL_H +#define NADIR_SERIAL_H + +/* Program COM1 for 38400 8N1 polling (interrupts off, FIFO on). + * Ring 0 only. Safe to call again to re-program the port. */ +void serial_init(void); + +/* Block until the THR is empty (LSR bit 5), then emit one byte. */ +void serial_putchar(char c); + +/* Emit a NUL-terminated string via serial_putchar(). NULL is a no-op. */ +void serial_puts(const char *s); + +/* Nonzero if a UART looks present (LSR reads back sane, not 0xFF + * as an open bus returns). Zero means "do not trust TX polls". */ +int serial_ok(void); + +#endif /* NADIR_SERIAL_H */ diff --git a/kernel/arch/x86_64/serial.c b/kernel/arch/x86_64/serial.c new file mode 100644 index 0000000..ebc3810 --- /dev/null +++ b/kernel/arch/x86_64/serial.c @@ -0,0 +1,65 @@ +/* COM1 16550 UART polling log (see serial.h). */ + +#include + +#include "io.h" +#include "serial.h" + +/* Register offsets from the COM1 base. */ +#define COM1_BASE 0x3F8U +#define COM_DATA 0x3F8U /* THR/RBR (DLAB=0) / divisor low (DLAB=1) */ +#define COM_IER 0x3F9U /* interrupt enable / divisor high (DLAB=1) */ +#define COM_FIFO 0x3FAU /* FIFO control */ +#define COM_LCTL 0x3FBU /* line control + DLAB bit 7 */ +#define COM_MCTL 0x3FCU /* modem control */ +#define COM_LSR 0x3FDU /* line status: bit 5 = THR empty, bit 0 = RX ready */ + +#define LSR_TX_EMPTY 0x20U /* THR holds no byte: safe to write */ +#define LSR_RX_READY 0x01U /* RBR holds a byte (reserved for future RX) */ + +/* Port I/O owned by include/io.h; no local inb/outb duplicates. */ + +void serial_init(void) +{ + /* No UART interrupts: pure polling, so the 8259/IDT stays untouched. */ + outb(COM_IER, 0x00); + /* DLAB on to program the divisor. */ + outb(COM_LCTL, 0x80); + /* Divisor 3 = 115200/3 = 38400 baud. */ + outb(COM_DATA, 0x03); + outb(COM_IER, 0x00); + /* 8N1: 8 bits, no parity, one stop bit; DLAB back off. */ + outb(COM_LCTL, 0x03); + /* FIFO on, clear both queues, 14-byte threshold. */ + outb(COM_FIFO, 0xC7); + /* DTR+RTS on, OUT2 on so the UART drives interrupts (unused). */ + outb(COM_MCTL, 0x0B); +} + +void serial_putchar(char c) +{ + /* Spin until THR is empty; hangs only if hardware is absent/wedged, + * which is why callers gate on serial_ok() when unsure. */ + while ((inb(COM_LSR) & LSR_TX_EMPTY) == 0) { + /* Poll. */ + } + outb(COM_DATA, (uint8_t)c); +} + +void serial_puts(const char *s) +{ + if (s == 0) { + return; + } + while (*s != '\0') { + serial_putchar(*s); + s++; + } +} + +int serial_ok(void) +{ + /* An absent port reads back 0xFF (open bus); a real 16550 never + * reports all status bits set at once, so that means no UART. */ + return inb(COM_LSR) != 0xFFU; +} From 6ee1900c1a02a95f28e9c7f33745a3e0d0bb9fba Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:21:25 +0200 Subject: [PATCH 02/10] Nadir 8254 PIT timer driver --- include/pit.h | 56 +++++++++++++++++++++++ kernel/arch/x86_64/pit.c | 95 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 include/pit.h create mode 100644 kernel/arch/x86_64/pit.c diff --git a/include/pit.h b/include/pit.h new file mode 100644 index 0000000..c9001be --- /dev/null +++ b/include/pit.h @@ -0,0 +1,56 @@ +/* Nadir 8254 PIT timer driver (see pit.c). + * + * Runs in 64-bit long mode at Ring 0 (CPL 0): programs the PIT via `outb` + * to ISA ports PIT_CMD (0x43) and PIT_CH0 (0x40), and handles IRQ0 + * (8259 PIC IRQ0 -> CPU vector 32 after pic_remap). + * + * Ports used: 0x43 (command/mode register, write-only), 0x40 (channel 0 + * data, write-only for the divisor latch). + * + * Preconditions: caller owns interrupt policy — pit_init() neither enables + * (sti) nor disables (cli) interrupts; target init order is + * pic_remap -> idt_install_irqs -> pit_init -> pic_unmask(0) -> sti. + * pit_sleep() requires interrupts enabled, otherwise `hlt` never wakes. + * + * Failure modes: divisor is fixed at compile time (PIT_DIVISOR); a zero + * divisor would mean "65536" to the hardware and the wrong rate, so it is + * rejected with a compile-time _Static_assert. Frequency is fixed at 100Hz; + * any other rate needs a deliberate driver change, not a runtime argument. + */ + +#ifndef NADIR_PIT_H +#define NADIR_PIT_H + +#include + +/* PIT input clock: crystal-derived base rate shared by all IBM-PC PITs. */ +#define PIT_BASE_HZ 1193182U +/* Driver tick rate: 100Hz = 10ms per tick. */ +#define PIT_HZ 100U +/* Divisor latched into channel 0 (1193182/100 = 11931). */ +#define PIT_DIVISOR (PIT_BASE_HZ / PIT_HZ) +/* 8259 PIC IRQ line carrying channel-0 ticks (remapped to vector 32). */ +#define PIT_IRQ 0U + +/* Program channel 0 for mode 3 square-wave at PIT_HZ and register + * pit_on_tick as the IRQ0 handler (if irq_register_handler is linked in). + * Ring 0 only. Idempotent: second and later calls do nothing. Leaves IF + * exactly as found; the caller decides when to sti. */ +void pit_init(void); + +/* IRQ0 handler: increments the tick counter. Called from IRQ context + * (interrupts already gated by the CPU/IRQ stub); must stay async-safe + * and never block. */ +void pit_on_tick(void); + +/* Monotonic tick count since pit_init (10ms per tick at 100Hz). Wraps only + * after ~5.8 billion years; unsigned subtraction stays correct across it. */ +uint64_t pit_ticks(void); + +/* Busy-wait `ticks` ticks using `hlt` to save power between interrupts. + * Requires interrupts enabled (IF=1) and the PIT IRQ unmasked, otherwise + * the CPU sleeps forever. Uses unsigned delta so a counter wrap mid-sleep + * still terminates. */ +void pit_sleep(uint64_t ticks); + +#endif /* NADIR_PIT_H */ diff --git a/kernel/arch/x86_64/pit.c b/kernel/arch/x86_64/pit.c new file mode 100644 index 0000000..8428212 --- /dev/null +++ b/kernel/arch/x86_64/pit.c @@ -0,0 +1,95 @@ +/* 8254 PIT timer driver (see pit.h). + * + * Programs channel 0 in mode 3 (square-wave generator) at 100Hz on IRQ0. + * + * Why 1193182Hz base: the PIT input clock is the IBM-PC crystal frequency + * (14.31818MHz / 12); every PC-compatible PIT ticks at this rate, so the + * divisor for N Hz is always 1193182 / N. + * Why mode 3: square-wave output stays high/low for half the period and + * retriggers the IRQ line cleanly each cycle; mode 2 (rate generator) + * emits narrow pulses that are fine too, but mode 3 is the conventional + * choice for periodic system ticks on channel 0. + * Why volatile + no locking: single-core kernel; s_ticks is written only + * in IRQ0 context and read in mainline code. `volatile` keeps the compiler + * from caching the counter across the hlt loop, and on x86-64 an aligned + * 64-bit load/store is atomic, so no spinlock is needed while IF gates the + * writer. + */ + +#include + +#include "idt.h" +#include "io.h" +#include "pit.h" + +#define PIT_CMD 0x43U +#define PIT_CH0 0x40U +/* Ch0, lobyte/hibyte access, mode 3 square-wave, binary (not BCD). */ +#define PIT_CMD_CH0_MODE3 0x36U + +/* Compile-time guard: a zero divisor would mean 65536 to the hardware and + * silently run at ~18Hz instead of PIT_HZ. Frequency is fixed on purpose. */ +_Static_assert(PIT_DIVISOR != 0, "PIT divisor must not be zero"); +_Static_assert(PIT_DIVISOR <= 0xFFFFU, "PIT divisor must fit in 16 bits"); + +/* Port I/O is owned by include/io.h (single owner); irq_register_handler + * is owned by idt.c via include/idt.h. */ + +/* Monotonic tick counter: written in IRQ context. */ +static volatile uint64_t s_ticks; +/* pit_init is idempotent; re-entry after the first programming is a no-op. */ +static uint8_t s_initialized; + +void pit_init(void) +{ + uint16_t divisor; + uint8_t lo; + uint8_t hi; + + if (s_initialized != 0) { + return; + } + + /* No cli/sti here on purpose: latch programming is two plain outb + * writes (safe with IF=0 at boot) and interrupt policy belongs to the + * caller (kmain integrator), which unmasks IRQ0 and stis afterwards. */ + divisor = (uint16_t)PIT_DIVISOR; + lo = (uint8_t)(divisor & 0xFFU); + hi = (uint8_t)((divisor >> 8) & 0xFFU); + + /* Latch order matters: command byte first selects ch0 + lobyte/hibyte + * mode, then the divisor follows low byte first, high byte second. */ + outb(PIT_CMD, PIT_CMD_CH0_MODE3); + outb(PIT_CH0, lo); + outb(PIT_CH0, hi); + + /* Strong idt.h registration: IRQ0 -> vector 32 wires pit_on_tick. */ + irq_register_handler(PIT_IRQ, pit_on_tick); + + s_initialized = 1; +} + +void pit_on_tick(void) +{ + /* Single increment async-safe, no logging or port I/O here. The PIC + * EOI is owned by the IRQ dispatcher not this driver. */ + s_ticks++; +} + +uint64_t pit_ticks(void) +{ + return s_ticks; +} + +void pit_sleep(uint64_t ticks) +{ + uint64_t start = s_ticks; + + /* hlt (not a spin loop) so the CPU sleeps until the next interrupt. + * Assumes IF=1 and IRQ0 unmasked; with IF=0 this would halt forever, + * which is why pit_init leaves interrupt policy to the caller. The + * unsigned delta keeps working if s_ticks wraps mid-sleep. */ + while ((s_ticks - start) < ticks) { + __asm__ volatile("hlt"); + } +} From b1b78bb5b9682a7bf5b4fa6f0f6408fa1d92db88 Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:26:10 +0200 Subject: [PATCH 03/10] Nadir 8259 PIC driver --- include/pic.h | 58 ++++++++++++++++++++++++++ kernel/arch/x86_64/pic.c | 90 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 include/pic.h create mode 100644 kernel/arch/x86_64/pic.c diff --git a/include/pic.h b/include/pic.h new file mode 100644 index 0000000..af41903 --- /dev/null +++ b/include/pic.h @@ -0,0 +1,58 @@ +/* Nadir 8259 PIC driver: remap, EOI, IRQ masking. + * + * Runs in 64-bit long mode at Ring 0 (CPL 0) with the stage-2 GDT. + * Privileged instructions: `outb`/`inb` (via io.h) trap with #GP(0) + * when CPL > IOPL, so every entry point below is Ring 0 only. + * Fault behavior: no CPU fault is possible beyond a programming bug; + * a wrong sequence masks the wrong IRQ or loses an EOI (hung line), + * never a triple fault by itself. IF stays cleared by the caller; + * this driver never executes `sti`/`cli`. + */ + +#ifndef NADIR_PIC_H +#define NADIR_PIC_H + +#include + +/* Command/data ports for the master (PIC1) and slave (PIC2). */ +#define PIC1_CMD 0x20U +#define PIC1_DATA 0x21U +#define PIC2_CMD 0xA0U +#define PIC2_DATA 0xA1U + +/* End-of-interrupt command byte. */ +#define PIC_EOI 0x20U + +/* Remapped vector bases: IRQ 0-7 -> vectors 32-39, IRQ 8-15 -> 40-47. */ +#define PIC_IRQ0_VECTOR 0x20U +#define PIC_IRQ8_VECTOR 0x28U + +/* Number of IRQ lines behind the cascaded 8259 pair. */ +#define PIC_IRQ_COUNT 16U + +/* Remap the cascaded 8259 pair to vectors 32-47 and mask everything. + * After reset IRQs overlap CPU exceptions 0-15, so without a + * remap a timer tick looks like a fault. Mask-all leaves each device + * opt-in via pic_unmask(). + * Preconditions: Ring 0, called once before idt_install_irqs(). + * Failure modes: none. */ +void pic_remap(void); + +/* Signal end-of-interrupt for IRQ line `irq` (0-15). + * WHY cascade order: the slave must be acked before the master or the + * master keeps the cascade line busy and IRQ 2 stops firing. + * Preconditions: Ring 0, `irq` < 16. + * Failure modes: out-of-range `irq` is ignored. */ +void pic_eoi(uint8_t irq); + +/* Mask (disable) IRQ line `irq` (0-15). + * Preconditions: Ring 0, `irq` < 16. + * Failure modes: out-of-range `irq` is ignored. */ +void pic_mask(uint8_t irq); + +/* Unmask (enable) IRQ line `irq` (0-15). + * Preconditions: Ring 0, `irq` < 16, PIC already remapped. + * Failure modes: out-of-range `irq` is ignored. */ +void pic_unmask(uint8_t irq); + +#endif /* NADIR_PIC_H */ diff --git a/kernel/arch/x86_64/pic.c b/kernel/arch/x86_64/pic.c new file mode 100644 index 0000000..d729a0f --- /dev/null +++ b/kernel/arch/x86_64/pic.c @@ -0,0 +1,90 @@ +/* Nadir 8259 PIC driver: cascaded master/slave remap + EOI + masking. + * + * Runs in 64-bit long mode at Ring 0 (CPL 0) with the stage-2 GDT. + * Privileged instructions: `outb`/`inb` via io.h (trap #GP if CPL > + * IOPL); no `cli`/`sti`/`lidt`. + * Fault behavior: no CPU fault possible a wrong ICW order only + * misroutes IRQs. + */ + +#include + +#include "io.h" +#include "pic.h" + +/* Init control words: ICW1 starts init, ICW2 sets the vector base, + * ICW3 wires the cascade (master IRQ2 <-> slave identity 2), ICW4 + * selects 8086 mode. io_wait() after each byte gives the slow 8259 + * time to latch it. */ +#define PIC_ICW1_INIT 0x11U +#define PIC_ICW3_MASTER_SLAVE_LINE 0x04U +#define PIC_ICW3_SLAVE_ID 0x02U +#define PIC_ICW4_8086 0x01U + +void pic_remap(void) +{ + /* WHY this exact order: the 8259 latches ICW2-4 only while in init + * mode, and the cascade identities must match or slave IRQs never + * reach the CPU. Mask-all at the end keeps devices quiet until + * their driver calls pic_unmask(). */ + outb(PIC1_CMD, PIC_ICW1_INIT); + io_wait(); + outb(PIC2_CMD, PIC_ICW1_INIT); + io_wait(); + outb(PIC1_DATA, PIC_IRQ0_VECTOR); + io_wait(); + outb(PIC2_DATA, PIC_IRQ8_VECTOR); + io_wait(); + outb(PIC1_DATA, PIC_ICW3_MASTER_SLAVE_LINE); + io_wait(); + outb(PIC2_DATA, PIC_ICW3_SLAVE_ID); + io_wait(); + outb(PIC1_DATA, PIC_ICW4_8086); + io_wait(); + outb(PIC2_DATA, PIC_ICW4_8086); + io_wait(); + outb(PIC1_DATA, 0xFFU); + io_wait(); + outb(PIC2_DATA, 0xFFU); + io_wait(); +} + +void pic_eoi(uint8_t irq) +{ + if (irq >= PIC_IRQ_COUNT) { + return; + } + + if (irq >= 8U) { + outb(PIC2_CMD, PIC_EOI); + } + outb(PIC1_CMD, PIC_EOI); +} + +void pic_mask(uint8_t irq) +{ + uint16_t port; + uint8_t mask; + + if (irq >= PIC_IRQ_COUNT) { + return; + } + + port = (irq < 8U) ? PIC1_DATA : PIC2_DATA; + mask = inb(port) | (uint8_t)(1U << (irq & 7U)); + outb(port, mask); +} + +void pic_unmask(uint8_t irq) +{ + uint16_t port; + uint8_t mask; + + if (irq >= PIC_IRQ_COUNT) { + return; + } + + port = (irq < 8U) ? PIC1_DATA : PIC2_DATA; + mask = inb(port) & (uint8_t) ~(1U << (irq & 7U)); + outb(port, mask); +} From 5d5f89d08ebbd040b4a0d9ed824b1320eb1d39af Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:29:30 +0200 Subject: [PATCH 04/10] Nadir PS/2 keyboard driver --- include/keyboard.h | 42 ++++++++ kernel/arch/x86_64/keyboard.c | 189 ++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 include/keyboard.h create mode 100644 kernel/arch/x86_64/keyboard.c diff --git a/include/keyboard.h b/include/keyboard.h new file mode 100644 index 0000000..3559926 --- /dev/null +++ b/include/keyboard.h @@ -0,0 +1,42 @@ +/* Nadir PS/2 keyboard: scancode set-1 IRQ1 driver with ASCII queue. + * + * Runs in 64-bit long mode at Ring 0 (CPL 0); uses `inb` on ports + * 0x60 (data) / 0x64 (status), which fault with #GP above CPL 0, so + * this driver is kernel-only. QEMU/SeaBIOS provides a PS/2 controller + * in set-1 mode; the 8259 routes IRQ1 to vector 33 after the PIC remap. + * Fault behavior: port I/O cannot fault at CPL 0. Reading 0x60 when + * the controller has no byte returns stale data, so keyboard_on_irq() + * must run only from IRQ1 (data-ready guaranteed). + * + * Locking: single core, one producer (IRQ1 handler) and one consumer + * (polling kmain). `volatile` head/tail plus single-byte loads/stores + * suffice — no locks, no atomics. IF stays cleared; the queue is both + * polling-safe and IRQ-safe under that model. + * + * Preconditions: call keyboard_init() once before use. It registers + * keyboard_on_irq() for IRQ1 (via irq_register_handler) and unmasks + * the line (via pic_unmask); the PIC remap, IRQ-gate install, and + * `sti` stay integrator duty, and IF stays cleared until then. + * Failure modes: unknown/extended/release codes are dropped silently; + * a full queue drops the newest byte; dequeue on empty returns 0. + */ + +#ifndef NADIR_KEYBOARD_H +#define NADIR_KEYBOARD_H + +/* Clear the queue and modifier state, register keyboard_on_irq() for + * IRQ1, and unmask IRQ1. */ +void keyboard_init(void); + +/* IRQ1 entry point: read one scancode from 0x60, translate, enqueue. + * Runs in interrupt context. */ +void keyboard_on_irq(void); + +/* Nonzero if at least one translated byte waits in the queue. */ +int keyboard_has_key(void); + +/* Remove and return the oldest byte, or 0 when the queue is empty. + * Backspace arrives as '\b', Enter as '\n'. */ +char keyboard_dequeue(void); + +#endif /* NADIR_KEYBOARD_H */ diff --git a/kernel/arch/x86_64/keyboard.c b/kernel/arch/x86_64/keyboard.c new file mode 100644 index 0000000..3c462d9 --- /dev/null +++ b/kernel/arch/x86_64/keyboard.c @@ -0,0 +1,189 @@ +/* PS/2 scancode set-1 IRQ1 driver (see keyboard.h). + * + * IRQ wiring uses the live framework: keyboard_init() registers + * keyboard_on_irq() via irq_register_handler() (idt.h) and unmasks + * line 1 via pic_unmask() (pic.h), both feature-detected with + * __has_include so this file still compiles if either header is + * absent (registration then falls back to caller duty). The driver + * itself never executes `sti`: IF stays cleared and IRQs only fire + * after the integrator remaps the PIC, installs the IRQ gates, and + * enables interrupts. 8259 IRQ1 -> vector 33. + */ + +#include + +#include "keyboard.h" + +/* Port I/O owned by include/io.h; no local inb duplicate. */ +#include "io.h" + +#if defined(__has_include) +#if __has_include("idt.h") +#include "idt.h" +#define NADIR_HAS_IRQ_REG 1 +#endif +#if __has_include("pic.h") +#include "pic.h" +#define NADIR_HAS_PIC 1 +#endif +#endif + +#define KBD_DATA 0x60U /* PS/2 data port: one scancode per IRQ1 */ +#define KBD_BUF_SIZE 128U /* power of two so wrap is cheap */ + +#define SC_RELEASE 0x80U /* high bit set = key release, ignore */ +#define SC_EXTENDED 0xE0U /* prefix for arrows/extended keys, ignore */ +#define SC_LSHIFT 0x2AU +#define SC_RSHIFT 0x36U +#define SC_CAPS 0x3AU + +/* US set-1 codes without Shift (0 = no printable mapping). Backspace + * 0x0E stays '\b' and Enter 0x1C stays '\n' so editing works raw. */ +static const char k_unshifted[128] = { + [0x02] = '1', [0x03] = '2', [0x04] = '3', [0x05] = '4', + [0x06] = '5', [0x07] = '6', [0x08] = '7', [0x09] = '8', + [0x0A] = '9', [0x0B] = '0', [0x0C] = '-', [0x0D] = '=', + [0x0E] = '\b', [0x0F] = '\t', + [0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r', + [0x14] = 't', [0x15] = 'y', [0x16] = 'u', [0x17] = 'i', + [0x18] = 'o', [0x19] = 'p', [0x1A] = '[', [0x1B] = ']', + [0x1C] = '\n', + [0x1E] = 'a', [0x1F] = 's', [0x20] = 'd', [0x21] = 'f', + [0x22] = 'g', [0x23] = 'h', [0x24] = 'j', [0x25] = 'k', + [0x26] = 'l', [0x27] = ';', [0x28] = '\'', [0x29] = '`', + [0x2B] = '\\', + [0x2C] = 'z', [0x2D] = 'x', [0x2E] = 'c', [0x2F] = 'v', + [0x30] = 'b', [0x31] = 'n', [0x32] = 'm', [0x33] = ',', + [0x34] = '.', [0x35] = '/', [0x39] = ' ', +}; + +/* Same codes with Shift held (letters stay lowercase here; the Shift + * + CapsLock XOR in keyboard_on_irq() uppercases them instead). */ +static const char k_shifted[128] = { + [0x02] = '!', [0x03] = '@', [0x04] = '#', [0x05] = '$', + [0x06] = '%', [0x07] = '^', [0x08] = '&', [0x09] = '*', + [0x0A] = '(', [0x0B] = ')', [0x0C] = '_', [0x0D] = '+', + [0x0E] = '\b', [0x0F] = '\t', + [0x10] = 'Q', [0x11] = 'W', [0x12] = 'E', [0x13] = 'R', + [0x14] = 'T', [0x15] = 'Y', [0x16] = 'U', [0x17] = 'I', + [0x18] = 'O', [0x19] = 'P', [0x1A] = '{', [0x1B] = '}', + [0x1C] = '\n', + [0x1E] = 'A', [0x1F] = 'S', [0x20] = 'D', [0x21] = 'F', + [0x22] = 'G', [0x23] = 'H', [0x24] = 'J', [0x25] = 'K', + [0x26] = 'L', [0x27] = ':', [0x28] = '"', [0x29] = '~', + [0x2B] = '|', + [0x2C] = 'Z', [0x2D] = 'X', [0x2E] = 'C', [0x2F] = 'V', + [0x30] = 'B', [0x31] = 'N', [0x32] = 'M', [0x33] = '<', + [0x34] = '>', [0x35] = '?', [0x39] = ' ', +}; + +/* Single-producer (IRQ) / single-consumer (poll) ring. `volatile` stops + * the compiler caching head/tail across the IRQ boundary; on one core + * with byte-sized index traffic no lock is needed because the producer + * only writes head (+buf slot) and the consumer only writes tail. */ +static char k_buf[KBD_BUF_SIZE]; +static volatile uint8_t k_head; +static volatile uint8_t k_tail; +/* IRQ-only state: touched solely in keyboard_on_irq(), so plain. */ +static uint8_t k_shift; +static uint8_t k_caps; +static uint8_t k_e0; + +/* Queue helpers below; port I/O comes from io.h. */ + +/* Drop-newest on full: keeps the consumer's prefix intact for editing. */ +static void enqueue(char c) +{ + uint8_t next = (uint8_t)((k_head + 1) % KBD_BUF_SIZE); + if (next == k_tail) { + return; + } + k_buf[k_head] = c; + k_head = next; +} + +void keyboard_init(void) +{ + k_head = 0; + k_tail = 0; + k_shift = 0; + k_caps = 0; + k_e0 = 0; +#ifdef NADIR_HAS_IRQ_REG + /* Route vector 33 -> keyboard_on_irq(); without idt.h this stays + * caller duty (manual irq_register_handler call). */ + irq_register_handler(1, keyboard_on_irq); +#endif +#ifdef NADIR_HAS_PIC + pic_unmask(1); +#endif +} + +void keyboard_on_irq(void) +{ + uint8_t code = inb(KBD_DATA); + + if (code == SC_EXTENDED) { + k_e0 = 1; + return; + } + if (k_e0 != 0) { + /* Extended make/break: swallow, maps to 0. */ + k_e0 = 0; + return; + } + /* only Shift release changes state the rest is ignored. */ + if ((code & SC_RELEASE) != 0) { + uint8_t make = (uint8_t)(code & (uint8_t)~SC_RELEASE); + if (make == SC_LSHIFT || make == SC_RSHIFT) { + k_shift = 0; + } + return; + } + if (code == SC_LSHIFT || code == SC_RSHIFT) { + k_shift = 1; + return; + } + /* CapsLock toggles on press only */ + if (code == SC_CAPS) { + k_caps = (uint8_t)(k_caps == 0 ? 1 : 0); + return; + } + if (code >= sizeof(k_unshifted)) { + return; + } + char base = k_unshifted[code]; + if (base == '\0') { + return; + } + char out = base; + if (base >= 'a' && base <= 'z') { + /* Shift XOR CapsLock uppercases letters. */ + if ((k_shift != 0) != (k_caps != 0)) { + out = (char)(base - 'a' + 'A'); + } + } else if (k_shift != 0) { + char shifted = k_shifted[code]; + if (shifted != '\0') { + out = shifted; + } + } else if (k_caps != 0) { + /* CapsLock alone must not shift digits/symbols */ + } + enqueue(out); +} + +int keyboard_has_key(void) +{ + return k_head != k_tail; +} + +char keyboard_dequeue(void) +{ + if (k_head == k_tail) { + return 0; + } + char c = k_buf[k_tail]; + k_tail = (uint8_t)((k_tail + 1) % KBD_BUF_SIZE); + return c; +} From 29c79f20c2636353855a6f8b4ea32b0dffd27344 Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:30:26 +0200 Subject: [PATCH 05/10] Nadir x86 port I/O primitives --- include/io.h | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 include/io.h diff --git a/include/io.h b/include/io.h new file mode 100644 index 0000000..19208fb --- /dev/null +++ b/include/io.h @@ -0,0 +1,47 @@ +/* Nadir x86 port I/O primitives (inb/outb/io_wait). + * + * Runs in 64-bit long mode at Ring 0 (CPL 0) with the stage-2 GDT. + * Privileged instructions: `in`/`out` trap with #GP(0) when CPL > IOPL, + * so these helpers are Ring 0 only and must never be exposed to + * userspace. Fault behavior: a bad port number cannot fault by itself + * (unused ports read back 0xFF or are ignored); a fault here means the + * caller programmed the wrong device, not a CPU trap. + * Interrupts stay disabled by the caller where timing matters; these + * helpers do not touch IF themselves. + */ + +#ifndef NADIR_IO_H +#define NADIR_IO_H + +#include + +/* Read one byte from ISA port `port`. + * Preconditions: CPL 0, port is a valid x86 I/O address (0-0xFFFF). + * Failure modes: none (returns bus value; 0xFF on unmapped ports). */ +static inline uint8_t inb(uint16_t port) +{ + uint8_t value; + + __asm__ volatile("inb %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +/* Write byte `value` to ISA port `port`. + * Preconditions: CPL 0, port is writable for this device. + * Failure modes: none from the CPU; wrong port = device misbehavior. */ +static inline void outb(uint16_t port, uint8_t value) +{ + __asm__ volatile("outb %0, %1" ::"a"(value), "Nd"(port)); +} + +/* Tiny delay for legacy ISA devices (writes to unused port 0x80). + * + * WHY: the 8259 PIC and other slow devices need ~1us between back to + * back command bytes; port 0x80 is the conventional throwaway delay. + * Preconditions: CPL 0. Failure modes: none. */ +static inline void io_wait(void) +{ + outb(0x80U, 0U); +} + +#endif /* NADIR_IO_H */ From 830613ccb65758ca92d8e0f397ac3d81895a0b2e Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:31:46 +0200 Subject: [PATCH 06/10] Added missing vectors --- include/idt.h | 29 +++++++++++++++++++++-- kernel/arch/x86_64/idt.c | 50 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/include/idt.h b/include/idt.h index 5b31dd7..0031cae 100644 --- a/include/idt.h +++ b/include/idt.h @@ -4,9 +4,12 @@ * Privileged primitive: `lidt` in idt_init(). Fault behavior: before * idt_init() any exception triple-faults (no IDT); after, CPU vectors * 0-31 land in the assembly stubs (isr.asm) and then in - * exception_handler(), which prints and halts. Vectors 32-255 are left - * zero (not present) on purpose because PIC remap + IRQ handling is a later step. + * exception_handler(), which prints and halts. Vectors 32-47 stay + * not-present until the 8259 PIC is remapped and idt_install_irqs() + * installs the IRQ gates; then they land in irq_dispatch(), which + * sends the PIC EOI and calls the per-IRQ handler (if any). * Interrupts stay disabled throughout (IF=0); exceptions still fire. + * IRQs only fire after the integrator enables them with `sti`. */ #ifndef NADIR_IDT_H @@ -17,6 +20,10 @@ /* Number of CPU exception vectors this base installs (0-31). */ #define IDT_EXCEPTION_COUNT 32 +/* First remapped IRQ vector (IRQ 0 -> vector 32) and IRQ line count. */ +#define IDT_IRQ_BASE 32 +#define IDT_IRQ_COUNT 16 + /* Stack layout on entry to exception_handler(). * * Built by the CPU (RIP/CS/RFLAGS, plus an error code on vectors @@ -63,4 +70,22 @@ void idt_init(void); * frame above. Prints vector/error/RIP and halts; never returns. */ void exception_handler(struct interrupt_frame *frame); +/* C entry point for vectors 32-47, called from irq_common with the + * same frame layout (vector = 32 + irq, error = 0 dummy). Runs at + * Ring 0 with IF cleared; sends the PIC EOI and calls the handler + * registered for that IRQ, if any. Spurious vectors outside 32-47 + * are ignored. */ +void irq_dispatch(struct interrupt_frame *frame); + +/* Register `handler` for IRQ line `irq` (0-15); NULL unregisters. + * Preconditions: Ring 0, `irq` < 16 (out-of-range calls ignored). + * Failure modes: none; takes effect on the next IRQ after return. */ +void irq_register_handler(uint8_t irq, void (*handler)(void)); + +/* Install the 16 IRQ gates (vectors 32-47 -> irq stubs) with selector + * 0x18 and flags 0x8E (present, DPL 0, 64-bit interrupt gate). + * Preconditions: Ring 0, idt_init() already ran, PIC already remapped + * via pic_remap(). Leaves IF cleared. Failure modes: none. */ +void idt_install_irqs(void); + #endif /* NADIR_IDT_H */ diff --git a/kernel/arch/x86_64/idt.c b/kernel/arch/x86_64/idt.c index e6bd4e3..df3dd09 100644 --- a/kernel/arch/x86_64/idt.c +++ b/kernel/arch/x86_64/idt.c @@ -4,14 +4,17 @@ * (kernel code selector 0x18). Privileged primitive: `lidt` in * idt_init(). Fault behavior: before idt_init() any fault * triple-faults; after, vectors 0-31 print diagnostics and halt. - * Vectors 32-255 stay not-present until the PIC is remapped. - * IF stays cleared; exceptions fire regardless of IF. + * Vectors 32-47 stay not-present until pic_remap() + + * idt_install_irqs() install the IRQ gates; vectors 48-255 stay + * not-present (no APIC yet). IF stays cleared; exceptions fire + * regardless of IF, IRQs only after the integrator runs `sti`. */ #include #include "console.h" #include "idt.h" +#include "pic.h" /* 64-bit interrupt gate descriptor (Intel SDM Vol. 3, IDT gate). */ struct idt_entry { @@ -42,6 +45,14 @@ static struct idtr idtr; /* Stub addresses from isr.asm, one per exception vector 0-31. */ extern uint64_t isr_stub_table[IDT_EXCEPTION_COUNT]; +/* Stub addresses from isr.asm, one per remapped IRQ vector 32-47. */ +extern uint64_t irq_stub_table[IDT_IRQ_COUNT]; + +/* Per-IRQ handlers for lines 0-15; NULL means "EOI only". + * WHY a plain table: one subsystem per file — device drivers own + * their logic and only register a callback here. BSS-zeroed. */ +static void (*irq_handlers[IDT_IRQ_COUNT])(void); + /* Install one gate: handler address + segment + IST index + flags. */ static void idt_set_gate(uint8_t vector, uint64_t handler, uint16_t selector, uint8_t ist, uint8_t flags) @@ -86,6 +97,41 @@ void idt_init(void) __asm__ volatile("lidt %0" ::"m"(idtr) : "memory"); } +void idt_install_irqs(void) +{ + for (uint16_t i = 0; i < IDT_IRQ_COUNT; i++) { + idt_set_gate((uint8_t)(IDT_IRQ_BASE + i), irq_stub_table[i], + KERNEL_CS, 0, IDT_FLAG_PRESENT_INT); + } +} + +void irq_register_handler(uint8_t irq, void (*handler)(void)) +{ + if (irq >= IDT_IRQ_COUNT) { + return; + } + irq_handlers[irq] = handler; +} + +void irq_dispatch(struct interrupt_frame *frame) +{ + uint64_t vector = frame->vector; + uint8_t irq; + + if (vector < IDT_IRQ_BASE || vector >= IDT_IRQ_BASE + IDT_IRQ_COUNT) { + return; + } + irq = (uint8_t)(vector - IDT_IRQ_BASE); + + /* WHY handler before EOI: the EOI re-arms the (edge-triggered) PIC + * line, so acking first would let the same IRQ re-enter before the + * driver finished. A NULL slot just gets an EOI. */ + if (irq_handlers[irq] != 0) { + irq_handlers[irq](); + } + pic_eoi(irq); +} + void exception_handler(struct interrupt_frame *frame) { uint64_t vector = frame->vector; From a60338054a4ad9a3d398481eefa1f12076087e2e Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:32:05 +0200 Subject: [PATCH 07/10] Added missing vectors --- kernel/arch/x86_64/isr.asm | 90 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/kernel/arch/x86_64/isr.asm b/kernel/arch/x86_64/isr.asm index 6df5da1..809e9a4 100644 --- a/kernel/arch/x86_64/isr.asm +++ b/kernel/arch/x86_64/isr.asm @@ -16,7 +16,9 @@ bits 64 section .text global isr_stub_table +global irq_stub_table extern exception_handler +extern irq_dispatch ; Vectors without a CPU error code: normalize with a dummy 0. %macro ISR_NOERR 1 @@ -105,3 +107,91 @@ isr_stub_table: dq isr_stub_20, isr_stub_21, isr_stub_22, isr_stub_23 dq isr_stub_24, isr_stub_25, isr_stub_26, isr_stub_27 dq isr_stub_28, isr_stub_29, isr_stub_30, isr_stub_31 + +; --------------------------------------------------------------------------- +; Nadir IRQ stubs: vectors 32-47 (remapped 8259 IRQs 0-15) -> irq_dispatch. +; +; Same Ring 0 / CPL 0 entry context as the exception stubs above: the CPU +; entered with IF cleared (interrupt gate), no privilege change, so only +; RIP/CS/RFLAGS were pushed. IRQs never carry a CPU error code, so every +; stub normalizes with a dummy 0 exactly like ISR_NOERR, keeping the +; [vector, error] layout — and thus struct interrupt_frame — identical to +; the exception path. Field order must stay in sync with include/idt.h. +; The EOI is sent by the C dispatcher (pic_eoi), not here, so a future +; per-IRQ EOI policy only touches C. -mno-red-zone keeps pushes safe. + +section .text +%macro IRQ_NOERR 1 +global isr_stub_%1 +isr_stub_%1: + push 0 + push %1 + jmp irq_common +%endmacro + +IRQ_NOERR 32 +IRQ_NOERR 33 +IRQ_NOERR 34 +IRQ_NOERR 35 +IRQ_NOERR 36 +IRQ_NOERR 37 +IRQ_NOERR 38 +IRQ_NOERR 39 +IRQ_NOERR 40 +IRQ_NOERR 41 +IRQ_NOERR 42 +IRQ_NOERR 43 +IRQ_NOERR 44 +IRQ_NOERR 45 +IRQ_NOERR 46 +IRQ_NOERR 47 + +; Common IRQ entry: save GPRs, call void irq_dispatch(frame in RDI), +; restore GPRs, drop [vector, error], return to the interrupted code. +; Unlike isr_common (which halts), this path must iretq: IRQs are +; resumable. Interrupts stay disabled throughout (IF=0 on entry via an +; interrupt gate; irq_dispatch runs with IF cleared; iretq restores the +; caller's RFLAGS). +irq_common: + push rax + push rbx + push rcx + push rdx + push rsi + push rdi + push rbp + push r8 + push r9 + push r10 + push r11 + push r12 + push r13 + push r14 + push r15 + mov rdi, rsp + call irq_dispatch + pop r15 + pop r14 + pop r13 + pop r12 + pop r11 + pop r10 + pop r9 + pop r8 + pop rbp + pop rdi + pop rsi + pop rdx + pop rcx + pop rbx + pop rax + add rsp, 16 + iretq + +section .rodata +align 8 +irq_stub_table: + dq isr_stub_32, isr_stub_33, isr_stub_34, isr_stub_35 + dq isr_stub_36, isr_stub_37, isr_stub_38, isr_stub_39 + dq isr_stub_40, isr_stub_41, isr_stub_42, isr_stub_43 + dq isr_stub_44, isr_stub_45, isr_stub_46, isr_stub_47 From 0727a2c1d419d14dc94194c0078862328695eaa1 Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:38:04 +0200 Subject: [PATCH 08/10] fix: backspace behavior --- kernel/core/kmain.c | 135 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 131 insertions(+), 4 deletions(-) diff --git a/kernel/core/kmain.c b/kernel/core/kmain.c index 3152165..9d984e5 100644 --- a/kernel/core/kmain.c +++ b/kernel/core/kmain.c @@ -2,30 +2,157 @@ * * Called once from the stage-2 entry stub after the CPU is in 64-bit long * mode with a valid stack, zeroed BSS and identity-mapped memory. Runs at - * Ring 0 (CPL 0) interrupts disabled. Installs the exception IDT first - * so from then on a fault prints diagnostics instead of triple-faulting. - * It never returns; on return the caller halts the CPU. + * Ring 0 (CPL 0) with IF=0 on entry under the stage-2 GDT (CS 0x18); + * `-mno-red-zone` keeps IRQ pushes off the red zone so interrupt frames + * are safe. Installs the exception IDT first so from then on a fault + * prints diagnostics instead of triple-faulting. `sti` runs exactly once + * after every device is programmed; the main loop keeps IF=1 and sleeps + * in `hlt` (which wakes on each IRQ) — never `cli`-parks, or timer ticks + * and keystrokes would stop. It never returns; on return the caller halts. */ +#include + #include "console.h" #include "idt.h" +#include "keyboard.h" +#include "pic.h" +#include "pit.h" +#include "serial.h" /* A value that only comes out right if the CPU truly executes 64-bit * arithmetic: every nibble survives the shifts above 32 bits. */ #define PROOF_VALUE 0x0123456789ABCDEFULL +/* Print an unsigned decimal via console_putchar (no libc printf). */ +static void print_dec(uint64_t v) +{ + char buf[20]; + int n = 0; + + if (v == 0) { + console_putchar('0'); + return; + } + while (v != 0 && n < (int)sizeof(buf)) { + buf[n++] = (char)('0' + (v % 10U)); + v /= 10U; + } + while (n > 0) { + n--; + console_putchar(buf[n]); + } +} + +/* Mirror one echoed keystroke to COM1 when the UART looks present. */ +static void serial_echo(char c, int ok) +{ + char tmp[2]; + + if (ok == 0) { + return; + } + if (c == '\b') { + /* Erase on a serial terminal: back, blank, back. */ + serial_puts("\b \b"); + } else if (c == '\n') { + serial_puts("\r\n"); + } else { + tmp[0] = c; + tmp[1] = '\0'; + serial_puts(tmp); + } +} + void kmain(void) { + int serial_present; + uint64_t last_uptime; + console_clear(); console_puts("Hello World from Nadir Kernel !\n"); console_puts("Running C code in 64-bit long mode (Ring 0).\n"); + + /* WHY this order: pic_remap must precede idt_install_irqs so IRQ + * gates land on valid remapped vectors 32-47 (pre-remap they would + * overlap CPU exceptions 0-15 and a tick would look like a fault); + * devices (serial/PIT/keyboard) are programmed before sti so no IRQ + * fires into a half-wired handler; pic_remap masks everything and + * each device opts in via pic_unmask, so stray lines stay silent. */ idt_init(); console_puts("IDT installed (vectors 0-31).\n"); + + pic_remap(); + console_puts("PIC remapped (IRQ 0-15 -> vectors 32-47, masked).\n"); + + idt_install_irqs(); + console_puts("IRQ gates installed (vectors 32-47).\n"); + + serial_init(); + serial_present = serial_ok(); + console_puts("Serial COM1 38400 8N1 ready.\n"); + if (serial_present != 0) { + serial_puts("Hello World from Nadir Kernel !\n"); + serial_puts("Serial COM1 38400 8N1 ready.\n"); + } + + pit_init(); + console_puts("PIT 100Hz on IRQ0 ready.\n"); + if (serial_present != 0) { + serial_puts("PIT 100Hz on IRQ0 ready.\n"); + } + + keyboard_init(); + console_puts("Keyboard PS/2 IRQ1 ready.\n"); + if (serial_present != 0) { + serial_puts("Keyboard PS/2 IRQ1 ready.\n"); + } + + /* Explicit opt-in for the two live lines; idempotent even though + * keyboard_init already unmasked IRQ1. */ + pic_unmask(0); + pic_unmask(1); + + __asm__ volatile("sti"); + console_puts("Interrupts enabled (IRQs 0-1).\n"); + if (serial_present != 0) { + serial_puts("Interrupts enabled (IRQs 0-1).\n"); + } + console_puts("64-bit proof value: "); console_puthex64(PROOF_VALUE); - console_puts("\nHalting.\n"); + console_puts("\nHalting in hlt loop (timer + keyboard live).\n"); + /* Main loop: hlt sleeps until the next IRQ (IF=1 throughout), then + * one poll pass prints uptime ~1/s and drains echoed keys. */ + last_uptime = 0; for (;;) { + uint64_t now; + __asm__ volatile("hlt"); + now = pit_ticks(); + if ((now - last_uptime) >= 100U) { + uint64_t secs = now / 100U; + uint64_t tenths = (now / 10U) % 10U; + + last_uptime = now; + console_puts("uptime "); + print_dec(secs); + console_puts("."); + print_dec(tenths); + console_puts("s (ticks "); + console_puthex64(now); + console_puts(")\n"); + if (serial_present != 0) { + serial_puts("uptime (see VGA for value)\n"); + } + } + while (keyboard_has_key() != 0) { + char c = keyboard_dequeue(); + + /* console_putchar erases on '\b' and wraps on '\n'. */ + console_putchar(c); + serial_echo(c, serial_present); + } } } From 06d63769ad500b938ee11b0e6c059d3e76ebd878 Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:38:22 +0200 Subject: [PATCH 09/10] Updated Makefile --- Makefile | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2408a9d..0e2dce7 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ CFLAGS := -std=c17 -ffreestanding -nostdlib -fno-builtin \ -fno-pic -fno-pie -fno-stack-protector \ -Wall -Wextra -Werror -O2 -Iinclude -OBJS := $(BUILD)/entry.o $(BUILD)/isr.o $(BUILD)/kmain.o $(BUILD)/console.o $(BUILD)/idt.o +OBJS := $(BUILD)/entry.o $(BUILD)/isr.o $(BUILD)/kmain.o $(BUILD)/console.o $(BUILD)/idt.o $(BUILD)/pic.o $(BUILD)/pit.o $(BUILD)/serial.o $(BUILD)/keyboard.o all: $(IMG) @@ -33,13 +33,25 @@ $(BUILD)/entry.o: $(ARCH_DIR)/entry.asm | $(BUILD) $(BUILD)/isr.o: $(ARCH_DIR)/isr.asm | $(BUILD) $(NASM) -f elf64 $< -o $@ -$(BUILD)/kmain.o: kernel/core/kmain.c include/console.h include/idt.h | $(BUILD) +$(BUILD)/kmain.o: kernel/core/kmain.c include/console.h include/idt.h include/pic.h include/pit.h include/serial.h include/keyboard.h | $(BUILD) $(CC) $(CFLAGS) -c $< -o $@ $(BUILD)/console.o: $(ARCH_DIR)/console.c include/console.h | $(BUILD) $(CC) $(CFLAGS) -c $< -o $@ -$(BUILD)/idt.o: $(ARCH_DIR)/idt.c include/idt.h include/console.h | $(BUILD) +$(BUILD)/idt.o: $(ARCH_DIR)/idt.c include/idt.h include/console.h include/pic.h | $(BUILD) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/pic.o: $(ARCH_DIR)/pic.c include/pic.h include/io.h | $(BUILD) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/pit.o: $(ARCH_DIR)/pit.c include/pit.h include/idt.h include/io.h | $(BUILD) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/serial.o: $(ARCH_DIR)/serial.c include/serial.h include/io.h | $(BUILD) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/keyboard.o: $(ARCH_DIR)/keyboard.c include/keyboard.h include/idt.h include/pic.h include/io.h | $(BUILD) $(CC) $(CFLAGS) -c $< -o $@ $(BUILD)/kernel.elf: $(OBJS) $(ARCH_DIR)/link.ld From 5c93703cec8b52408fcbeb8625cc4f2abbe551ba Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:38:45 +0200 Subject: [PATCH 10/10] fix: backspace behavior --- kernel/arch/x86_64/console.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/kernel/arch/x86_64/console.c b/kernel/arch/x86_64/console.c index 6e13ef6..dd3a9a4 100644 --- a/kernel/arch/x86_64/console.c +++ b/kernel/arch/x86_64/console.c @@ -46,6 +46,21 @@ void console_putchar(char c) if (c == '\n') { cursor_row++; cursor_col = 0; + } else if (c == '\b') { + /* kmain echoes keyboard '\b' for editing. Without this the + * raw 0x08 glyph would print instead of erasing. Move back one + * cell and blank it (IRQ-safe: mainline only, IF=1 but the + * IRQ handlers never touch VGA). */ + if (cursor_col > 0) { + cursor_col--; + } else if (cursor_row > 0) { + cursor_row--; + cursor_col = VGA_WIDTH - 1; + } else { + return; + } + vga[cursor_row * VGA_WIDTH + cursor_col] = + (uint16_t)((COLOR_DEFAULT << 8) | ' '); } else { vga[cursor_row * VGA_WIDTH + cursor_col] = (uint16_t)((COLOR_DEFAULT << 8) | c);