Merge pull request #2 from Plexi09/feature/idt-base

Installs an exceptions-only IDT base: 32 CPU-exception gates (interrupt gates, DPL 0, selector 0x18) with NASM stubs + a C exception_handler that prints vector/error/RIP/CS/RFLAGS and halts. kmain calls idt_init() early, so faults print diagnostics instead of triple-faulting. PIC remap + IRQs are intentionally left for a follow-up
This commit is contained in:
Pierre-Emmanuel Gob. 2026-09-18 12:17:01 +02:00 • committed by GitHub
commit 7ee953925a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 301 additions and 4 deletions

View file

@ -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)/kmain.o $(BUILD)/console.o
OBJS := $(BUILD)/entry.o $(BUILD)/isr.o $(BUILD)/kmain.o $(BUILD)/console.o $(BUILD)/idt.o
all: $(IMG)
@ -30,12 +30,18 @@ $(BUILD):
$(BUILD)/entry.o: $(ARCH_DIR)/entry.asm | $(BUILD)
$(NASM) -f elf64 $< -o $@
$(BUILD)/kmain.o: kernel/core/kmain.c include/console.h | $(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)
$(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)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel.elf: $(OBJS) $(ARCH_DIR)/link.ld
$(LD) -m elf_x86_64 -T $(ARCH_DIR)/link.ld -o $@ $(OBJS)

66
include/idt.h Normal file
View file

@ -0,0 +1,66 @@
/* Nadir interrupt IDT + CPU exception entry points.
*
* Runs in 64-bit long mode at Ring 0 (CPL 0) with the stage-2 GDT.
* 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.
* Interrupts stay disabled throughout (IF=0); exceptions still fire.
*/
#ifndef NADIR_IDT_H
#define NADIR_IDT_H
#include <stdint.h>
/* Number of CPU exception vectors this base installs (0-31). */
#define IDT_EXCEPTION_COUNT 32
/* Stack layout on entry to exception_handler().
*
* Built by the CPU (RIP/CS/RFLAGS, plus an error code on vectors
* 8, 10-14, 17, 21) plus isr.asm: the stub normalizes the stack to
* [vector, error] and isr_common pushes the 15 general-purpose
* registers. RDI points at this struct (System V first argument).
* Field order matches the push order in isr_common — keep in sync.
*/
struct interrupt_frame {
uint64_t r15;
uint64_t r14;
uint64_t r13;
uint64_t r12;
uint64_t r11;
uint64_t r10;
uint64_t r9;
uint64_t r8;
uint64_t rbp;
uint64_t rdi;
uint64_t rsi;
uint64_t rdx;
uint64_t rcx;
uint64_t rbx;
uint64_t rax;
/* Pushed by the stub: exception vector 0-31. */
uint64_t vector;
/* CPU-pushed error code, or 0 inserted by the stub. */
uint64_t error;
/* Pushed by the CPU. */
uint64_t rip;
uint64_t cs;
uint64_t rflags;
/* Pushed by the CPU only on a privilege change; garbage for
* kernel-mode faults */
uint64_t rsp;
uint64_t ss;
};
/* Build the IDT (vectors 0-31 -> isr stubs) and load it with `lidt`.
* Ring 0 only. Leaves IF cleared; returns with exceptions catchable. */
void idt_init(void);
/* C entry point for vectors 0-31, called from isr_common with the
* frame above. Prints vector/error/RIP and halts; never returns. */
void exception_handler(struct interrupt_frame *frame);
#endif /* NADIR_IDT_H */

115
kernel/arch/x86_64/idt.c Normal file
View file

@ -0,0 +1,115 @@
/* Nadir IDT base: 32 CPU-exception gates + fault printer.
*
* Runs in 64-bit long mode at Ring 0 (CPL 0) with the stage-2 GDT
* (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.
*/
#include <stdint.h>
#include "console.h"
#include "idt.h"
/* 64-bit interrupt gate descriptor (Intel SDM Vol. 3, IDT gate). */
struct idt_entry {
uint16_t offset_low;
uint16_t selector;
uint8_t ist;
uint8_t type_attr;
uint16_t offset_mid;
uint32_t offset_high;
uint32_t reserved;
} __attribute__((packed));
/* IDTR image for `lidt`. */
struct idtr {
uint16_t limit;
uint64_t base;
} __attribute__((packed));
/* Gate flags: present, DPL 0, 64-bit interrupt gate (0xE). */
#define IDT_FLAG_PRESENT_INT 0x8EU
/* Kernel code segment from the stage-2 GDT (entry.asm). */
#define KERNEL_CS 0x18U
/* Full 256-entry table, 16-byte aligned as the CPU prefers. BSS. */
static struct idt_entry idt[256] __attribute__((aligned(16)));
static struct idtr idtr;
/* Stub addresses from isr.asm, one per exception vector 0-31. */
extern uint64_t isr_stub_table[IDT_EXCEPTION_COUNT];
/* 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)
{
struct idt_entry *e = &idt[vector];
e->offset_low = (uint16_t)(handler & 0xFFFFU);
e->selector = selector;
e->ist = ist & 0x07U;
e->type_attr = flags;
e->offset_mid = (uint16_t)((handler >> 16) & 0xFFFFU);
e->offset_high = (uint32_t)((handler >> 32) & 0xFFFFFFFFU);
e->reserved = 0;
}
/* Human-readable names for vectors 0-31 (Intel SDM Vol. 3). */
static const char *const exception_names[IDT_EXCEPTION_COUNT] = {
"Divide Error (#DE)", "Debug Exception (#DB)", "NMI",
"Breakpoint (#BP)", "Overflow (#OF)", "Bound Range (#BR)",
"Invalid Opcode (#UD)", "Device Not Available (#NM)",
"Double Fault (#DF)", "Coprocessor Segment Overrun",
"Invalid TSS (#TS)", "Segment Not Present (#NP)",
"Stack-Segment Fault (#SS)", "General Protection (#GP)",
"Page Fault (#PF)", "Reserved (15)",
"x87 Floating-Point (#MF)", "Alignment Check (#AC)",
"Machine Check (#MC)", "SIMD Floating-Point (#XM)",
"Virtualization (#VE)", "Control Protection (#CP)",
"Reserved (22)", "Reserved (23)", "Reserved (24)", "Reserved (25)",
"Reserved (26)", "Reserved (27)", "Hypervisor Injection (#HV)",
"VMM Communication (#VC)", "Security Exception (#SX)", "Reserved (31)",
};
void idt_init(void)
{
for (uint16_t i = 0; i < IDT_EXCEPTION_COUNT; i++) {
idt_set_gate((uint8_t)i, isr_stub_table[i], KERNEL_CS, 0,
IDT_FLAG_PRESENT_INT);
}
idtr.limit = (uint16_t)(sizeof(idt) - 1);
idtr.base = (uint64_t)&idt[0];
__asm__ volatile("lidt %0" ::"m"(idtr) : "memory");
}
void exception_handler(struct interrupt_frame *frame)
{
uint64_t vector = frame->vector;
const char *name = "Unknown Exception";
if (vector < IDT_EXCEPTION_COUNT) {
name = exception_names[vector];
}
console_puts("\n*** EXCEPTION: ");
console_puts(name);
console_puts(" (vector ");
console_puthex64(vector);
console_puts(") ***\nerror: ");
console_puthex64(frame->error);
console_puts(" rip: ");
console_puthex64(frame->rip);
console_puts(" cs: ");
console_puthex64(frame->cs);
console_puts(" rflags: ");
console_puthex64(frame->rflags);
console_puts("\nSystem halted.\n");
for (;;) {
__asm__ volatile("cli; hlt");
}
}

