Merge pull request #7 from Plexi09/feature/pmm-e820

E820 memory map + bitmap PMM + timestamped klog
This commit is contained in:
Pierre-Emmanuel Gob. 2026-10-05 18:19:51 +02:00 • committed by GitHub
commit 5077695c70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 644 additions and 87 deletions

View file

@ -20,7 +20,7 @@ CFLAGS := -std=c17 -ffreestanding -nostdlib -fno-builtin \
-fno-pic -fno-pie -fno-stack-protector \ -fno-pic -fno-pie -fno-stack-protector \
-Wall -Wextra -Werror -O2 -Iinclude -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) all: $(IMG)
@ -33,7 +33,13 @@ $(BUILD)/entry.o: $(ARCH_DIR)/entry.asm | $(BUILD)
$(BUILD)/isr.o: $(ARCH_DIR)/isr.asm | $(BUILD) $(BUILD)/isr.o: $(ARCH_DIR)/isr.asm | $(BUILD)
$(NASM) -f elf64 $< -o $@ $(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 $@ $(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/console.o: $(ARCH_DIR)/console.c include/console.h include/io.h | $(BUILD) $(BUILD)/console.o: $(ARCH_DIR)/console.c include/console.h include/io.h | $(BUILD)

34
include/e820.h Normal file
View file

@ -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 <stdint.h>
/* 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 */

48
include/klog.h Normal file
View file

@ -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 <stdint.h>
/* 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 */

53
include/pmm.h Normal file
View file

@ -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 <stdint.h>
#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 */

View file

@ -10,6 +10,11 @@
; flat GDT, enter 32-bit protected mode and jump to the stage-2 entry. ; flat GDT, enter 32-bit protected mode and jump to the stage-2 entry.
; ;
; Memory map used by the whole boot chain: ; 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) ; 0x007C00 this sector (512 bytes)
; 0x010000 kernel image (loaded here, linked here) ; 0x010000 kernel image (loaded here, linked here)
; 0x70000 PML4 | 0x71000 PDPT | 0x72000 PD (built by stage 2) ; 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 KERNEL_LOAD_SEG equ 0x1000 ; ES for load target 0x1000:0x0000 = 0x10000
STACK_TOP equ 0x7C00 ; real-mode stack grows down from here 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: start:
cli cli
@ -40,8 +49,8 @@ start:
call puts call puts
call enable_a20 call enable_a20
call read_drive_params
call load_kernel call load_kernel
call query_e820
mov si, msg_entering mov si, msg_entering
call puts call puts
@ -73,18 +82,15 @@ puts:
ret ret
; --------------------------------------------------------------------------- ; ---------------------------------------------------------------------------
; enable_a20: try BIOS, keyboard-controller and fast-A20 methods in turn. ; enable_a20: try the BIOS and fast-A20 methods in turn. Each is harmless
; Each is harmless if A20 is already on. ; 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: enable_a20:
mov ax, 0x2401 ; BIOS: enable A20 mov ax, 0x2401 ; BIOS: enable A20
int 0x15 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) in al, 0x92 ; fast A20 (skip if already set or absent)
test al, 0x02 test al, 0x02
jnz .done jnz .done
@ -93,34 +99,11 @@ enable_a20:
.done: .done:
ret 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. ; Floppy geometry is fixed at 18 sectors/track, 2 heads (the image only
; Falls back to floppy defaults (18 spt, 2 heads) on failure or nonsense. ; ever boots via -fda; see [spt]/[heads] inits below). The old int 0x13
read_drive_params: ; AH=08 probe was dropped to free sector bytes for the E820 query above.
xor ax, ax ; HDD generality wants EDD packet reads (AH=42), planned, not CHS probing.
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
; --------------------------------------------------------------------------- ; ---------------------------------------------------------------------------
; lba_to_chs: convert LBA in AX to int 0x13 CHS in CH/CL/DH. ; lba_to_chs: convert LBA in AX to int 0x13 CHS in CH/CL/DH.
; Clobbers AX, BX, CX, DX. ; Clobbers AX, BX, CX, DX.
@ -193,6 +176,79 @@ load_kernel:
jnz .next jnz .next
ret 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: halt:
cli cli
.hang: .hang:
@ -226,9 +282,9 @@ gdt_ptr:
dw gdt_end - gdt - 1 dw gdt_end - gdt - 1
dd gdt dd gdt
msg_loading db "Nadir: loading kernel...", 0x0D, 0x0A, 0 msg_loading db "Nadir: load", 0x0D, 0x0A, 0
msg_entering db "Nadir: entering protected mode", 0x0D, 0x0A, 0 msg_entering db "Nadir: pmode", 0x0D, 0x0A, 0
msg_disk_err db "Nadir: disk read error", 0x0D, 0x0A, 0 msg_disk_err db "Nadir: disk err", 0x0D, 0x0A, 0
boot_drive db 0 boot_drive db 0
spt dw 18 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 buf_off dw 0 ; load buffer offset within segment
sectors_left dw 0 ; sectors still to load sectors_left dw 0 ; sectors still to load
tries db 0 ; retries left for current sector 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 times 510-($-$$) db 0
dw 0xAA55 dw 0xAA55

View file

@ -9,8 +9,14 @@
; ;
; Steps: verify long mode exists, identity-map the first 1GB with 2MB ; 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 ; 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. ; 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 bits 32
section .boot32 section .boot32
@ -18,11 +24,17 @@ global boot32_entry
extern kmain extern kmain
extern __bss_start extern __bss_start
extern __bss_end extern __bss_end
extern memmap_count
extern memmap_entries
PML4 equ 0x70000 PML4 equ 0x70000
PDPT equ 0x71000 PDPT equ 0x71000
PD equ 0x72000 PD equ 0x72000
STACK64_TOP equ 0x90000 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: boot32_entry:
; --- CPUID: does this CPU do 64-bit long mode? --- ; --- CPUID: does this CPU do 64-bit long mode? ---
@ -121,6 +133,25 @@ long_entry:
xor eax, eax xor eax, eax
rep stosb 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 call kmain ; first C code — does not return
cli cli
.hang: .hang:

