feat: expand user mode, add DOOM game, add manpages

This commit is contained in:
2026-02-18 15:13:53 +01:00
parent 605fbcbe42
commit 24af60d669
51 changed files with 4484 additions and 43 deletions
+3
View File
@@ -7,3 +7,6 @@ qemu.log
toolchain/local/ toolchain/local/
toolchain/build/ toolchain/build/
toolchain/src/ toolchain/src/
programs/bin/
programs/obj/
programs/src/doom/obj/
+1062
View File
File diff suppressed because it is too large Load Diff
Submodule
+1
Submodule doomgeneric added at fc60163949
+79 -1
View File
@@ -20,6 +20,8 @@
#include <Net/ByteOrder.hpp> #include <Net/ByteOrder.hpp>
#include <Hal/MSR.hpp> #include <Hal/MSR.hpp>
#include <Hal/GDT.hpp> #include <Hal/GDT.hpp>
#include <Graphics/Cursor.hpp>
#include "../Libraries/flanterm/src/flanterm.h"
// Assembly entry point // Assembly entry point
extern "C" void SyscallEntry(); extern "C" void SyscallEntry();
@@ -175,6 +177,47 @@ namespace Zenith {
static uint16_t g_pingSeq = 0; static uint16_t g_pingSeq = 0;
static constexpr uint16_t PING_ID = 0x2E01; // "ZE" static constexpr uint16_t PING_ID = 0x2E01; // "ZE"
static void Sys_FbInfo(FbInfo* out) {
if (out == nullptr) return;
out->width = Graphics::Cursor::GetFramebufferWidth();
out->height = Graphics::Cursor::GetFramebufferHeight();
out->pitch = Graphics::Cursor::GetFramebufferPitch();
out->bpp = 32;
out->userAddr = 0;
}
static void Sys_WaitPid(int pid) {
while (Sched::IsAlive(pid)) {
Sched::Schedule(); // yield until the process exits
}
}
static uint64_t Sys_FbMap() {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return 0;
uint32_t* fbBase = Graphics::Cursor::GetFramebufferBase();
if (fbBase == nullptr) return 0;
uint64_t fbPhys = Memory::SubHHDM((uint64_t)fbBase);
uint64_t fbSize = Graphics::Cursor::GetFramebufferHeight()
* Graphics::Cursor::GetFramebufferPitch();
uint64_t numPages = (fbSize + 0xFFF) / 0x1000;
// Map at a fixed user VA
constexpr uint64_t userVa = 0x50000000ULL;
for (uint64_t i = 0; i < numPages; i++) {
Memory::VMM::Paging::MapUserIn(
proc->pml4Phys,
fbPhys + i * 0x1000,
userVa + i * 0x1000
);
}
return userVa;
}
static int32_t Sys_Ping(uint32_t ipAddr, uint32_t timeoutMs) { static int32_t Sys_Ping(uint32_t ipAddr, uint32_t timeoutMs) {
uint16_t seq = g_pingSeq++; uint16_t seq = g_pingSeq++;
@@ -192,6 +235,27 @@ namespace Zenith {
return (int32_t)(Timekeeping::GetMilliseconds() - start); return (int32_t)(Timekeeping::GetMilliseconds() - start);
} }
static int Sys_Spawn(const char* path, const char* args) {
return Sched::Spawn(path, args);
}
static int Sys_GetArgs(char* buf, uint64_t maxLen) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || buf == nullptr || maxLen == 0) return -1;
int i = 0;
for (; i < (int)maxLen - 1 && proc->args[i]; i++) {
buf[i] = proc->args[i];
}
buf[i] = '\0';
return i;
}
static uint64_t Sys_TermSize() {
size_t cols = 0, rows = 0;
flanterm_get_dimensions(Kt::ctx, &cols, &rows);
return (rows << 32) | (cols & 0xFFFFFFFF);
}
// ---- Dispatch ---- // ---- Dispatch ----
extern "C" int64_t SyscallDispatch(SyscallFrame* frame) { extern "C" int64_t SyscallDispatch(SyscallFrame* frame) {
@@ -248,6 +312,20 @@ namespace Zenith {
return (int64_t)Sys_GetChar(); return (int64_t)Sys_GetChar();
case SYS_PING: case SYS_PING:
return (int64_t)Sys_Ping((uint32_t)frame->arg1, (uint32_t)frame->arg2); return (int64_t)Sys_Ping((uint32_t)frame->arg1, (uint32_t)frame->arg2);
case SYS_SPAWN:
return (int64_t)Sys_Spawn((const char*)frame->arg1, (const char*)frame->arg2);
case SYS_WAITPID:
Sys_WaitPid((int)frame->arg1);
return 0;
case SYS_FBINFO:
Sys_FbInfo((FbInfo*)frame->arg1);
return 0;
case SYS_FBMAP:
return (int64_t)Sys_FbMap();
case SYS_TERMSIZE:
return (int64_t)Sys_TermSize();
case SYS_GETARGS:
return (int64_t)Sys_GetArgs((char*)frame->arg1, frame->arg2);
default: default:
return -1; return -1;
} }
@@ -274,7 +352,7 @@ namespace Zenith {
Hal::WriteMSR(Hal::IA32_FMASK, 0x200); Hal::WriteMSR(Hal::IA32_FMASK, 0x200);
Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR=" Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
<< kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 20 syscalls)"; << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 26 syscalls)";
} }
} }
+14
View File
@@ -31,6 +31,20 @@ namespace Zenith {
static constexpr uint64_t SYS_GETKEY = 17; static constexpr uint64_t SYS_GETKEY = 17;
static constexpr uint64_t SYS_GETCHAR = 18; static constexpr uint64_t SYS_GETCHAR = 18;
static constexpr uint64_t SYS_PING = 19; static constexpr uint64_t SYS_PING = 19;
static constexpr uint64_t SYS_SPAWN = 20;
static constexpr uint64_t SYS_FBINFO = 21;
static constexpr uint64_t SYS_FBMAP = 22;
static constexpr uint64_t SYS_WAITPID = 23;
static constexpr uint64_t SYS_TERMSIZE = 24;
static constexpr uint64_t SYS_GETARGS = 25;
struct FbInfo {
uint64_t width;
uint64_t height;
uint64_t pitch; // bytes per scanline
uint64_t bpp; // bits per pixel (32)
uint64_t userAddr; // filled by SYS_FBMAP (0 until mapped)
};
struct SysInfo { struct SysInfo {
char osName[32]; char osName[32];
+35 -8
View File
@@ -176,39 +176,66 @@ namespace Drivers::PS2::Keyboard {
return; return;
} }
// Handle modifier keys // Handle modifier keys (update state, but still push event to buffer)
bool isModifier = false;
switch (keycode) { switch (keycode) {
case ScLeftShift: case ScLeftShift:
g_Modifiers.LeftShift = !released; g_Modifiers.LeftShift = !released;
return; isModifier = true;
break;
case ScRightShift: case ScRightShift:
g_Modifiers.RightShift = !released; g_Modifiers.RightShift = !released;
return; isModifier = true;
break;
case ScLeftCtrl: case ScLeftCtrl:
g_Modifiers.LeftCtrl = !released; g_Modifiers.LeftCtrl = !released;
return; isModifier = true;
break;
case ScLeftAlt: case ScLeftAlt:
g_Modifiers.LeftAlt = !released; g_Modifiers.LeftAlt = !released;
return; isModifier = true;
break;
case ScCapsLock: case ScCapsLock:
if (!released) { if (!released) {
g_Modifiers.CapsLock = !g_Modifiers.CapsLock; g_Modifiers.CapsLock = !g_Modifiers.CapsLock;
} }
return; isModifier = true;
break;
case ScNumLock: case ScNumLock:
if (!released) { if (!released) {
g_Modifiers.NumLock = !g_Modifiers.NumLock; g_Modifiers.NumLock = !g_Modifiers.NumLock;
} }
return; isModifier = true;
break;
case ScScrollLock: case ScScrollLock:
if (!released) { if (!released) {
g_Modifiers.ScrollLock = !g_Modifiers.ScrollLock; g_Modifiers.ScrollLock = !g_Modifiers.ScrollLock;
} }
return; isModifier = true;
break;
default: default:
break; break;
} }
// Modifiers still need events in the buffer (for apps like doom)
// but lock keys (caps/num/scroll) don't need buffer events
if (isModifier && keycode != ScCapsLock && keycode != ScNumLock && keycode != ScScrollLock) {
KeyEvent event = {
.Scancode = scancode,
.Ascii = 0,
.Pressed = !released,
.Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift,
.Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl,
.Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt,
.CapsLock = g_Modifiers.CapsLock
};
g_BufferLock.Acquire();
BufferPush(event);
g_BufferLock.Release();
return;
}
if (isModifier) return;
// Translate scancode to ASCII // Translate scancode to ASCII
char ascii = 0; char ascii = 0;
if (keycode < 128) { if (keycode < 128) {
+5
View File
@@ -162,4 +162,9 @@ namespace Graphics::Cursor {
g_OldY = newY; g_OldY = newY;
} }
uint32_t* GetFramebufferBase() { return g_FbBase; }
uint64_t GetFramebufferWidth() { return g_FbWidth; }
uint64_t GetFramebufferHeight() { return g_FbHeight; }
uint64_t GetFramebufferPitch() { return g_FbPitch; }
}; };
+5
View File
@@ -13,4 +13,9 @@ namespace Graphics::Cursor {
void Initialize(limine_framebuffer* framebuffer); void Initialize(limine_framebuffer* framebuffer);
void Update(); void Update();
uint32_t* GetFramebufferBase();
uint64_t GetFramebufferWidth();
uint64_t GetFramebufferHeight();
uint64_t GetFramebufferPitch();
}; };
+17
View File
@@ -93,6 +93,23 @@ extern "C" void kmain() {
#if defined (__x86_64__) #if defined (__x86_64__)
Hal::PrepareGDT(); Hal::PrepareGDT();
Hal::BridgeLoadGDT(); Hal::BridgeLoadGDT();
// Enable SSE/SSE2 — required for userspace programs compiled with SSE
// CR0: clear EM (bit 2), set MP (bit 1)
{
uint64_t cr0;
asm volatile("mov %%cr0, %0" : "=r"(cr0));
cr0 &= ~(1ULL << 2); // Clear EM
cr0 |= (1ULL << 1); // Set MP
asm volatile("mov %0, %%cr0" :: "r"(cr0));
// CR4: set OSFXSR (bit 9) and OSXMMEXCPT (bit 10)
uint64_t cr4;
asm volatile("mov %%cr4, %0" : "=r"(cr4));
cr4 |= (1ULL << 9); // OSFXSR
cr4 |= (1ULL << 10); // OSXMMEXCPT
asm volatile("mov %0, %%cr4" :: "r"(cr4));
}
#endif #endif
uint64_t hhdm_offset = hhdm_request.response->offset; uint64_t hhdm_offset = hhdm_request.response->offset;
+7 -2
View File
@@ -77,12 +77,17 @@ namespace Memory {
}; };
} }
// Allocate() returns pages from the top of a free region in descending
// order, so 'first' is the highest address. The contiguous block
// actually starts (n-1) pages below 'first'.
void* base = (void*)((uint64_t)first - (uint64_t)(n - 1) * 0x1000);
if (ptr != nullptr) { if (ptr != nullptr) {
memcpy(first, ptr, n); memcpy(base, ptr, (uint64_t)n * 0x1000);
Free(ptr); Free(ptr);
} }
return first; return base;
} }
void PageFrameAllocator::Free(void* ptr) { void PageFrameAllocator::Free(void* ptr) {
+4 -9
View File
@@ -52,8 +52,6 @@ namespace Sched {
} }
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys) { uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys) {
Kt::KernelLogStream(Kt::INFO, "ELF") << "Loading " << vfsPath;
int handle = Fs::Vfs::VfsOpen(vfsPath); int handle = Fs::Vfs::VfsOpen(vfsPath);
if (handle < 0) { if (handle < 0) {
Kt::KernelLogStream(Kt::ERROR, "ELF") << "Failed to open " << vfsPath; Kt::KernelLogStream(Kt::ERROR, "ELF") << "Failed to open " << vfsPath;
@@ -78,6 +76,10 @@ namespace Sched {
Fs::Vfs::VfsRead(handle, fileData, 0, fileSize); Fs::Vfs::VfsRead(handle, fileData, 0, fileSize);
Fs::Vfs::VfsClose(handle); Fs::Vfs::VfsClose(handle);
// Prevent the optimizer from reordering the VfsRead store past the
// header validation reads that follow.
asm volatile("" ::: "memory");
// Validate ELF header // Validate ELF header
Elf64Header* hdr = (Elf64Header*)fileData; Elf64Header* hdr = (Elf64Header*)fileData;
if (!ValidateElfHeader(hdr)) { if (!ValidateElfHeader(hdr)) {
@@ -85,9 +87,6 @@ namespace Sched {
return 0; return 0;
} }
Kt::KernelLogStream(Kt::OK, "ELF") << "Entry point: " << kcp::hex << hdr->e_entry << kcp::dec
<< ", " << (uint64_t)hdr->e_phnum << " program header(s)";
// Process program headers // Process program headers
for (uint16_t i = 0; i < hdr->e_phnum; i++) { for (uint16_t i = 0; i < hdr->e_phnum; i++) {
Elf64ProgramHeader* phdr = (Elf64ProgramHeader*)(fileData + hdr->e_phoff + i * hdr->e_phentsize); Elf64ProgramHeader* phdr = (Elf64ProgramHeader*)(fileData + hdr->e_phoff + i * hdr->e_phentsize);
@@ -100,9 +99,6 @@ namespace Sched {
continue; continue;
} }
Kt::KernelLogStream(Kt::INFO, "ELF") << "PT_LOAD: vaddr=" << kcp::hex << phdr->p_vaddr
<< " filesz=" << phdr->p_filesz << " memsz=" << phdr->p_memsz << kcp::dec;
// Allocate pages and map them in the process PML4 with User bit // Allocate pages and map them in the process PML4 with User bit
uint64_t segBase = phdr->p_vaddr & ~0xFFFULL; uint64_t segBase = phdr->p_vaddr & ~0xFFFULL;
uint64_t segEnd = (phdr->p_vaddr + phdr->p_memsz + 0xFFF) & ~0xFFFULL; uint64_t segEnd = (phdr->p_vaddr + phdr->p_memsz + 0xFFF) & ~0xFFFULL;
@@ -147,7 +143,6 @@ namespace Sched {
uint64_t entryPoint = hdr->e_entry; uint64_t entryPoint = hdr->e_entry;
Memory::g_heap->Free(fileData); Memory::g_heap->Free(fileData);
Kt::KernelLogStream(Kt::OK, "ELF") << "Loaded successfully, entry=" << kcp::hex << entryPoint << kcp::dec;
return entryPoint; return entryPoint;
} }
+25 -14
View File
@@ -75,6 +75,7 @@ namespace Sched {
processTable[i].kernelStackTop = 0; processTable[i].kernelStackTop = 0;
processTable[i].userStackTop = 0; processTable[i].userStackTop = 0;
processTable[i].heapNext = 0; processTable[i].heapNext = 0;
processTable[i].args[0] = '\0';
} }
currentPid = -1; currentPid = -1;
@@ -85,7 +86,7 @@ namespace Sched {
<< " process slots, " << (uint64_t)TimeSliceMs << " ms time slice)"; << " process slots, " << (uint64_t)TimeSliceMs << " ms time slice)";
} }
void Spawn(const char* vfsPath) { int Spawn(const char* vfsPath, const char* args) {
int slot = -1; int slot = -1;
for (int i = 0; i < MaxProcesses; i++) { for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Free) { if (processTable[i].state == ProcessState::Free) {
@@ -96,7 +97,7 @@ namespace Sched {
if (slot < 0) { if (slot < 0) {
Kt::KernelLogStream(Kt::ERROR, "Sched") << "No free process slots"; Kt::KernelLogStream(Kt::ERROR, "Sched") << "No free process slots";
return; return -1;
} }
// Create per-process PML4 with kernel-half copied // Create per-process PML4 with kernel-half copied
@@ -106,20 +107,20 @@ namespace Sched {
uint64_t entry = ElfLoad(vfsPath, pml4Phys); uint64_t entry = ElfLoad(vfsPath, pml4Phys);
if (entry == 0) { if (entry == 0) {
Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to load ELF: " << vfsPath; Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to load ELF: " << vfsPath;
return; return -1;
} }
// Allocate kernel stack (used during syscalls and interrupts) // Allocate kernel stack (used during syscalls and interrupts)
void* firstPage = Memory::g_pfa->AllocateZeroed(); void* firstPage = Memory::g_pfa->AllocateZeroed();
if (firstPage == nullptr) { if (firstPage == nullptr) {
Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for kernel stack"; Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for kernel stack";
return; return -1;
} }
void* stackMem = Memory::g_pfa->ReallocConsecutive(firstPage, StackPages); void* stackMem = Memory::g_pfa->ReallocConsecutive(firstPage, StackPages);
if (stackMem == nullptr) { if (stackMem == nullptr) {
Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to allocate contiguous kernel stack"; Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to allocate contiguous kernel stack";
Memory::g_pfa->Free(firstPage); Memory::g_pfa->Free(firstPage);
return; return -1;
} }
uint8_t* kernelStackBase = (uint8_t*)stackMem; uint8_t* kernelStackBase = (uint8_t*)stackMem;
@@ -132,7 +133,7 @@ namespace Sched {
void* page = Memory::g_pfa->AllocateZeroed(); void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) { if (page == nullptr) {
Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for user stack"; Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for user stack";
return; return -1;
} }
uint64_t physAddr = Memory::SubHHDM((uint64_t)page); uint64_t physAddr = Memory::SubHHDM((uint64_t)page);
Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, userStackBase + i * 0x1000); Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, userStackBase + i * 0x1000);
@@ -145,7 +146,7 @@ namespace Sched {
void* stubPage = Memory::g_pfa->AllocateZeroed(); void* stubPage = Memory::g_pfa->AllocateZeroed();
if (stubPage == nullptr) { if (stubPage == nullptr) {
Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for exit stub"; Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for exit stub";
return; return -1;
} }
uint64_t stubPhys = Memory::SubHHDM((uint64_t)stubPage); uint64_t stubPhys = Memory::SubHHDM((uint64_t)stubPage);
Memory::VMM::Paging::MapUserIn(pml4Phys, stubPhys, ExitStubAddr); Memory::VMM::Paging::MapUserIn(pml4Phys, stubPhys, ExitStubAddr);
@@ -189,11 +190,17 @@ namespace Sched {
proc.userStackTop = UserStackTop - 8; // account for pushed exit stub return address proc.userStackTop = UserStackTop - 8; // account for pushed exit stub return address
proc.heapNext = UserHeapBase; proc.heapNext = UserHeapBase;
Kt::KernelLogStream(Kt::OK, "Sched") << "Spawned process " << (uint64_t)proc.pid // Copy arguments string into process
<< " (" << vfsPath << ") entry=" << kcp::hex << entry proc.args[0] = '\0';
<< " kstack=" << (uint64_t)kernelStackBase << "-" << kernelStackTop if (args != nullptr) {
<< " ustack=" << userStackBase << "-" << UserStackTop int i = 0;
<< " pml4=" << pml4Phys << kcp::dec; for (; i < 255 && args[i]; i++) {
proc.args[i] = args[i];
}
proc.args[i] = '\0';
}
return proc.pid;
} }
void Schedule() { void Schedule() {
@@ -271,8 +278,6 @@ namespace Sched {
return; return;
} }
Kt::KernelLogStream(Kt::OK, "Sched") << "Process " << (uint64_t)processTable[currentPid].pid << " terminated";
processTable[currentPid].state = ProcessState::Terminated; processTable[currentPid].state = ProcessState::Terminated;
int next = -1; int next = -1;
@@ -305,4 +310,10 @@ namespace Sched {
} }
} }
bool IsAlive(int pid) {
if (pid < 0 || pid >= MaxProcesses) return false;
return processTable[pid].state == ProcessState::Ready
|| processTable[pid].state == ProcessState::Running;
}
} }
+5 -1
View File
@@ -38,10 +38,11 @@ namespace Sched {
uint64_t kernelStackTop; // Top of kernel stack (for TSS RSP0 / SYSCALL) uint64_t kernelStackTop; // Top of kernel stack (for TSS RSP0 / SYSCALL)
uint64_t userStackTop; // User-space stack top uint64_t userStackTop; // User-space stack top
uint64_t heapNext; // Simple bump allocator for user heap uint64_t heapNext; // Simple bump allocator for user heap
char args[256]; // Command-line arguments (set by parent via Spawn)
}; };
void Initialize(); void Initialize();
void Spawn(const char* vfsPath); int Spawn(const char* vfsPath, const char* args = nullptr);
void Schedule(); void Schedule();
// Called from the APIC timer handler on every tick. // Called from the APIC timer handler on every tick.
@@ -56,4 +57,7 @@ namespace Sched {
// Called by terminated processes to mark themselves done // Called by terminated processes to mark themselves done
void ExitProcess(); void ExitProcess();
// Check if a process is still alive (Ready or Running)
bool IsAlive(int pid);
} }
+6
View File
@@ -166,3 +166,9 @@ namespace Kt
}; };
extern Kt::KernelErrorStream kerr; extern Kt::KernelErrorStream kerr;
// Forward-declare flanterm context for syscall access
struct flanterm_context;
namespace Kt {
extern flanterm_context *ctx;
}
+11 -1
View File
@@ -59,9 +59,19 @@ PROGRAMS := $(notdir $(wildcard src/*))
# Build targets: one ELF per program. # Build targets: one ELF per program.
TARGETS := $(addprefix $(BINDIR)/,$(addsuffix .elf,$(PROGRAMS))) TARGETS := $(addprefix $(BINDIR)/,$(addsuffix .elf,$(PROGRAMS)))
# Man pages source directory.
MANDIR := man
MANSRC := $(wildcard $(MANDIR)/*.*)
MANDST := $(patsubst $(MANDIR)/%,$(BINDIR)/man/%,$(MANSRC))
.PHONY: all clean .PHONY: all clean
all: $(TARGETS) all: $(TARGETS) $(MANDST)
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
$(BINDIR)/man/%: $(MANDIR)/%
mkdir -p $(BINDIR)/man
cp $< $@
# Build each program from its source files. # Build each program from its source files.
# For now each program is a single .cpp file compiled and linked directly. # For now each program is a single .cpp file compiled and linked directly.
Binary file not shown.
Binary file not shown.
+14
View File
@@ -31,6 +31,20 @@ namespace Zenith {
static constexpr uint64_t SYS_GETKEY = 17; static constexpr uint64_t SYS_GETKEY = 17;
static constexpr uint64_t SYS_GETCHAR = 18; static constexpr uint64_t SYS_GETCHAR = 18;
static constexpr uint64_t SYS_PING = 19; static constexpr uint64_t SYS_PING = 19;
static constexpr uint64_t SYS_SPAWN = 20;
static constexpr uint64_t SYS_FBINFO = 21;
static constexpr uint64_t SYS_FBMAP = 22;
static constexpr uint64_t SYS_WAITPID = 23;
static constexpr uint64_t SYS_TERMSIZE = 24;
static constexpr uint64_t SYS_GETARGS = 25;
struct FbInfo {
uint64_t width;
uint64_t height;
uint64_t pitch; // bytes per scanline
uint64_t bpp; // bits per pixel (32)
uint64_t userAddr; // filled by SYS_FBMAP (0 until mapped)
};
struct SysInfo { struct SysInfo {
char osName[32]; char osName[32];
+23
View File
@@ -0,0 +1,23 @@
#ifndef _LIBC_ASSERT_H
#define _LIBC_ASSERT_H
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void __assert_fail(const char *expr, const char *file, int line, const char *func);
#ifdef NDEBUG
#define assert(expr) ((void)0)
#else
#define assert(expr) \
((expr) ? (void)0 : __assert_fail(#expr, __FILE__, __LINE__, __func__))
#endif
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_ASSERT_H */
+28
View File
@@ -0,0 +1,28 @@
#ifndef _LIBC_CTYPE_H
#define _LIBC_CTYPE_H
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
int isalpha(int c);
int isdigit(int c);
int isalnum(int c);
int isspace(int c);
int isupper(int c);
int islower(int c);
int isprint(int c);
int ispunct(int c);
int isxdigit(int c);
int iscntrl(int c);
int isgraph(int c);
int toupper(int c);
int tolower(int c);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_CTYPE_H */
+29
View File
@@ -0,0 +1,29 @@
#ifndef _LIBC_ERRNO_H
#define _LIBC_ERRNO_H
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
extern int errno;
#define ENOENT 2
#define EIO 5
#define ENOMEM 12
#define EACCES 13
#define EINVAL 22
#define ERANGE 34
#define ENOSYS 38
#define EISDIR 21
#define ENOTDIR 20
#define EEXIST 17
#define EBADF 9
#define EPERM 1
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_ERRNO_H */
+22
View File
@@ -0,0 +1,22 @@
#ifndef _LIBC_FCNTL_H
#define _LIBC_FCNTL_H
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#define O_RDONLY 0
#define O_WRONLY 1
#define O_RDWR 2
#define O_CREAT 0x40
#define O_TRUNC 0x200
int open(const char *path, int flags, ...);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_FCNTL_H */
+27
View File
@@ -0,0 +1,27 @@
#ifndef _LIBC_INTTYPES_H
#define _LIBC_INTTYPES_H
#include <stdint.h>
#define PRId8 "d"
#define PRId16 "d"
#define PRId32 "d"
#define PRId64 "ld"
#define PRIi8 "i"
#define PRIi16 "i"
#define PRIi32 "i"
#define PRIi64 "li"
#define PRIu8 "u"
#define PRIu16 "u"
#define PRIu32 "u"
#define PRIu64 "lu"
#define PRIx8 "x"
#define PRIx16 "x"
#define PRIx32 "x"
#define PRIx64 "lx"
#define PRIX8 "X"
#define PRIX16 "X"
#define PRIX32 "X"
#define PRIX64 "lX"
#endif /* _LIBC_INTTYPES_H */
+31
View File
@@ -0,0 +1,31 @@
#ifndef _LIBC_LIMITS_H
#define _LIBC_LIMITS_H
#pragma once
#define CHAR_BIT 8
#define SCHAR_MIN (-128)
#define SCHAR_MAX 127
#define UCHAR_MAX 255
#define SHRT_MIN (-32768)
#define SHRT_MAX 32767
#define USHRT_MAX 65535
#define INT_MIN (-2147483647 - 1)
#define INT_MAX 2147483647
#define UINT_MAX 4294967295U
#define LONG_MIN (-9223372036854775807L - 1)
#define LONG_MAX 9223372036854775807L
#define ULONG_MAX 18446744073709551615UL
#define LLONG_MIN (-9223372036854775807LL - 1)
#define LLONG_MAX 9223372036854775807LL
#define ULLONG_MAX 18446744073709551615ULL
#define PATH_MAX 4096
#define NAME_MAX 256
#endif /* _LIBC_LIMITS_H */
+31
View File
@@ -0,0 +1,31 @@
#ifndef _LIBC_MATH_H
#define _LIBC_MATH_H
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#define HUGE_VAL __builtin_huge_val()
#define INFINITY __builtin_inff()
#define NAN __builtin_nanf("")
double fabs(double x);
double floor(double x);
double ceil(double x);
double sqrt(double x);
double sin(double x);
double cos(double x);
double atan2(double y, double x);
double pow(double base, double exp);
double log(double x);
double exp(double x);
double fmod(double x, double y);
double round(double x);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_MATH_H */
+77
View File
@@ -0,0 +1,77 @@
#ifndef _LIBC_STDIO_H
#define _LIBC_STDIO_H
#pragma once
#include <stdarg.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define EOF (-1)
#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2
#define BUFSIZ 1024
#define FILENAME_MAX 256
typedef struct _FILE {
int handle;
unsigned long pos;
unsigned long size;
int eof;
int error;
int is_std;
int ungetc_buf;
} FILE;
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
int printf(const char *fmt, ...);
int fprintf(FILE *stream, const char *fmt, ...);
int sprintf(char *str, const char *fmt, ...);
int snprintf(char *str, size_t size, const char *fmt, ...);
int vprintf(const char *fmt, va_list ap);
int vfprintf(FILE *stream, const char *fmt, va_list ap);
int vsprintf(char *str, const char *fmt, va_list ap);
int vsnprintf(char *str, size_t size, const char *fmt, va_list ap);
int puts(const char *s);
int putchar(int c);
FILE *fopen(const char *path, const char *mode);
int fclose(FILE *stream);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
int fflush(FILE *stream);
int rename(const char *oldpath, const char *newpath);
int remove(const char *path);
int sscanf(const char *str, const char *fmt, ...);
int feof(FILE *stream);
int ferror(FILE *stream);
void clearerr(FILE *stream);
int fgetc(FILE *stream);
int getc(FILE *stream);
int ungetc(int c, FILE *stream);
char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);
void perror(const char *s);
FILE *tmpfile(void);
char *tmpnam(char *s);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_STDIO_H */
+66
View File
@@ -0,0 +1,66 @@
#ifndef _LIBC_STDLIB_H
#define _LIBC_STDLIB_H
#pragma once
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef NULL
#define NULL ((void *)0)
#endif
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
#define RAND_MAX 0x7fffffff
typedef struct {
int quot;
int rem;
} div_t;
typedef struct {
long quot;
long rem;
} ldiv_t;
void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
int atoi(const char *s);
long atol(const char *s);
double atof(const char *s);
int abs(int j);
long labs(long j);
void exit(int status);
void abort(void);
int atexit(void (*func)(void));
char *getenv(const char *name);
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
long strtol(const char *nptr, char **endptr, int base);
unsigned long strtoul(const char *nptr, char **endptr, int base);
int rand(void);
void srand(unsigned int seed);
div_t div(int numer, int denom);
ldiv_t ldiv(long numer, long denom);
int system(const char *command);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_STDLIB_H */
+35
View File
@@ -0,0 +1,35 @@
#ifndef _LIBC_STRING_H
#define _LIBC_STRING_H
#pragma once
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
void *memcpy(void *dest, const void *src, size_t n);
void *memset(void *s, int c, size_t n);
void *memmove(void *dest, const void *src, size_t n);
int memcmp(const void *s1, const void *s2, size_t n);
size_t strlen(const char *s);
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
char *strcpy(char *dest, const char *src);
char *strncpy(char *dest, const char *src, size_t n);
char *strcat(char *dest, const char *src);
char *strncat(char *dest, const char *src, size_t n);
char *strdup(const char *s);
char *strchr(const char *s, int c);
char *strrchr(const char *s, int c);
int strcasecmp(const char *s1, const char *s2);
int strncasecmp(const char *s1, const char *s2, size_t n);
char *strstr(const char *haystack, const char *needle);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_STRING_H */
+17
View File
@@ -0,0 +1,17 @@
#ifndef _LIBC_STRINGS_H
#define _LIBC_STRINGS_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
int strcasecmp(const char *s1, const char *s2);
int strncasecmp(const char *s1, const char *s2, size_t n);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_STRINGS_H */
+24
View File
@@ -0,0 +1,24 @@
#ifndef _LIBC_SYS_STAT_H
#define _LIBC_SYS_STAT_H
#pragma once
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
struct stat {
unsigned long st_size;
};
int mkdir(const char *path, unsigned int mode);
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_SYS_STAT_H */
+24
View File
@@ -0,0 +1,24 @@
#ifndef _LIBC_SYS_TIME_H
#define _LIBC_SYS_TIME_H
/* Stub header for ZenithOS */
#ifdef __cplusplus
extern "C" {
#endif
struct timeval {
long tv_sec;
long tv_usec;
};
struct timezone {
int tz_minuteswest;
int tz_dsttime;
};
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_SYS_TIME_H */
+23
View File
@@ -0,0 +1,23 @@
#ifndef _LIBC_SYS_TYPES_H
#define _LIBC_SYS_TYPES_H
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef unsigned long size_t;
typedef long ssize_t;
typedef long off_t;
typedef int pid_t;
typedef unsigned int mode_t;
typedef unsigned int uid_t;
typedef unsigned int gid_t;
typedef long time_t;
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_SYS_TYPES_H */
+20
View File
@@ -0,0 +1,20 @@
#ifndef _LIBC_UNISTD_H
#define _LIBC_UNISTD_H
#pragma once
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
int read(int fd, void *buf, size_t count);
int write(int fd, const void *buf, size_t count);
int close(int fd);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_UNISTD_H */
+133
View File
@@ -0,0 +1,133 @@
/*
* heap.h
* Userspace heap allocator for ZenithOS programs
* Free-list allocator backed by SYS_ALLOC page requests.
* Adapted from the kernel HeapAllocator.
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <zenith/syscall.h>
namespace zenith {
namespace heap_detail {
static constexpr uint64_t HEADER_MAGIC = 0x5A484541; // "ZHEA"
struct Header {
uint64_t magic;
uint64_t size; // user-requested size
} __attribute__((packed));
struct FreeNode {
uint64_t size; // total size of this free block (including node)
FreeNode* next;
};
// Per-process heap state (single TU per program, so static is fine)
static FreeNode g_head{0, nullptr};
static bool g_initialized = false;
static inline Header* get_header(void* block) {
return (Header*)((uint8_t*)block - sizeof(Header));
}
static inline void insert_free(void* ptr, uint64_t size) {
auto* node = (FreeNode*)ptr;
node->size = size;
node->next = g_head.next;
g_head.next = node;
}
static inline void grow(uint64_t bytes) {
uint64_t pages = (bytes + 0xFFF) / 0x1000;
if (pages < 4) pages = 4; // grow at least 16 KiB at a time
void* mem = zenith::alloc(pages * 0x1000);
if (mem != nullptr)
insert_free(mem, pages * 0x1000);
}
} // namespace heap_detail
// ---- Public API ----
inline void* malloc(uint64_t size) {
using namespace heap_detail;
if (!g_initialized) {
grow(16 * 0x1000); // seed with 64 KiB
g_initialized = true;
}
uint64_t needed = size + sizeof(Header);
needed = (needed + 15) & ~15ULL; // 16-byte alignment
FreeNode* prev = &g_head;
FreeNode* current = g_head.next;
while (current != nullptr) {
if (current->size >= needed) {
uint64_t blockSize = current->size;
// Unlink
prev->next = current->next;
// Split if worthwhile
if (blockSize > needed + sizeof(FreeNode) + 16) {
void* rest = (void*)((uint8_t*)current + needed);
uint64_t restSize = blockSize - needed;
insert_free(rest, restSize);
}
// Write allocation header
Header* header = (Header*)current;
header->magic = HEADER_MAGIC;
header->size = size;
return (void*)((uint8_t*)header + sizeof(Header));
}
prev = current;
current = current->next;
}
// No fit — grow and retry
grow(needed);
return malloc(size);
}
inline void mfree(void* ptr) {
using namespace heap_detail;
if (ptr == nullptr) return;
Header* header = get_header(ptr);
uint64_t blockSize = header->size + sizeof(Header);
blockSize = (blockSize + 15) & ~15ULL;
insert_free((void*)header, blockSize);
}
inline void* realloc(void* ptr, uint64_t size) {
if (ptr == nullptr) return malloc(size);
auto* header = heap_detail::get_header(ptr);
uint64_t old = header->size;
void* newBlock = malloc(size);
if (newBlock == nullptr) return nullptr;
// Copy the smaller of old/new sizes
uint64_t copySize = (old < size) ? old : size;
uint8_t* dst = (uint8_t*)newBlock;
uint8_t* src = (uint8_t*)ptr;
for (uint64_t i = 0; i < copySize; i++)
dst[i] = src[i];
mfree(ptr);
return newBlock;
}
} // namespace zenith
+22
View File
@@ -117,6 +117,9 @@ namespace zenith {
inline void yield() { syscall0(Zenith::SYS_YIELD); } inline void yield() { syscall0(Zenith::SYS_YIELD); }
inline void sleep_ms(uint64_t ms) { syscall1(Zenith::SYS_SLEEP_MS, ms); } inline void sleep_ms(uint64_t ms) { syscall1(Zenith::SYS_SLEEP_MS, ms); }
inline int getpid() { return (int)syscall0(Zenith::SYS_GETPID); } inline int getpid() { return (int)syscall0(Zenith::SYS_GETPID); }
inline int spawn(const char* path, const char* args = nullptr) {
return (int)syscall2(Zenith::SYS_SPAWN, (uint64_t)path, (uint64_t)args);
}
// Console // Console
inline void print(const char* text) { syscall1(Zenith::SYS_PRINT, (uint64_t)text); } inline void print(const char* text) { syscall1(Zenith::SYS_PRINT, (uint64_t)text); }
@@ -154,4 +157,23 @@ namespace zenith {
return (int32_t)syscall2(Zenith::SYS_PING, (uint64_t)ip, (uint64_t)timeoutMs); return (int32_t)syscall2(Zenith::SYS_PING, (uint64_t)ip, (uint64_t)timeoutMs);
} }
// Process management
inline void waitpid(int pid) { syscall1(Zenith::SYS_WAITPID, (uint64_t)pid); }
// Framebuffer
inline void fb_info(Zenith::FbInfo* info) { syscall1(Zenith::SYS_FBINFO, (uint64_t)info); }
inline void* fb_map() { return (void*)syscall0(Zenith::SYS_FBMAP); }
// Arguments
inline int getargs(char* buf, uint64_t maxLen) {
return (int)syscall2(Zenith::SYS_GETARGS, (uint64_t)buf, maxLen);
}
// Terminal
inline void termsize(int* cols, int* rows) {
uint64_t r = (uint64_t)syscall0(Zenith::SYS_TERMSIZE);
if (cols) *cols = (int)(r & 0xFFFFFFFF);
if (rows) *rows = (int)(r >> 32);
}
} }
+74
View File
@@ -0,0 +1,74 @@
.TH FILE 2
.SH NAME
open, read, getsize, close, readdir - file I/O system calls
.SH SYNOPSIS
.BI int zenith::open(const char* path);
.BI int zenith::read(int handle, uint8_t* buf, uint64_t offset, uint64_t size);
.BI uint64_t zenith::getsize(int handle);
.BI void zenith::close(int handle);
.BI int zenith::readdir(const char* path, const char** names, int max);
.SH DESCRIPTION
ZenithOS provides a simple read-only Virtual File System (VFS)
backed by the boot ramdisk. Files are accessed via paths in the
format "<drive>:/<name>", where drive 0 is the ramdisk.
.SS open
Opens a file and returns a non-negative handle on success, or a
negative value on error (file not found, no free handles).
int h = zenith::open("0:/shell.elf");
.SS read
Reads up to 'size' bytes starting at 'offset' into 'buf'.
Returns the number of bytes actually read, or negative on error.
There is no implicit file position -- the offset is explicit on
every call.
uint8_t buf[512];
int n = zenith::read(h, buf, 0, 512);
.SS getsize
Returns the total size in bytes of the file.
uint64_t sz = zenith::getsize(h);
.SS close
Closes the file handle and frees kernel resources.
zenith::close(h);
.SS readdir
Lists entries in a directory. Up to 'max' entry names (max 64)
are written to the 'names' array. The kernel allocates a user-
accessible page for the string data automatically.
const char* entries[64];
int count = zenith::readdir("0:/", entries, 64);
.SH READING PATTERN
The standard pattern for reading a file:
int h = zenith::open("0:/myfile.txt");
uint64_t size = zenith::getsize(h);
uint8_t buf[512];
uint64_t off = 0;
while (off < size) {
uint64_t chunk = size - off;
if (chunk > 511) chunk = 511;
int n = zenith::read(h, buf, off, chunk);
if (n <= 0) break;
buf[n] = '\0';
zenith::print((const char*)buf);
off += n;
}
zenith::close(h);
.SH NOTES
The filesystem is read-only. There are no write or create calls.
All files live on the ramdisk which is loaded at boot from a
USTAR tar archive.
.SH SEE ALSO
syscalls(2), spawn(2), malloc(3)
+62
View File
@@ -0,0 +1,62 @@
.TH FRAMEBUFFER 2
.SH NAME
fb_info, fb_map - direct framebuffer access
.SH SYNOPSIS
.BI void zenith::fb_info(Zenith::FbInfo* info);
.BI void* zenith::fb_map();
.SH DESCRIPTION
These syscalls allow userspace programs to access the linear
framebuffer directly for graphical output.
.SS fb_info
Fills in an FbInfo structure with the framebuffer geometry:
Zenith::FbInfo fb;
zenith::fb_info(&fb);
// fb.width, fb.height, fb.pitch, fb.bpp
The pitch is the number of bytes per scanline (may be larger
than width * 4 due to alignment). bpp is always 32.
.SS fb_map
Maps the physical framebuffer into the process address space at
a fixed virtual address (0x50000000) and returns that address.
uint32_t* pixels = (uint32_t*)zenith::fb_map();
Each pixel is a 32-bit value in 0xAARRGGBB format (blue in the
low byte). Writing to this memory directly updates the screen.
.SH PIXEL FORMAT
Bits 31-24: Alpha (unused, typically 0xFF)
Bits 23-16: Red
Bits 15-8: Green
Bits 7-0: Blue
Example: red = 0x00FF0000, green = 0x0000FF00, blue = 0x000000FF
.SH EXAMPLE
Fill the screen with blue:
Zenith::FbInfo fb;
zenith::fb_info(&fb);
uint32_t* pixels = (uint32_t*)zenith::fb_map();
for (uint64_t y = 0; y < fb.height; y++) {
uint32_t* row = (uint32_t*)((uint8_t*)pixels + y * fb.pitch);
for (uint64_t x = 0; x < fb.width; x++) {
row[x] = 0x000000FF;
}
}
.SH NOTES
After mapping, the cursor overlay is not composited. Programs
that use the framebuffer take full control of screen output.
Only one mapping per process is supported. Calling fb_map()
multiple times returns the same address.
.SH SEE ALSO
syscalls(2), malloc(3)
+49
View File
@@ -0,0 +1,49 @@
.TH INTRO 1
.SH NAME
intro - introduction to ZenithOS userspace
.SH DESCRIPTION
ZenithOS is a hobbyist 64-bit operating system written in C++20.
Userspace programs run in Ring 3, are loaded as static ELF64
binaries, and communicate with the kernel through the x86-64
SYSCALL/SYSRET mechanism.
Programs are compiled with a freestanding cross-compiler and
linked at virtual address 0x400000. There is no standard C
library for C++ programs -- all system interaction goes through
the zenith:: syscall wrappers.
.SH GETTING STARTED
To write a new program, create a directory under programs/src/
with a main.cpp file. The entry point is:
extern "C" void _start() { ... }
There is no argc/argv. Include <zenith/syscall.h> for the full
typed syscall API. Include <zenith/heap.h> for malloc/mfree.
Build with:
cd programs && make
The resulting ELF binary appears in programs/bin/.
.SH SHELL
The built-in shell is the primary way to interact with ZenithOS.
Type 'help' at the shell prompt for a list of commands. Use
'man shell' for detailed shell documentation.
.SH MAN PAGES
The following man pages are available:
intro(1) This page
shell(1) Shell commands reference
man(1) The man command itself
syscalls(2) Overview of all syscalls
spawn(2) Process spawning
file(2) File I/O syscalls
framebuffer(2) Framebuffer access
malloc(3) Memory allocation
.SH SEE ALSO
shell(1), syscalls(2), malloc(3)
+57
View File
@@ -0,0 +1,57 @@
.TH MALLOC 3
.SH NAME
malloc, mfree, realloc - userspace heap allocation
.SH SYNOPSIS
.BI void* zenith::malloc(uint64_t size);
.BI void zenith::mfree(void* ptr);
.BI void* zenith::realloc(void* ptr, uint64_t size);
.SH DESCRIPTION
The userspace heap provides dynamic memory allocation on top of
the kernel's page-mapping syscall (SYS_ALLOC). Include the
header <zenith/heap.h> to use these functions.
.SS malloc
Allocates 'size' bytes from the free list. Returns a 16-byte
aligned pointer, or nullptr on failure. When the free list is
empty, it requests more pages from the kernel via SYS_ALLOC
(minimum 16 KiB growth, initial seed of 64 KiB).
char* buf = (char*)zenith::malloc(1024);
.SS mfree
Returns the block to the userspace free list. No syscall is
made -- the memory stays mapped and is immediately reusable.
Passing nullptr is a safe no-op.
zenith::mfree(buf);
.SS realloc
Resizes the allocation to 'size' bytes. Allocates a new block,
copies the smaller of old/new sizes, and frees the old block.
If ptr is nullptr, behaves like malloc.
buf = (char*)zenith::realloc(buf, 2048);
.SH IMPLEMENTATION
The allocator uses a linked free-list with first-fit search.
Blocks larger than needed are split. The allocation header is
16 bytes (magic + size). All allocations are 16-byte aligned.
The heap grows by requesting pages from the kernel via
SYS_ALLOC. These pages are never returned to the kernel (since
SYS_FREE is currently a no-op), but mfree makes them available
for future malloc calls within the process.
.SH LOW-LEVEL PAGE API
For large allocations or when direct page control is needed:
void* zenith::alloc(uint64_t size); // SYS_ALLOC
void zenith::free(void* ptr); // SYS_FREE (no-op)
alloc() maps zeroed pages starting at 0x40000000 and growing
upward. Size is rounded up to 4 KiB page boundaries.
.SH SEE ALSO
syscalls(2), file(2)
+47
View File
@@ -0,0 +1,47 @@
.TH MAN 1
.SH NAME
man - display manual pages
.SH SYNOPSIS
.BI man topic
.BI man section topic
.SH DESCRIPTION
The man command displays manual pages from the ramdisk in a
fullscreen pager. Pages are stored as plain text files with
simple formatting directives.
If no section is specified, sections 1, 2, and 3 are searched
in order. If a section number is given, only that section is
checked.
.SH KEY BINDINGS
.B Navigation
j, Down Arrow Scroll down one line
k, Up Arrow Scroll up one line
Space, Page Down Scroll down one page
b, Page Up Scroll up one page
g, Home Go to top
G, End Go to bottom
q Quit
.SH SECTIONS
1 User commands (shell built-ins)
2 System calls (kernel interface)
3 Library functions (userspace libraries)
.SH FILES
Man pages are stored on the ramdisk at:
0:/man/<topic>.<section>
For example, man intro reads 0:/man/intro.1
.SH EXAMPLES
man intro View the introduction
man 2 syscalls View syscall overview (section 2)
man malloc View malloc documentation
.SH SEE ALSO
intro(1), shell(1), syscalls(2)
+54
View File
@@ -0,0 +1,54 @@
.TH SHELL 1
.SH NAME
shell - ZenithOS interactive command shell
.SH DESCRIPTION
The ZenithOS shell is a simple command interpreter that runs as
the first userspace process. It provides basic file inspection,
process management, networking, and documentation access.
.SH COMMANDS
.SS help
Display a list of available commands.
.SS info
Show the OS name, version, and syscall API version number.
.SS man <topic>
Open a manual page in the fullscreen pager. See man(1).
.SS ls
List all files on the ramdisk (drive 0:/).
.SS cat <file>
Print the contents of a ramdisk file to the terminal.
Example: cat hello.elf
.SS run <file>
Spawn a new process from an ELF binary on the ramdisk and wait
for it to exit. The shell blocks until the child process
terminates.
Example: run hello.elf
.SS ping <ip>
Send 4 ICMP echo requests to the given IP address and display
round-trip times. Timeout is 3 seconds per request.
Example: ping 10.0.2.2
.SS uptime
Display the system uptime in minutes, seconds, and milliseconds.
.SS clear
Clear the terminal screen.
.SS exit
Terminate the shell process.
.SH INPUT
The shell reads input character by character using SYS_GETCHAR.
Backspace is supported. Lines are limited to 255 characters.
There is no command history or tab completion.
.SH SEE ALSO
man(1), intro(1), syscalls(2)
+69
View File
@@ -0,0 +1,69 @@
.TH SPAWN 2
.SH NAME
spawn, waitpid - create and wait for processes
.SH SYNOPSIS
.BI int zenith::spawn(const char* path, const char* args = nullptr);
.BI void zenith::waitpid(int pid);
.BI int zenith::getargs(char* buf, uint64_t maxLen);
.SH DESCRIPTION
.SS spawn
Loads the ELF64 binary at the given VFS path and creates a new
process. The path must include the drive prefix, for example:
int pid = zenith::spawn("0:/hello.elf");
An optional second argument passes a string to the child:
int pid = zenith::spawn("0:/man.elf", "intro");
The new process gets its own PML4 page table, a 16 KiB stack
(at 0x7FFFFEF000-0x7FFFFFF000), and begins executing at the
ELF entry point (_start).
Returns the new process's PID on success, or -1 on failure.
Failure occurs when there are no free process slots (max 16),
the file cannot be found, or the ELF is invalid.
.SS waitpid
Blocks the calling process until the process with the given PID
has exited. Internally, this yields the CPU in a loop:
zenith::waitpid(pid);
This is how the shell implements foreground process execution --
it spawns a child and waits for it to complete before showing
the next prompt.
.SH EXAMPLES
Spawn a program and wait for it:
int pid = zenith::spawn("0:/hello.elf");
if (pid < 0) {
zenith::print("spawn failed\n");
} else {
zenith::waitpid(pid);
zenith::print("child exited\n");
}
.SS getargs
Copies the argument string into buf (up to maxLen bytes, always
null-terminated). Returns the number of characters copied, or
-1 on error.
char args[256];
zenith::getargs(args, sizeof(args));
The argument string is set by the parent when calling spawn().
If no arguments were provided, the buffer will be empty.
.SH NOTES
The _start() entry point receives no argc/argv. Use getargs()
to retrieve the argument string passed by the parent process.
Process exit codes are not yet collected by waitpid.
.SH SEE ALSO
syscalls(2), file(2)
+137
View File
@@ -0,0 +1,137 @@
.TH SYSCALLS 2
.SH NAME
syscalls - overview of ZenithOS system calls
.SH DESCRIPTION
ZenithOS provides 26 system calls (numbers 0-25) for userspace
programs. Syscalls use the x86-64 SYSCALL instruction with the
following register convention:
RAX Syscall number (in) / return value (out)
RDI Argument 1
RSI Argument 2
RDX Argument 3
R10 Argument 4
R8 Argument 5
R9 Argument 6
Include <zenith/syscall.h> for typed wrappers in the zenith::
namespace.
.SH PROCESS MANAGEMENT
.B SYS_EXIT (0)
Terminate the calling process.
void zenith::exit(int code = 0);
.B SYS_YIELD (1)
Yield the remainder of the time slice.
void zenith::yield();
.B SYS_SLEEP_MS (2)
Sleep for at least the given number of milliseconds.
void zenith::sleep_ms(uint64_t ms);
.B SYS_GETPID (3)
Return the PID of the calling process.
int zenith::getpid();
.B SYS_SPAWN (20)
Spawn a new process from an ELF binary on the VFS.
int zenith::spawn(const char* path, const char* args = nullptr);
.B SYS_WAITPID (23)
Block until the given process has exited.
void zenith::waitpid(int pid);
.SH CONSOLE I/O
.B SYS_PRINT (4)
Write a null-terminated string to the terminal.
void zenith::print(const char* text);
.B SYS_PUTCHAR (5)
Write a single character to the terminal.
void zenith::putchar(char c);
.SH FILE I/O
.B SYS_OPEN (6)
Open a file. Returns a handle or negative on error.
int zenith::open(const char* path);
.B SYS_READ (7)
Read bytes from a file at a given offset.
int zenith::read(int h, uint8_t* buf, uint64_t off, uint64_t sz);
.B SYS_GETSIZE (8)
Get the size of an open file in bytes.
uint64_t zenith::getsize(int handle);
.B SYS_CLOSE (9)
Close a file handle.
void zenith::close(int handle);
.B SYS_READDIR (10)
List directory entries (max 64 per call).
int zenith::readdir(const char* path, const char** names, int max);
.SH MEMORY
.B SYS_ALLOC (11)
Map zeroed pages into the process address space.
void* zenith::alloc(uint64_t size);
.B SYS_FREE (12)
Reserved (currently a no-op).
void zenith::free(void* ptr);
.SH TIMEKEEPING
.B SYS_GETTICKS (13)
Get APIC timer ticks since boot.
uint64_t zenith::get_ticks();
.B SYS_GETMILLISECONDS (14)
Get milliseconds elapsed since boot.
uint64_t zenith::get_milliseconds();
.SH SYSTEM
.B SYS_GETINFO (15)
Get OS name, version, and configuration.
void zenith::get_info(Zenith::SysInfo* info);
.SH KEYBOARD
.B SYS_ISKEYAVAILABLE (16)
Check if a key event is pending (non-blocking).
bool zenith::is_key_available();
.B SYS_GETKEY (17)
Get the next key event (press or release).
void zenith::getkey(Zenith::KeyEvent* out);
.B SYS_GETCHAR (18)
Block until a printable character is typed.
char zenith::getchar();
.SH NETWORKING
.B SYS_PING (19)
Send an ICMP echo request and wait for reply.
int32_t zenith::ping(uint32_t ip, uint32_t timeoutMs);
.SH FRAMEBUFFER
.B SYS_FBINFO (21)
Get framebuffer dimensions and format.
void zenith::fb_info(Zenith::FbInfo* info);
.B SYS_FBMAP (22)
Map the framebuffer into process memory at 0x50000000.
void* zenith::fb_map();
.SH TERMINAL
.B SYS_TERMSIZE (24)
Get terminal dimensions (columns and rows).
void zenith::termsize(int* cols, int* rows);
.SH ARGUMENTS
.B SYS_GETARGS (25)
Get the argument string passed to this process at spawn time.
int zenith::getargs(char* buf, uint64_t maxLen);
.SH SEE ALSO
spawn(2), file(2), framebuffer(2), malloc(3)
Binary file not shown.
Binary file not shown.
+187
View File
@@ -0,0 +1,187 @@
# Makefile for DOOM (doomgeneric) on ZenithOS
# Copyright (c) 2025 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
# ---- Toolchain ----
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CC := $(TOOLCHAIN_PREFIX)gcc
LD := $(TOOLCHAIN_PREFIX)gcc
else
CC := gcc
LD := gcc
endif
# ---- Paths ----
DOOM_SRC := ../../../doomgeneric/doomgeneric
LIBC_INC := ../../include/libc
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
# ---- Compiler flags ----
CFLAGS := \
-std=gnu11 \
-g -O2 -pipe \
-Wall \
-Wno-unused-parameter \
-Wno-unused-variable \
-Wno-missing-field-initializers \
-Wno-sign-compare \
-Wno-pointer-sign \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-DNORMALUNIX \
-DLINUX \
-isystem $(LIBC_INC) \
-I $(PROG_INC) \
-I $(DOOM_SRC) \
-isystem $(shell $(CC) -print-file-name=include)
# ---- Linker flags ----
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
# ---- DOOM source files (excluding platform-specific ports and sound backends) ----
DOOM_SRCS := \
am_map.c \
d_event.c \
d_items.c \
d_iwad.c \
d_loop.c \
d_main.c \
d_mode.c \
d_net.c \
doomdef.c \
doomgeneric.c \
doomstat.c \
dstrings.c \
dummy.c \
f_finale.c \
f_wipe.c \
g_game.c \
gusconf.c \
hu_lib.c \
hu_stuff.c \
i_cdmus.c \
i_endoom.c \
i_input.c \
i_joystick.c \
i_scale.c \
i_sound.c \
i_system.c \
i_timer.c \
i_video.c \
info.c \
m_argv.c \
m_bbox.c \
m_cheat.c \
m_config.c \
m_controls.c \
m_fixed.c \
m_menu.c \
m_misc.c \
m_random.c \
memio.c \
mus2mid.c \
p_ceilng.c \
p_doors.c \
p_enemy.c \
p_floor.c \
p_inter.c \
p_lights.c \
p_map.c \
p_maputl.c \
p_mobj.c \
p_plats.c \
p_pspr.c \
p_saveg.c \
p_setup.c \
p_sight.c \
p_spec.c \
p_switch.c \
p_telept.c \
p_tick.c \
p_user.c \
r_bsp.c \
r_data.c \
r_draw.c \
r_main.c \
r_plane.c \
r_segs.c \
r_sky.c \
r_things.c \
s_sound.c \
sha1.c \
sounds.c \
st_lib.c \
st_stuff.c \
statdump.c \
tables.c \
v_video.c \
w_checksum.c \
w_file.c \
w_file_stdc.c \
w_main.c \
w_wad.c \
wi_stuff.c \
z_zone.c
# Local source files
LOCAL_SRCS := doomgeneric_zenith.c libc.c
# ---- Object files ----
DOOM_OBJS := $(addprefix $(OBJDIR)/,$(DOOM_SRCS:.c=.o))
LOCAL_OBJS := $(addprefix $(OBJDIR)/,$(LOCAL_SRCS:.c=.o))
ALL_OBJS := $(DOOM_OBJS) $(LOCAL_OBJS)
# ---- Target ----
TARGET := $(BINDIR)/doom.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(ALL_OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)
$(LD) $(CFLAGS) $(LDFLAGS) $(ALL_OBJS) -o $@
# DOOM source files (from doomgeneric directory)
$(OBJDIR)/%.o: $(DOOM_SRC)/%.c Makefile
mkdir -p $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
# Local source files
$(OBJDIR)/%.o: %.c Makefile
mkdir -p $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
+230
View File
@@ -0,0 +1,230 @@
/*
* doomgeneric_zenith.c
* DOOM platform implementation for ZenithOS
* Copyright (c) 2025 Daniel Hammer
*/
#include "doomgeneric.h"
#include "doomkeys.h"
#include <string.h>
#include <stdio.h>
/* ---- Raw syscall interface (C versions) ---- */
static inline long _zos_syscall0(long nr) {
long ret;
__asm__ volatile("syscall" : "=a"(ret) : "a"(nr)
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
return ret;
}
static inline long _zos_syscall1(long nr, long a1) {
long ret;
__asm__ volatile(
"mov %[a1], %%rdi\n\t"
"syscall"
: "=a"(ret)
: "a"(nr), [a1] "r"(a1)
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
return ret;
}
/* Syscall numbers (must match kernel/src/Api/Syscall.hpp) */
#define SYS_EXIT 0
#define SYS_SLEEP_MS 2
#define SYS_PRINT 4
#define SYS_GETMILLISECONDS 14
#define SYS_ISKEYAVAILABLE 16
#define SYS_GETKEY 17
#define SYS_FBINFO 21
#define SYS_FBMAP 22
/* FbInfo struct (must match kernel definition) */
struct FbInfo {
unsigned long width;
unsigned long height;
unsigned long pitch;
unsigned long bpp;
unsigned long userAddr;
};
/* KeyEvent struct (must match kernel definition) */
struct KeyEvent {
unsigned char scancode;
char ascii;
unsigned char pressed;
unsigned char shift;
unsigned char ctrl;
unsigned char alt;
};
/* ---- Framebuffer state ---- */
static uint32_t* g_fbPtr = 0;
static uint32_t g_fbWidth = 0;
static uint32_t g_fbHeight = 0;
static uint32_t g_fbPitch = 0; /* bytes per scanline */
/* ---- Circular key queue ---- */
#define KEY_QUEUE_SIZE 64
struct KeyQueueEntry {
int pressed;
unsigned char doomkey;
};
static struct KeyQueueEntry g_keyQueue[KEY_QUEUE_SIZE];
static int g_keyQueueRead = 0;
static int g_keyQueueWrite = 0;
static void key_queue_push(int pressed, unsigned char doomkey) {
int next = (g_keyQueueWrite + 1) % KEY_QUEUE_SIZE;
if (next == g_keyQueueRead) return; /* full, drop */
g_keyQueue[g_keyQueueWrite].pressed = pressed;
g_keyQueue[g_keyQueueWrite].doomkey = doomkey;
g_keyQueueWrite = next;
}
static int key_queue_pop(int* pressed, unsigned char* doomkey) {
if (g_keyQueueRead == g_keyQueueWrite) return 0;
*pressed = g_keyQueue[g_keyQueueRead].pressed;
*doomkey = g_keyQueue[g_keyQueueRead].doomkey;
g_keyQueueRead = (g_keyQueueRead + 1) % KEY_QUEUE_SIZE;
return 1;
}
/* ---- PS/2 scancode to ASCII table (set 1, unshifted) ---- */
static const char scancode_to_ascii[128] = {
0, 27, '1','2','3','4','5','6','7','8','9','0','-','=','\b',
'\t','q','w','e','r','t','y','u','i','o','p','[',']','\n',
0, 'a','s','d','f','g','h','j','k','l',';','\'','`',
0, '\\','z','x','c','v','b','n','m',',','.','/', 0,
'*', 0, ' '
};
/* ---- PS/2 scancode to DOOM key mapping ---- */
static unsigned char scancode_to_doomkey(unsigned char scancode, char ascii) {
switch (scancode) {
case 0x48: return KEY_UPARROW;
case 0x50: return KEY_DOWNARROW;
case 0x4B: return KEY_LEFTARROW;
case 0x4D: return KEY_RIGHTARROW;
case 0x1C: return KEY_ENTER;
case 0x01: return KEY_ESCAPE;
case 0x39: return ' '; /* Space = use */
case 0x1D: return KEY_RCTRL; /* LCtrl = fire */
case 0x2A: return KEY_RSHIFT; /* LShift = run */
case 0x36: return KEY_RSHIFT; /* RShift = run */
case 0x38: return KEY_RALT; /* Alt = strafe */
case 0x0E: return KEY_BACKSPACE;
case 0x0F: return KEY_TAB;
/* F1-F10 */
case 0x3B: return KEY_F1;
case 0x3C: return KEY_F2;
case 0x3D: return KEY_F3;
case 0x3E: return KEY_F4;
case 0x3F: return KEY_F5;
case 0x40: return KEY_F6;
case 0x41: return KEY_F7;
case 0x42: return KEY_F8;
case 0x43: return KEY_F9;
case 0x44: return KEY_F10;
case 0x57: return KEY_F11;
case 0x58: return KEY_F12;
/* Equals and minus for screen size */
case 0x0D: return KEY_EQUALS;
case 0x0C: return KEY_MINUS;
default:
/* Pass through printable ASCII as lowercase */
if (ascii >= 'a' && ascii <= 'z') return (unsigned char)ascii;
if (ascii >= '0' && ascii <= '9') return (unsigned char)ascii;
return 0;
}
}
/* ---- Poll keyboard and enqueue events ---- */
static void poll_keyboard(void) {
while (_zos_syscall0(SYS_ISKEYAVAILABLE)) {
struct KeyEvent evt;
_zos_syscall1(SYS_GETKEY, (long)&evt);
unsigned char baseSc = evt.scancode & 0x7F; /* strip break bit */
char ascii = 0;
if (baseSc < 128)
ascii = scancode_to_ascii[baseSc];
unsigned char dk = scancode_to_doomkey(baseSc, ascii);
if (dk != 0) {
key_queue_push(evt.pressed ? 1 : 0, dk);
}
}
}
/* ---- DG platform functions ---- */
void DG_Init(void) {
struct FbInfo info;
_zos_syscall1(SYS_FBINFO, (long)&info);
g_fbWidth = (uint32_t)info.width;
g_fbHeight = (uint32_t)info.height;
g_fbPitch = (uint32_t)info.pitch;
g_fbPtr = (uint32_t*)(unsigned long)_zos_syscall0(SYS_FBMAP);
printf("DOOM: framebuffer %ux%u pitch=%u mapped at %p\n",
g_fbWidth, g_fbHeight, g_fbPitch, (void*)g_fbPtr);
}
void DG_DrawFrame(void) {
/* Poll keyboard first */
poll_keyboard();
/* Copy DG_ScreenBuffer (DOOMGENERIC_RESX x DOOMGENERIC_RESY) to framebuffer */
if (g_fbPtr == 0 || DG_ScreenBuffer == 0) return;
uint32_t copyW = DOOMGENERIC_RESX;
uint32_t copyH = DOOMGENERIC_RESY;
if (copyW > g_fbWidth) copyW = g_fbWidth;
if (copyH > g_fbHeight) copyH = g_fbHeight;
uint32_t fbStride = g_fbPitch / 4; /* pixels per scanline */
for (uint32_t y = 0; y < copyH; y++) {
uint32_t* dst = g_fbPtr + y * fbStride;
uint32_t* src = DG_ScreenBuffer + y * DOOMGENERIC_RESX;
memcpy(dst, src, copyW * sizeof(uint32_t));
}
}
void DG_SleepMs(uint32_t ms) {
_zos_syscall1(SYS_SLEEP_MS, (long)ms);
}
uint32_t DG_GetTicksMs(void) {
return (uint32_t)_zos_syscall0(SYS_GETMILLISECONDS);
}
int DG_GetKey(int* pressed, unsigned char* doomKey) {
return key_queue_pop(pressed, doomKey);
}
void DG_SetWindowTitle(const char* title) {
(void)title;
}
/* ---- Entry point ---- */
void _start(void) {
char *argv[] = { "doom", "-iwad", "0:/doom1.wad", 0 };
doomgeneric_Create(3, argv);
for (;;) {
doomgeneric_Tick();
}
}
File diff suppressed because it is too large Load Diff
+374
View File
@@ -0,0 +1,374 @@
/*
* main.cpp
* Manual page viewer for ZenithOS
* Fullscreen pager with ANSI formatting and keyboard navigation
* Copyright (c) 2025 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/heap.h>
// ---- Utility functions ----
static bool starts_with(const char* str, const char* prefix) {
while (*prefix) {
if (*str != *prefix) return false;
str++; prefix++;
}
return true;
}
static const char* skip_spaces(const char* s) {
while (*s == ' ') s++;
return s;
}
static int slen(const char* s) {
int n = 0;
while (s[n]) n++;
return n;
}
static void print_int(uint64_t n) {
if (n == 0) {
zenith::putchar('0');
return;
}
char buf[20];
int i = 0;
while (n > 0) {
buf[i++] = '0' + (n % 10);
n /= 10;
}
for (int j = i - 1; j >= 0; j--) {
zenith::putchar(buf[j]);
}
}
// ---- Pager rendering ----
static constexpr int MAN_MAX_LINES = 2048;
static int int_to_buf(char* buf, int n) {
if (n == 0) { buf[0] = '0'; return 1; }
char tmp[12];
int i = 0;
while (n > 0) { tmp[i++] = '0' + (n % 10); n /= 10; }
for (int j = 0; j < i; j++) buf[j] = tmp[i - 1 - j];
return i;
}
static void cursor_to(int row, int col) {
char seq[24] = "\033[";
int p = 2;
p += int_to_buf(seq + p, row);
seq[p++] = ';';
p += int_to_buf(seq + p, col);
seq[p++] = 'H';
seq[p] = '\0';
zenith::print(seq);
}
struct ManLine {
const char* text;
int len;
bool isSH;
bool isSS;
bool isBold;
bool isTH;
};
static void man_render(ManLine* lines, int totalLines, int scroll, int rows, int cols,
const char* name, int section) {
int contentRows = rows - 1;
for (int r = 0; r < contentRows; r++) {
cursor_to(r + 1, 1);
zenith::print("\033[2K");
int idx = scroll + r;
if (idx < 0 || idx >= totalLines) continue;
ManLine& ln = lines[idx];
if (ln.isTH) continue;
if (ln.isSH || ln.isSS || ln.isBold) {
zenith::print("\033[1m");
}
if (ln.isSS) {
zenith::print(" ");
}
int maxW = cols;
if (ln.isSS) maxW -= 3;
int printLen = ln.len;
if (printLen > maxW) printLen = maxW;
for (int c = 0; c < printLen; c++) {
zenith::putchar(ln.text[c]);
}
if (ln.isSH || ln.isSS || ln.isBold) {
zenith::print("\033[0m");
}
}
// Status bar
cursor_to(rows, 1);
zenith::print("\033[7m");
zenith::print(" Manual page ");
zenith::print(name);
zenith::putchar('(');
print_int((uint64_t)section);
zenith::putchar(')');
zenith::print(" line ");
print_int((uint64_t)(scroll + 1));
zenith::putchar('/');
print_int((uint64_t)totalLines);
int padCount = cols - 30 - slen(name);
for (int i = 0; i < padCount; i++) zenith::putchar(' ');
zenith::print("\033[0m");
}
// ---- Main ----
extern "C" void _start() {
// Get arguments passed by the shell (e.g. "intro" or "2 syscalls")
char argbuf[256];
zenith::getargs(argbuf, sizeof(argbuf));
const char* arg = skip_spaces(argbuf);
if (*arg == '\0') {
zenith::print("Usage: man <topic>\n");
zenith::print(" man <section> <topic>\n");
zenith::print("Try: man intro\n");
return;
}
// Parse optional section number and topic name
int section = 0;
const char* topic = arg;
if (arg[0] >= '1' && arg[0] <= '9' && arg[1] == ' ') {
section = arg[0] - '0';
topic = skip_spaces(arg + 2);
}
// Try to open man page file
int handle = -1;
int foundSection = 0;
char path[128];
if (section > 0) {
const char* prefix = "0:/man/";
int p = 0;
while (prefix[p]) { path[p] = prefix[p]; p++; }
int t = 0;
while (topic[t] && p < 120) { path[p++] = topic[t++]; }
path[p++] = '.';
path[p++] = '0' + section;
path[p] = '\0';
handle = zenith::open(path);
if (handle >= 0) foundSection = section;
} else {
for (int s = 1; s <= 3; s++) {
const char* prefix = "0:/man/";
int p = 0;
while (prefix[p]) { path[p] = prefix[p]; p++; }
int t = 0;
while (topic[t] && p < 120) { path[p++] = topic[t++]; }
path[p++] = '.';
path[p++] = '0' + s;
path[p] = '\0';
handle = zenith::open(path);
if (handle >= 0) {
foundSection = s;
break;
}
}
}
if (handle < 0) {
zenith::print("No manual entry for ");
zenith::print(topic);
zenith::putchar('\n');
return;
}
// Load entire file into heap
uint64_t fileSize = zenith::getsize(handle);
if (fileSize == 0) {
zenith::close(handle);
zenith::print("Empty manual page.\n");
return;
}
char* fileData = (char*)zenith::malloc(fileSize + 1);
if (fileData == nullptr) {
zenith::close(handle);
zenith::print("Out of memory.\n");
return;
}
uint64_t offset = 0;
while (offset < fileSize) {
uint64_t chunk = fileSize - offset;
if (chunk > 4096) chunk = 4096;
int bytesRead = zenith::read(handle, (uint8_t*)fileData + offset, offset, chunk);
if (bytesRead <= 0) break;
offset += bytesRead;
}
fileData[offset] = '\0';
zenith::close(handle);
// Parse into lines
ManLine* lines = (ManLine*)zenith::malloc(MAN_MAX_LINES * sizeof(ManLine));
if (lines == nullptr) {
zenith::mfree(fileData);
zenith::print("Out of memory.\n");
return;
}
int totalLines = 0;
const char* p = fileData;
while (*p && totalLines < MAN_MAX_LINES) {
const char* lineStart = p;
while (*p && *p != '\n') p++;
int lineLen = (int)(p - lineStart);
if (*p == '\n') p++;
ManLine& ln = lines[totalLines];
ln.isSH = false;
ln.isSS = false;
ln.isBold = false;
ln.isTH = false;
if (starts_with(lineStart, ".TH ")) {
ln.isTH = true;
ln.text = lineStart + 4;
ln.len = lineLen - 4;
} else if (starts_with(lineStart, ".SH ")) {
ln.isSH = true;
ln.text = lineStart + 4;
ln.len = lineLen - 4;
} else if (starts_with(lineStart, ".SS ")) {
ln.isSS = true;
ln.text = lineStart + 4;
ln.len = lineLen - 4;
} else if (starts_with(lineStart, ".B ")) {
ln.isBold = true;
ln.text = lineStart + 3;
ln.len = lineLen - 3;
} else if (starts_with(lineStart, ".BI ")) {
ln.isBold = true;
ln.text = lineStart + 4;
ln.len = lineLen - 4;
} else {
ln.text = lineStart;
ln.len = lineLen;
}
totalLines++;
}
if (totalLines == 0) {
zenith::mfree(lines);
zenith::mfree(fileData);
zenith::print("Empty manual page.\n");
return;
}
// Get terminal dimensions
int cols = 80, rows = 25;
zenith::termsize(&cols, &rows);
// Enter alternate screen, hide cursor
zenith::print("\033[?1049h");
zenith::print("\033[?25l");
int scroll = 0;
int maxScroll = totalLines - (rows - 1);
if (maxScroll < 0) maxScroll = 0;
man_render(lines, totalLines, scroll, rows, cols, topic, foundSection);
// Event loop — yield while waiting for key input
bool running = true;
while (running) {
while (!zenith::is_key_available()) {
zenith::yield();
}
Zenith::KeyEvent ev;
zenith::getkey(&ev);
if (!ev.pressed) continue;
int contentRows = rows - 1;
switch (ev.ascii) {
case 'q':
running = false;
break;
case 'j':
if (scroll < maxScroll) scroll++;
break;
case 'k':
if (scroll > 0) scroll--;
break;
case ' ':
scroll += contentRows;
if (scroll > maxScroll) scroll = maxScroll;
break;
case 'b':
scroll -= contentRows;
if (scroll < 0) scroll = 0;
break;
case 'g':
scroll = 0;
break;
case 'G':
scroll = maxScroll;
break;
default:
// Handle scancodes for special keys
switch (ev.scancode) {
case 0x48: // Up arrow
if (scroll > 0) scroll--;
break;
case 0x50: // Down arrow
if (scroll < maxScroll) scroll++;
break;
case 0x49: // Page Up
scroll -= contentRows;
if (scroll < 0) scroll = 0;
break;
case 0x51: // Page Down
scroll += contentRows;
if (scroll > maxScroll) scroll = maxScroll;
break;
case 0x47: // Home
scroll = 0;
break;
case 0x4F: // End
scroll = maxScroll;
break;
}
break;
}
if (running) {
man_render(lines, totalLines, scroll, rows, cols, topic, foundSection);
}
}
// Restore screen
zenith::print("\033[?25h");
zenith::print("\033[?1049l");
zenith::mfree(lines);
zenith::mfree(fileData);
}
+60 -6
View File
@@ -44,15 +44,17 @@ static void print_int(uint64_t n) {
} }
static void prompt() { static void prompt() {
zenith::print("zenith> "); zenith::print("% ");
} }
static void cmd_help() { static void cmd_help() {
zenith::print("Available commands:\n"); zenith::print("Available commands:\n");
zenith::print(" help Show this help message\n"); zenith::print(" help Show this help message\n");
zenith::print(" info Show system information\n"); zenith::print(" info Show system information\n");
zenith::print(" man <topic> View manual pages\n");
zenith::print(" ls List ramdisk files\n"); zenith::print(" ls List ramdisk files\n");
zenith::print(" cat <file> Display file contents\n"); zenith::print(" cat <file> Display file contents\n");
zenith::print(" run <file> Spawn a new process from an ELF file\n");
zenith::print(" ping <ip> Send ICMP echo requests\n"); zenith::print(" ping <ip> Send ICMP echo requests\n");
zenith::print(" uptime Show uptime in milliseconds\n"); zenith::print(" uptime Show uptime in milliseconds\n");
zenith::print(" clear Clear the screen\n"); zenith::print(" clear Clear the screen\n");
@@ -223,11 +225,52 @@ static void cmd_ping(const char* arg) {
} }
} }
static void cmd_clear() { static void cmd_run(const char* arg) {
// Print enough newlines to scroll past visible content arg = skip_spaces(arg);
for (int i = 0; i < 50; i++) { if (*arg == '\0') {
zenith::putchar('\n'); zenith::print("Usage: run <filename>\n");
return;
} }
// Build path "0:/<filename>"
char path[128];
const char* prefix = "0:/";
int i = 0;
while (prefix[i]) { path[i] = prefix[i]; i++; }
int j = 0;
while (arg[j] && i < 126) { path[i++] = arg[j++]; }
path[i] = '\0';
int pid = zenith::spawn(path);
if (pid < 0) {
zenith::print("Error: failed to spawn '");
zenith::print(arg);
zenith::print("'\n");
} else {
zenith::waitpid(pid);
}
}
static void cmd_man(const char* arg) {
arg = skip_spaces(arg);
if (*arg == '\0') {
zenith::print("Usage: man <topic>\n");
zenith::print(" man <section> <topic>\n");
zenith::print("Try: man intro\n");
return;
}
int pid = zenith::spawn("0:/man.elf", arg);
if (pid < 0) {
zenith::print("Error: failed to start man viewer\n");
} else {
zenith::waitpid(pid);
}
}
static void cmd_clear() {
zenith::print("\033[2J"); // Clear entire screen
zenith::print("\033[H"); // Move cursor to top-left
} }
static void process_command(const char* line) { static void process_command(const char* line) {
@@ -241,10 +284,18 @@ static void process_command(const char* line) {
cmd_info(); cmd_info();
} else if (streq(line, "ls")) { } else if (streq(line, "ls")) {
cmd_ls(); cmd_ls();
} else if (starts_with(line, "man ")) {
cmd_man(line + 4);
} else if (streq(line, "man")) {
cmd_man("");
} else if (starts_with(line, "cat ")) { } else if (starts_with(line, "cat ")) {
cmd_cat(line + 4); cmd_cat(line + 4);
} else if (streq(line, "cat")) { } else if (streq(line, "cat")) {
cmd_cat(""); cmd_cat("");
} else if (starts_with(line, "run ")) {
cmd_run(line + 4);
} else if (streq(line, "run")) {
cmd_run("");
} else if (starts_with(line, "ping ")) { } else if (starts_with(line, "ping ")) {
cmd_ping(line + 5); cmd_ping(line + 5);
} else if (streq(line, "ping")) { } else if (streq(line, "ping")) {
@@ -265,7 +316,10 @@ static void process_command(const char* line) {
extern "C" void _start() { extern "C" void _start() {
zenith::print("\n"); zenith::print("\n");
zenith::print(" ZenithOS Shell v0.1\n"); zenith::print(" ZenithOS\n");
zenith::print(" Copyright (c) 2025-2026 Daniel Hammer\n");
zenith::print("\n");
zenith::print(" Type 'help' for available commands.\n"); zenith::print(" Type 'help' for available commands.\n");
zenith::print("\n"); zenith::print("\n");
BIN
View File
Binary file not shown.