diff --git a/Makefile b/Makefile index ec51135..c963336 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 $(BUILD)/pic.o $(BUILD)/pit.o $(BUILD)/serial.o $(BUILD)/keyboard.o +OBJS := $(BUILD)/entry.o $(BUILD)/isr.o $(BUILD)/kmain.o $(BUILD)/klog.o $(BUILD)/console.o $(BUILD)/idt.o $(BUILD)/pic.o $(BUILD)/pit.o $(BUILD)/serial.o $(BUILD)/keyboard.o $(BUILD)/pmm.o all: $(IMG) @@ -33,7 +33,13 @@ $(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 include/pic.h include/pit.h include/serial.h include/keyboard.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 include/pmm.h include/e820.h include/klog.h | $(BUILD) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/klog.o: kernel/core/klog.c include/klog.h include/console.h include/pit.h | $(BUILD) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/pmm.o: kernel/mm/pmm.c include/pmm.h include/e820.h | $(BUILD) $(CC) $(CFLAGS) -c $< -o $@ $(BUILD)/console.o: $(ARCH_DIR)/console.c include/console.h include/io.h | $(BUILD) diff --git a/include/e820.h b/include/e820.h new file mode 100644 index 0000000..9f87c08 --- /dev/null +++ b/include/e820.h @@ -0,0 +1,34 @@ +/* Nadir BIOS E820 memory-map entry (int 0x15, eax=0xE820). + * + * Staged by the stage-1 boot sector in 16-bit real mode at Ring 0 (CPL 0) + * into low memory (0x5000 count, 0x5020 entries) while BIOS calls still + * work, then copied by the stage-2 entry into the .bss globals + * memmap_count / memmap_entries owned by the PMM. Runs under the + * identity-mapped first 1GB, so physical == virtual addresses here. + * Fault behavior: no IDT exists during staging/copy, so a bad pointer + * triple-faults; the entry count is clamped to E820_MAX on both ends. + */ + +#ifndef NADIR_E820_H +#define NADIR_E820_H + +#include + +/* Max entries the boot sector stages and the kernel keeps. */ +#define E820_MAX 64 + +/* Usable-RAM type in the E820 type field. */ +#define E820_USABLE 1 + +/* One 24-byte BIOS E820 entry: byte-based [base, base+length) range. + * Preconditions: filled by BIOS int 0x15 or by the boot-sector fallback + * (single type-1 region at 1MB); length may be 0 (ignored by the PMM). + * Failure modes: none in C — a corrupt table only mis-marks frames. */ +struct e820_entry { + uint64_t base; + uint64_t length; + uint32_t type; + uint32_t acpi_attr; +} __attribute__((packed)); + +#endif /* NADIR_E820_H */ diff --git a/include/klog.h b/include/klog.h new file mode 100644 index 0000000..d2ae44c --- /dev/null +++ b/include/klog.h @@ -0,0 +1,48 @@ +/* Nadir boot logger: timestamped `[ 0.000000] sub: msg` lines. + * + * Runs in 64-bit long mode at Ring 0 (CPL 0) on the VGA console via + * console_putchar, which is polling-only and IRQ-safe by construction. + * + * Timestamp source: pit_ticks() / PIT_HZ (100Hz, 10ms per tick), integer + * math only. At 100Hz one + * tick is 10ms, so the 6-digit fraction only changes in steps of 10000 + * (e.g. 0.010000); the last 4 digits are always zero by construction. + * Early boot note: IF=0 until kmain runs `sti`, so the PIT IRQ cannot fire + * and ticks stay frozen at 0. + * + * IF/IRQ safety: mainline only, never IRQ context. The PIT IRQ handler + * (pit_on_tick) must never call this. It only bumps the tick counter. + * Single-core kernel, so no locking is needed. + * + * Preconditions: console is usable (identity-mapped VGA); pit_ticks() is + * linked in (BSS-zeroed counter reads 0 before pit_init). + * Failure modes: none + */ + +#ifndef NADIR_KLOG_H +#define NADIR_KLOG_H + +#include + +/* Print one full line: `[ 0.000000] subsystem: msg` + '\n'. */ +void klog(const char *subsystem, const char *msg); + +/* Piecewise line builder for values (PMM stats, uptime): prints the + * `[ 0.000000] subsystem: ` prefix with no newline; append with + * klog_str/dec/hex64, then close with klog_end() ('\n'). + * Preconditions: calls nest as begin -> (str|dec|hex64)* -> end. */ +void klog_begin(const char *subsystem); + +/* Append a NUL-terminated string to the open line. */ +void klog_str(const char *s); + +/* Append an unsigned decimal value (no libc printf). */ +void klog_dec(uint64_t v); + +/* Append a 64-bit value as 0x-prefixed, zero-padded hexadecimal. */ +void klog_hex64(uint64_t v); + +/* Terminate the open line with '\n'. */ +void klog_end(void); + +#endif /* NADIR_KLOG_H */ diff --git a/include/pmm.h b/include/pmm.h new file mode 100644 index 0000000..2adebef --- /dev/null +++ b/include/pmm.h @@ -0,0 +1,53 @@ +/* Nadir physical memory manager + * + * Runs in 64-bit long mode at Ring 0 (CPL 0) under the stage-2 identity + * map; physical addresses are used as-is. Called once from kmain with + * interrupts disabled (IF=0), before sti, so no IRQ handler can allocate + * concurrently. No faults are possible beyond a programming bug; with no + * IDT yet at the call point a wild access would triple-fault. + */ + +#ifndef NADIR_PMM_H +#define NADIR_PMM_H + +#include + +#include "e820.h" + +/* 4KB frames over the identity-mapped first 1GB: hard capacity limit of + * this allocator (256K frames, 32KB bitmap in .bss). Regions above 1GB + * reported by E820 are ignored. */ +#define PMM_FRAME_SIZE 4096U +#define PMM_MAX_BYTES 0x40000000ULL +#define PMM_MAX_FRAMES (PMM_MAX_BYTES / PMM_FRAME_SIZE) + +/* Memory-map globals: filled by the stage-2 entry (copy from the + * stage-1 staging buffer) before kmain runs. pmm_init reads them. */ +extern uint32_t memmap_count; +extern struct e820_entry memmap_entries[E820_MAX]; + +/* Build the bitmap from the E820 map: type-1 (usable) frames start free, + * everything else starts reserved, then the kernel image, page tables, + * staging buffer, low 1MB, stack and VGA regions are reserved. + * Preconditions: count <= E820_MAX, entries points at count valid + * entries, called once with IF=0 before any alloc/free. + * Failure modes: none — an empty/corrupt map just leaves 0 free frames. */ +void pmm_init(uint32_t count, const struct e820_entry *entries); + +/* Hand out one 4KB frame: returns its physical base address, or 0 when + * out of memory (frame 0 is always reserved, so 0 is never valid). + * Preconditions: pmm_init ran. Failure modes: returns 0 on OOM. */ +uint64_t pmm_alloc_frame(void); + +/* Return a frame from pmm_alloc_frame to the free pool. Silently ignores + * unaligned, out-of-range, or already-free addresses. + * Preconditions: pmm_init ran. Failure modes: none (no fault, no panic). */ +void pmm_free_frame(uint64_t addr); + +/* Live counters for stats printouts. + * Preconditions: pmm_init ran (0 before that). Failure modes: none. */ +uint64_t pmm_total_count(void); +uint64_t pmm_free_count(void); +uint64_t pmm_usable_count(void); + +#endif /* NADIR_PMM_H */ diff --git a/kernel/arch/x86_64/boot.asm b/kernel/arch/x86_64/boot.asm index 3d7797b..17ebf46 100644 --- a/kernel/arch/x86_64/boot.asm +++ b/kernel/arch/x86_64/boot.asm @@ -10,6 +10,11 @@ ; flat GDT, enter 32-bit protected mode and jump to the stage-2 entry. ; ; Memory map used by the whole boot chain: +; 0x005000 E820 staging: count byte at 0x5000, 24-byte entries from +; 0x5020 (64 max = 1536 bytes, ends 0x5620). Free: below the +; 0x7C00 sector, clear of the 0x10000 kernel load, the 0x70000 +; tables and the 0x90000 stack. Stage 2 copies it into kernel +; .bss (memmap_count/memmap_entries) after BSS zeroing. ; 0x007C00 this sector (512 bytes) ; 0x010000 kernel image (loaded here, linked here) ; 0x70000 PML4 | 0x71000 PDPT | 0x72000 PD (built by stage 2) @@ -25,6 +30,10 @@ org 0x7C00 KERNEL_LOAD_SEG equ 0x1000 ; ES for load target 0x1000:0x0000 = 0x10000 STACK_TOP equ 0x7C00 ; real-mode stack grows down from here +E820_COUNT equ 0x5000 ; staged E820 entry count (one byte) +E820_BUF equ 0x5020 ; staged E820 entries (24 bytes each) +E820_MAX equ 64 ; cap: 64 * 24 = 1536 bytes, ends 0x5620 +E820_ENTRY_SIZE equ 24 start: cli @@ -40,8 +49,8 @@ start: call puts call enable_a20 - call read_drive_params call load_kernel + call query_e820 mov si, msg_entering call puts @@ -73,18 +82,15 @@ puts: ret ; --------------------------------------------------------------------------- -; enable_a20: try BIOS, keyboard-controller and fast-A20 methods in turn. -; Each is harmless if A20 is already on. +; enable_a20: try the BIOS and fast-A20 methods in turn. Each is harmless +; if A20 is already on. +; WHY only two methods: the keyboard-controller pulse was dropped to free +; sector bytes for the E820 query above — every 64-bit CPU postdates +; chipsets with the 0x92 fast gate (or a BIOS that honors 0x2401), so the +; legacy AT pulse buys nothing on this kernel's hardware floor. enable_a20: mov ax, 0x2401 ; BIOS: enable A20 int 0x15 - call kbd_wait ; keyboard controller: pulse output port - mov al, 0xD1 - out 0x64, al - call kbd_wait - mov al, 0xDF - out 0x60, al - call kbd_wait in al, 0x92 ; fast A20 (skip if already set or absent) test al, 0x02 jnz .done @@ -93,34 +99,11 @@ enable_a20: .done: ret -kbd_wait: - in al, 0x64 - test al, 0x02 - jnz kbd_wait - ret - ; --------------------------------------------------------------------------- -; read_drive_params: query sectors/track and head count via int 0x13 AH=08. -; Falls back to floppy defaults (18 spt, 2 heads) on failure or nonsense. -read_drive_params: - xor ax, ax - mov ds, ax ; re-establish DS (BIOS may clobber it) - mov ah, 0x08 - mov dl, [boot_drive] - int 0x13 - jc .defaults - and cx, 0x3F ; CL[5:0] = max sector = sectors per track - jz .defaults - mov [spt], cl - inc dh ; DH = max head -> head count - jz .defaults - mov [heads], dh - ret -.defaults: - mov byte [spt], 18 - mov byte [heads], 2 - ret - +; Floppy geometry is fixed at 18 sectors/track, 2 heads (the image only +; ever boots via -fda; see [spt]/[heads] inits below). The old int 0x13 +; AH=08 probe was dropped to free sector bytes for the E820 query above. +; HDD generality wants EDD packet reads (AH=42), planned, not CHS probing. ; --------------------------------------------------------------------------- ; lba_to_chs: convert LBA in AX to int 0x13 CHS in CH/CL/DH. ; Clobbers AX, BX, CX, DX. @@ -193,6 +176,79 @@ load_kernel: jnz .next ret +; --------------------------------------------------------------------------- +; query_e820: ask BIOS for the physical memory map (int 0x15, eax=0xE820, +; edx='SMAP') and stage it at E820_BUF with the entry count byte at +; E820_COUNT. Must run BEFORE the protected-mode switch as BIOS calls are +; unavailable after. Runs in 16-bit real mode at Ring 0; clobbers AX, +; BX, CX, DX, DI, ES. +; +; NOTE: like load_kernel, no register survives int 0x15 — SeaBIOS/real +; BIOSes may clobber general registers across the call, so the +; continuation (EBX) lives in memory ([e820_next]) and ECX/EDX/ES:DI are +; reloaded from immediates before every call. DS is saved on the stack +; across the call (push/pop restores OUR value even if BIOS clobbers it); +; it must NOT be rebuilt with xor ax,ax + mov ds,ax after the call, as +; that zeroes EAX's low half and breaks the 'SMAP' check below. DI is recomputed from +; the in-memory count each pass (index < 64, so the 8-bit multiply +; cannot overflow) instead of keeping a second pointer. +; +; Fallback choice: if no entry was staged (first query failed via carry +; or EAX magic mismatch), synthesize a single usable region at 1MB, +; length 127MB (type 1) — enough for the kernel + PMM to boot on an +; unknown box — and continue silently (no message: the sector has no +; bytes to spare). A mid-list failure keeps whatever entries were +; already staged. Documented here so the PMM knows the map may be +; synthetic on old hardware. Kept golfed on purpose: this sector is +; exactly 510 bytes, every instruction here displaced a boot message. +query_e820: + xor ax, ax + mov ds, ax ; re-establish DS + mov [E820_COUNT], al ; count byte = 0 +.loop: + xor ax, ax + mov es, ax ; ES:DI rebuilt per call + mov al, [E820_COUNT] + mov cl, E820_ENTRY_SIZE + mul cl ; AX = index * 24 + mov di, E820_BUF + add di, ax + mov ecx, E820_ENTRY_SIZE ; reload every call: BIOS owns nothing + mov edx, 0x534D4150 + mov eax, 0xE820 + mov ebx, [e820_next] ; continuation lives in memory + push ds ; DS survives on the stack, EAX intact + int 0x15 + pop ds ; our DS back, whatever BIOS did + jc .done ; carry = end of list or call failed + cmp eax, 0x534D4150 ; BIOS must echo 'SMAP' in EAX + jne .done + cmp byte [E820_COUNT], E820_MAX ; buffer full: stop, keep what we have + jae .done + inc byte [E820_COUNT] ; entry already written by BIOS at ES:DI + mov [e820_next], ebx ; save continuation across the next call + test ebx, ebx ; EBX=0 ends the list + jnz .loop +.done: + cmp byte [E820_COUNT], 0 + jne .ok + xor ax, ax ; fallback: ES:DI may not have survived + mov es, ax + mov di, E820_BUF + mov eax, 0x00100000 + stosd ; base = 1MB + xor eax, eax + stosd ; base high = 0 + mov eax, 0x07F00000 + stosd ; length = 127MB + xor eax, eax + stosd ; length high = 0 + inc eax + stosd ; type = 1 (usable) + inc byte [E820_COUNT] ; count was 0, now 1 +.ok: + ret + halt: cli .hang: @@ -226,9 +282,9 @@ gdt_ptr: dw gdt_end - gdt - 1 dd gdt -msg_loading db "Nadir: loading kernel...", 0x0D, 0x0A, 0 -msg_entering db "Nadir: entering protected mode", 0x0D, 0x0A, 0 -msg_disk_err db "Nadir: disk read error", 0x0D, 0x0A, 0 +msg_loading db "Nadir: load", 0x0D, 0x0A, 0 +msg_entering db "Nadir: pmode", 0x0D, 0x0A, 0 +msg_disk_err db "Nadir: disk err", 0x0D, 0x0A, 0 boot_drive db 0 spt dw 18 @@ -238,6 +294,8 @@ buf_seg dw 0 ; load buffer segment (starts 0x1000) buf_off dw 0 ; load buffer offset within segment sectors_left dw 0 ; sectors still to load tries db 0 ; retries left for current sector +e820_next dd 0 ; E820 continuation across int 0x15 + ; (image-zeroed; first call starts at 0) times 510-($-$$) db 0 dw 0xAA55 diff --git a/kernel/arch/x86_64/entry.asm b/kernel/arch/x86_64/entry.asm index 34e73db..47a87b0 100644 --- a/kernel/arch/x86_64/entry.asm +++ b/kernel/arch/x86_64/entry.asm @@ -9,8 +9,14 @@ ; ; Steps: verify long mode exists, identity-map the first 1GB with 2MB ; pages, enable PAE + LME + paging, reload a GDT with a 64-bit code -; segment, zero BSS, then call the C entry point kmain (System V ABI). +; segment, zero BSS, copy the BIOS memory map staged by stage 1 into +; kernel .bss, then call the C entry point kmain (System V ABI). ; Interrupts stay disabled for the whole sequence. +; +; WHY copy after zeroing: the map globals live in .bss, so zeroing first +; then copying keeps the staged entries. Zeroing after would wipe them; +; copying before is impossible (BSS addresses are only valid once paging +; and the zeroed state the C code expects are both in place). bits 32 section .boot32 @@ -18,11 +24,17 @@ global boot32_entry extern kmain extern __bss_start extern __bss_end +extern memmap_count +extern memmap_entries PML4 equ 0x70000 PDPT equ 0x71000 PD equ 0x72000 STACK64_TOP equ 0x90000 +MEMMAP_COUNT_PHYS equ 0x5000 ; dword staged by stage 1 +MEMMAP_BUF_PHYS equ 0x5020 ; 24-byte E820 entries staged by stage 1 +E820_MAX equ 64 +E820_ENTRY_SIZE equ 24 boot32_entry: ; --- CPUID: does this CPU do 64-bit long mode? --- @@ -121,6 +133,25 @@ long_entry: xor eax, eax rep stosb + ; --- copy staged E820 map into .bss globals (identity map covers + ; both 0x5000 and .bss, and paging is already on) --- + ; WHY byte + clamp: stage 1 stages a single count byte (64 max fits); + ; the clamp is belt-and-braces against a corrupt staging byte. + movzx eax, byte [abs MEMMAP_COUNT_PHYS] + cmp eax, E820_MAX + jbe .map_count_ok + mov eax, E820_MAX +.map_count_ok: + mov [rel memmap_count], eax + test eax, eax + jz .map_done + mov rsi, MEMMAP_BUF_PHYS + lea rdi, [rel memmap_entries] + mov ecx, eax + imul ecx, ecx, E820_ENTRY_SIZE ; bytes = count * 24 + rep movsb +.map_done: + call kmain ; first C code — does not return cli .hang: diff --git a/kernel/arch/x86_64/link.ld b/kernel/arch/x86_64/link.ld index f282575..e665c60 100644 --- a/kernel/arch/x86_64/link.ld +++ b/kernel/arch/x86_64/link.ld @@ -17,6 +17,8 @@ SECTIONS { . = 0x10000; + __kernel_start = .; + .boot : { *(.boot32) } @@ -40,6 +42,8 @@ SECTIONS __bss_end = .; } + __kernel_end = .; + /DISCARD/ : { *(.note*) *(.comment*) diff --git a/kernel/core/klog.c b/kernel/core/klog.c new file mode 100644 index 0000000..29953cb --- /dev/null +++ b/kernel/core/klog.c @@ -0,0 +1,117 @@ +/* Nadir boot logger (see klog.h). + * + * The only backends are console_putchar (VGA polling) + * and pit_ticks (monotonic u64) — no hardware addresses, no port I/O, no + * serial dependency live here. Serial mirroring stays in kmain, which + * prints the same text without the timestamp prefix (kept simple). + * + * Timestamp math: secs = ticks / PIT_HZ, frac = (ticks % PIT_HZ) * 10000 + * (1000000us/s divided by 100Hz). The field is 12 wide, space-padded on + * the left (`[ 0.000000]`); past 99999s it overflows the + * pad and just prints long. + */ + +#include + +#include "console.h" +#include "klog.h" +#include "pit.h" + +/* Width of the `secs.frac` field between `[` and `]`. */ +#define KLOG_TIME_WIDTH 12U + +/* Print the `[ 0.000000] subsystem: ` prefix. Reads pit_ticks() live + * so piecewise lines and one-shot lines share one code path. */ +static void klog_prefix(const char *subsystem) +{ + uint64_t ticks = pit_ticks(); + uint64_t secs = ticks / (uint64_t)PIT_HZ; + uint64_t frac = (ticks % (uint64_t)PIT_HZ) * 10000ULL; + char num[22]; + int n = 0; + uint64_t v; + uint64_t div; + int digits; + uint64_t len; + + /* Decimal seconds into num (reversed then flipped below). */ + v = secs; + if (v == 0U) { + num[n++] = '0'; + } else { + while (v != 0U && n < 20) { + num[n++] = (char)('0' + (v % 10U)); + v /= 10U; + } + } + /* Field length is digits + '.' + 6 frac digits; pad left with spaces + * to KLOG_TIME_WIDTH so short uptimes align */ + digits = n; + len = (uint64_t)digits + 7U; + console_putchar('['); + while (len < (uint64_t)KLOG_TIME_WIDTH) { + console_putchar(' '); + len++; + } + while (n > 0) { + n--; + console_putchar(num[n]); + } + console_putchar('.'); + /* Exactly 6 fraction digits, zero-padded (10ms ticks leave the last + * 4 as zeros — resolution limit, not a bug). */ + div = 100000ULL; + while (div > 0U) { + console_putchar((char)('0' + ((frac / div) % 10U))); + div /= 10U; + } + console_puts("] "); + console_puts(subsystem); + console_puts(": "); +} + +void klog(const char *subsystem, const char *msg) +{ + klog_begin(subsystem); + klog_str(msg); + klog_end(); +} + +void klog_begin(const char *subsystem) +{ + klog_prefix(subsystem); +} + +void klog_str(const char *s) +{ + console_puts(s); +} + +void klog_dec(uint64_t v) +{ + char buf[20]; + int n = 0; + + if (v == 0U) { + console_putchar('0'); + return; + } + while (v != 0U && n < (int)sizeof(buf)) { + buf[n++] = (char)('0' + (v % 10U)); + v /= 10U; + } + while (n > 0) { + n--; + console_putchar(buf[n]); + } +} + +void klog_hex64(uint64_t v) +{ + console_puthex64(v); +} + +void klog_end(void) +{ + console_putchar('\n'); +} diff --git a/kernel/core/kmain.c b/kernel/core/kmain.c index 9d984e5..19f1e9b 100644 --- a/kernel/core/kmain.c +++ b/kernel/core/kmain.c @@ -9,6 +9,10 @@ * 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. + * + * Every line goes through klog`[ 0.000000] subsystem: message`, + * time from pit_ticks()/PIT_HZ). + * Serial COM1 mirrors the same text without the timestamp prefix. */ #include @@ -16,34 +20,16 @@ #include "console.h" #include "idt.h" #include "keyboard.h" +#include "klog.h" #include "pic.h" #include "pit.h" +#include "pmm.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) { @@ -70,8 +56,9 @@ void kmain(void) 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"); + /* Boot banner: keeps the "64-bit" needle the CI boot checker greps + * for (see .github/scripts/check_boot.py). */ + klog("nadir", "Hello World from Nadir kernel (64-bit long mode, Ring 0)"); /* WHY this order: pic_remap must precede idt_install_irqs so IRQ * gates land on valid remapped vectors 32-47 (pre-remap they would @@ -80,48 +67,68 @@ void kmain(void) * 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"); + klog("idt", "exceptions 0-31 installed"); pic_remap(); - console_puts("PIC remapped (IRQ 0-15 -> vectors 32-47, masked).\n"); + klog("pic", "remapped IRQ 0-15 to vectors 32-47"); idt_install_irqs(); - console_puts("IRQ gates installed (vectors 32-47).\n"); + klog("idt", "IRQ gates 32-47 installed"); serial_init(); serial_present = serial_ok(); - console_puts("Serial COM1 38400 8N1 ready.\n"); + klog("serial", "COM1 38400 8N1 ready"); if (serial_present != 0) { - serial_puts("Hello World from Nadir Kernel !\n"); - serial_puts("Serial COM1 38400 8N1 ready.\n"); + serial_puts("Hello World from Nadir kernel\n"); } pit_init(); - console_puts("PIT 100Hz on IRQ0 ready.\n"); + klog("pit", "100Hz on IRQ0"); if (serial_present != 0) { - serial_puts("PIT 100Hz on IRQ0 ready.\n"); + serial_puts("pit: 100Hz on IRQ0\n"); } keyboard_init(); - console_puts("Keyboard PS/2 IRQ1 ready.\n"); + klog("keyboard", "PS/2 ready on IRQ1"); if (serial_present != 0) { - serial_puts("Keyboard PS/2 IRQ1 ready.\n"); + serial_puts("keyboard: PS/2 ready on IRQ1\n"); } + /* pmm_init only reads the staged E820 globals and writes + * its own .bss bitmap, so it runs + * after all devices are programmed but before sti, with IF=0, so no + * handler can observe a half-built allocator. Stats go through the + * polling VGA console, which is IRQ-safe by construction. */ + pmm_init(memmap_count, memmap_entries); + klog_begin("pmm"); + klog_str("E820 entries "); + klog_dec(memmap_count); + klog_str(", usable frames "); + klog_dec(pmm_usable_count()); + klog_str(" / total frames "); + klog_dec(pmm_total_count()); + klog_str(", free frames "); + klog_dec(pmm_free_count()); + klog_str(", reserved "); + klog_dec(pmm_usable_count() - pmm_free_count()); + klog_end(); + /* 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"); + klog("nadir", "interrupts enabled (IRQs 0-1)"); if (serial_present != 0) { - serial_puts("Interrupts enabled (IRQs 0-1).\n"); + serial_puts("nadir: interrupts enabled (IRQs 0-1)\n"); } - console_puts("64-bit proof value: "); - console_puthex64(PROOF_VALUE); - console_puts("\nHalting in hlt loop (timer + keyboard live).\n"); + klog_begin("nadir"); + klog_str("64-bit proof value: "); + klog_hex64(PROOF_VALUE); + klog_end(); + klog("nadir", "halting in hlt loop (timer + keyboard live)"); /* Main loop: hlt sleeps until the next IRQ (IF=1 throughout), then * one poll pass prints uptime ~1/s and drains echoed keys. */ @@ -136,13 +143,15 @@ void kmain(void) 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"); + klog_begin("nadir"); + klog_str("uptime "); + klog_dec(secs); + klog_str("."); + klog_dec(tenths); + klog_str("s (ticks "); + klog_hex64(now); + klog_str(")"); + klog_end(); if (serial_present != 0) { serial_puts("uptime (see VGA for value)\n"); } diff --git a/kernel/mm/pmm.c b/kernel/mm/pmm.c new file mode 100644 index 0000000..565e3de --- /dev/null +++ b/kernel/mm/pmm.c @@ -0,0 +1,197 @@ +/* Nadir physical memory manager (PMM-only) + * + * very address it touches arrives as a plain + * physical range (E820 entries from the caller, reservation bounds from + * the linker/boot chain). Runs in 64-bit long mode at Ring 0 (CPL 0) + * under the stage-2 identity map, so physical addresses are dereferenced + * as-is. pmm_init runs once with IF=0 before sti; alloc/free never block + * and never touch hardware, so they are safe under any IF state. + * Fault behavior: no page fault is possible inside the first 1GB + * identity window; a caller passing a wild E820 pointer faults before we + * run, not in here. Bitmap: bit set = used/reserved, bit clear = free. + */ + +#include + +#include "e820.h" +#include "pmm.h" + +/* Filled by the stage-2 entry (copy from the stage-1 staging buffer at + * 0x5000/0x5020) after BSS zeroing, before kmain runs. */ +uint32_t memmap_count; +struct e820_entry memmap_entries[E820_MAX]; + +/* Kernel image bounds from the linker script (linked at 0x10000). */ +extern char __kernel_start[]; +extern char __kernel_end[]; + +/* One bit per 4KB frame over the first 1GB: 256K frames = 32KB in .bss. */ +static uint8_t bitmap[PMM_MAX_FRAMES / 8U]; + +static uint64_t g_usable; +static uint64_t g_free; + +/* WHY bit ops instead of a byte-per-frame array: 32KB of .bss keeps the + * kernel image small (sectors are loaded from floppy LBA 1..N, so every + * KB costs boot time); the shift/mask cost is negligible next to a VGA + * print. */ +static void set_bit(uint64_t frame) +{ + bitmap[frame / 8U] |= (uint8_t)(1U << (frame % 8U)); +} + +static void clear_bit(uint64_t frame) +{ + bitmap[frame / 8U] &= (uint8_t)~(1U << (frame % 8U)); +} + +static int test_bit(uint64_t frame) +{ + return (bitmap[frame / 8U] & (uint8_t)(1U << (frame % 8U))) != 0; +} + +/* Mark every frame in [base, base+len) reserved. Out-of-window and + * already-reserved frames are skipped, so overlapping/low regions are + * safe to reserve twice. */ +static void reserve_range(uint64_t base, uint64_t len) +{ + uint64_t end; + uint64_t frame; + + if (len == 0U) { + return; + } + end = base + len; + if (end < base) { + end = UINT64_MAX; + } + if (base >= PMM_MAX_BYTES) { + return; + } + if (end > PMM_MAX_BYTES) { + end = PMM_MAX_BYTES; + } + for (frame = base / PMM_FRAME_SIZE; frame < end / PMM_FRAME_SIZE; frame++) { + if (test_bit(frame) == 0) { + set_bit(frame); + g_free--; + } + } +} + +void pmm_init(uint32_t count, const struct e820_entry *entries) +{ + uint64_t i; + uint64_t frame; + uint64_t start; + uint64_t end; + + /* Start fully reserved; usable E820 frames are cleared below. This + * way unknown memory can never be handed out by accident. */ + for (i = 0; i < sizeof(bitmap); i++) { + bitmap[i] = 0xFFU; + } + g_usable = 0; + g_free = 0; + + if (entries == 0) { + count = 0; + } + if (count > E820_MAX) { + count = E820_MAX; + } + for (i = 0; i < count; i++) { + if (entries[i].type != E820_USABLE) { + continue; + } + if (entries[i].length == 0U) { + continue; + } + start = entries[i].base; + end = start + entries[i].length; + if (end < start) { + end = UINT64_MAX; + } + if (start >= PMM_MAX_BYTES) { + continue; /* above the identity-mapped window: ignored */ + } + if (end > PMM_MAX_BYTES) { + end = PMM_MAX_BYTES; + } + /* Align the range to whole frames */ + start = (start + PMM_FRAME_SIZE - 1U) / PMM_FRAME_SIZE; + end = end / PMM_FRAME_SIZE; + for (frame = start; frame < end; frame++) { + if (test_bit(frame) != 0) { + clear_bit(frame); + g_usable++; + g_free++; + } + } + } + + /* Second pass: carve out every non-usable E820 range. WHY a separate + * pass instead of skipping overlaps up front: BIOS maps routinely + * stack a reserved entry (e.g. ACPI data) inside a larger usable one, + * and reserve_range is idempotent, so usable-then-reserved converges + * on the right bitmap whatever order the firmware lists. */ + for (i = 0; i < count; i++) { + if (entries[i].type != E820_USABLE) { + reserve_range(entries[i].base, entries[i].length); + } + } + + reserve_range(0x00000000ULL, 0x00100000ULL); + reserve_range((uint64_t)(uintptr_t)__kernel_start, + (uint64_t)(uintptr_t)__kernel_end - (uint64_t)(uintptr_t)__kernel_start); + reserve_range(0x00070000ULL, 0x00004000ULL); + reserve_range(0x00005000ULL, 0x00000620ULL); + reserve_range(0x0008C000ULL, 0x00004000ULL); + reserve_range(0x000B8000ULL, 0x00001000ULL); +} + +uint64_t pmm_alloc_frame(void) +{ + uint64_t frame; + + for (frame = 0; frame < PMM_MAX_FRAMES; frame++) { + if (test_bit(frame) == 0) { + set_bit(frame); + g_free--; + return frame * PMM_FRAME_SIZE; + } + } + return 0; /* frame 0 is always reserved: 0 unambiguously means OOM */ +} + +void pmm_free_frame(uint64_t addr) +{ + uint64_t frame; + + if ((addr % PMM_FRAME_SIZE) != 0U) { + return; + } + if (addr >= PMM_MAX_BYTES) { + return; + } + frame = addr / PMM_FRAME_SIZE; + if (test_bit(frame) != 0) { + clear_bit(frame); + g_free++; + } +} + +uint64_t pmm_total_count(void) +{ + return PMM_MAX_FRAMES; +} + +uint64_t pmm_free_count(void) +{ + return g_free; +} + +uint64_t pmm_usable_count(void) +{ + return g_usable; +}