View file

@ -17,6 +17,8 @@ SECTIONS
{ {
. = 0x10000; . = 0x10000;
__kernel_start = .;
.boot : { .boot : {
*(.boot32) *(.boot32)
} }
@ -40,6 +42,8 @@ SECTIONS
__bss_end = .; __bss_end = .;
} }
__kernel_end = .;
/DISCARD/ : { /DISCARD/ : {
*(.note*) *(.note*)
*(.comment*) *(.comment*)

117
kernel/core/klog.c Normal file
View file

@ -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 <stdint.h>
#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');
}

View file

@ -9,6 +9,10 @@
* after every device is programmed; the main loop keeps IF=1 and sleeps * 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 * 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. * 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 <stdint.h> #include <stdint.h>
@ -16,34 +20,16 @@
#include "console.h" #include "console.h"
#include "idt.h" #include "idt.h"
#include "keyboard.h" #include "keyboard.h"
#include "klog.h"
#include "pic.h" #include "pic.h"
#include "pit.h" #include "pit.h"
#include "pmm.h"
#include "serial.h" #include "serial.h"
/* A value that only comes out right if the CPU truly executes 64-bit /* A value that only comes out right if the CPU truly executes 64-bit
* arithmetic: every nibble survives the shifts above 32 bits. */ * arithmetic: every nibble survives the shifts above 32 bits. */
#define PROOF_VALUE 0x0123456789ABCDEFULL #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. */ /* Mirror one echoed keystroke to COM1 when the UART looks present. */
static void serial_echo(char c, int ok) static void serial_echo(char c, int ok)
{ {
@ -70,8 +56,9 @@ void kmain(void)
uint64_t last_uptime; uint64_t last_uptime;
console_clear(); console_clear();
console_puts("Hello World from Nadir Kernel !\n"); /* Boot banner: keeps the "64-bit" needle the CI boot checker greps
console_puts("Running C code in 64-bit long mode (Ring 0).\n"); * 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 /* WHY this order: pic_remap must precede idt_install_irqs so IRQ
* gates land on valid remapped vectors 32-47 (pre-remap they would * 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 * fires into a half-wired handler; pic_remap masks everything and
* each device opts in via pic_unmask, so stray lines stay silent. */ * each device opts in via pic_unmask, so stray lines stay silent. */
idt_init(); idt_init();
console_puts("IDT installed (vectors 0-31).\n"); klog("idt", "exceptions 0-31 installed");
pic_remap(); 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(); idt_install_irqs();
console_puts("IRQ gates installed (vectors 32-47).\n"); klog("idt", "IRQ gates 32-47 installed");
serial_init(); serial_init();
serial_present = serial_ok(); serial_present = serial_ok();
console_puts("Serial COM1 38400 8N1 ready.\n"); klog("serial", "COM1 38400 8N1 ready");
if (serial_present != 0) { if (serial_present != 0) {
serial_puts("Hello World from Nadir Kernel !\n"); serial_puts("Hello World from Nadir kernel\n");
serial_puts("Serial COM1 38400 8N1 ready.\n");
} }
pit_init(); pit_init();
console_puts("PIT 100Hz on IRQ0 ready.\n"); klog("pit", "100Hz on IRQ0");
if (serial_present != 0) { if (serial_present != 0) {
serial_puts("PIT 100Hz on IRQ0 ready.\n"); serial_puts("pit: 100Hz on IRQ0\n");
} }
keyboard_init(); keyboard_init();
console_puts("Keyboard PS/2 IRQ1 ready.\n"); klog("keyboard", "PS/2 ready on IRQ1");
if (serial_present != 0) { 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 /* Explicit opt-in for the two live lines; idempotent even though
* keyboard_init already unmasked IRQ1. */ * keyboard_init already unmasked IRQ1. */
pic_unmask(0); pic_unmask(0);
pic_unmask(1); pic_unmask(1);
__asm__ volatile("sti"); __asm__ volatile("sti");
console_puts("Interrupts enabled (IRQs 0-1).\n"); klog("nadir", "interrupts enabled (IRQs 0-1)");
if (serial_present != 0) { 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: "); klog_begin("nadir");
console_puthex64(PROOF_VALUE); klog_str("64-bit proof value: ");
console_puts("\nHalting in hlt loop (timer + keyboard live).\n"); 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 /* Main loop: hlt sleeps until the next IRQ (IF=1 throughout), then
* one poll pass prints uptime ~1/s and drains echoed keys. */ * one poll pass prints uptime ~1/s and drains echoed keys. */
@ -136,13 +143,15 @@ void kmain(void)
uint64_t tenths = (now / 10U) % 10U; uint64_t tenths = (now / 10U) % 10U;
last_uptime = now; last_uptime = now;
console_puts("uptime "); klog_begin("nadir");
print_dec(secs); klog_str("uptime ");
console_puts("."); klog_dec(secs);
print_dec(tenths); klog_str(".");
console_puts("s (ticks "); klog_dec(tenths);
console_puthex64(now); klog_str("s (ticks ");
console_puts(")\n"); klog_hex64(now);
klog_str(")");
klog_end();
if (serial_present != 0) { if (serial_present != 0) {
serial_puts("uptime (see VGA for value)\n"); serial_puts("uptime (see VGA for value)\n");
} }

197
kernel/mm/pmm.c Normal file
View file

@ -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 <stdint.h>
#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;
}