107
kernel/arch/x86_64/isr.asm Normal file
View file

@ -0,0 +1,107 @@
; Nadir exception stubs: vectors 0-31 -> exception_handler.
;
; Runs in 64-bit long mode at Ring 0 (CPL 0). Entered by the CPU with
; IF cleared (interrupt gate); no privilege change, so the CPU pushed
; RIP/CS/RFLAGS plus an error code only on vectors 8, 10-14, 17, 21.
; Fault behavior: a fault here (before the IDT is loaded, or inside the
; handler) triple-faults. Never returns: the C handler halts.
;
; Contract with include/idt.h `struct interrupt_frame`: each stub leaves
; [vector, error] on the stack (pushing a dummy 0 where the CPU did not),
; then isr_common pushes the 15 GPRs and calls the C handler with RDI
; pointing at the frame. Field order must stay in sync. Interrupts stay
; disabled; -mno-red-zone makes the pushes safe.
bits 64
section .text
global isr_stub_table
extern exception_handler
; Vectors without a CPU error code: normalize with a dummy 0.
%macro ISR_NOERR 1
global isr_stub_%1
isr_stub_%1:
push 0
push %1
jmp isr_common
%endmacro
; Vectors where the CPU already pushed an error code: only push the vector.
%macro ISR_ERR 1
global isr_stub_%1
isr_stub_%1:
push %1
jmp isr_common
%endmacro
ISR_NOERR 0
ISR_NOERR 1
ISR_NOERR 2
ISR_NOERR 3
ISR_NOERR 4
ISR_NOERR 5
ISR_NOERR 6
ISR_NOERR 7
ISR_ERR 8
ISR_NOERR 9
ISR_ERR 10
ISR_ERR 11
ISR_ERR 12
ISR_ERR 13
ISR_ERR 14
ISR_NOERR 15
ISR_NOERR 16
ISR_ERR 17
ISR_NOERR 18
ISR_NOERR 19
ISR_NOERR 20
ISR_ERR 21
ISR_NOERR 22
ISR_NOERR 23
ISR_NOERR 24
ISR_NOERR 25
ISR_NOERR 26
ISR_NOERR 27
ISR_NOERR 28
ISR_NOERR 29
ISR_NOERR 30
ISR_NOERR 31
; Common entry: save GPRs, call void exception_handler(frame in RDI), halt
; if it ever returns (it must not).
isr_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 exception_handler
; Unreachable in practice; park the CPU if the handler returns.
cli
.hang:
hlt
jmp .hang
section .rodata
align 8
isr_stub_table:
dq isr_stub_0, isr_stub_1, isr_stub_2, isr_stub_3
dq isr_stub_4, isr_stub_5, isr_stub_6, isr_stub_7
dq isr_stub_8, isr_stub_9, isr_stub_10, isr_stub_11
dq isr_stub_12, isr_stub_13, isr_stub_14, isr_stub_15
dq isr_stub_16, isr_stub_17, isr_stub_18, isr_stub_19
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

View file

@ -2,12 +2,13 @@
*
* 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, no IDT yet: any fault triple-faults,
* so this function proves the whole boot chain simply by running.
* 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.
*/
#include "console.h"
#include "idt.h"
/* A value that only comes out right if the CPU truly executes 64-bit
* arithmetic: every nibble survives the shifts above 32 bits. */
@ -18,6 +19,8 @@ void kmain(void)
console_clear();
console_puts("Hello World from Nadir Kernel !\n");
console_puts("Running C code in 64-bit long mode (Ring 0).\n");
idt_init();
console_puts("IDT installed (vectors 0-31).\n");
console_puts("64-bit proof value: ");
console_puthex64(PROOF_VALUE);
console_puts("\nHalting.\n");