diff --git a/.gitignore b/.gitignore index 60d33ff..7e88317 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ qemu.log toolchain/local/ toolchain/build/ toolchain/src/ +programs/bin/ +programs/obj/ +programs/src/doom/obj/ \ No newline at end of file diff --git a/docs/userspace.html b/docs/userspace.html new file mode 100644 index 0000000..3fe51f6 --- /dev/null +++ b/docs/userspace.html @@ -0,0 +1,1062 @@ + + + + + +ZenithOS — Userspace Application Development + + + + + + + + + + + + +
+ + +

Userspace Developer's Handbook

+

+ 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. +

+

+ This document covers everything you need to write, build, and run + userspace applications — from a minimal "Hello World" to using + every available syscall — as well as how to extend the kernel + with new syscalls. +

+ + +

Getting Started

+ +

Project Structure

+
programs/
+├── GNUmakefile          # Build system (C++ programs)
+├── link.ld              # Linker script (base address 0x400000)
+├── include/
+│   ├── Api/
+│   │   └── Syscall.hpp      # Syscall numbers & data structures
+│   ├── zenith/
+│   │   ├── syscall.h        # Inline asm wrappers & typed API
+│   │   └── heap.h           # Userspace heap allocator (malloc/mfree/realloc)
+│   └── libc/                # Minimal C standard library headers
+│       ├── stdio.h          # printf, FILE I/O
+│       ├── stdlib.h         # malloc, free, atoi, exit
+│       ├── string.h         # memcpy, strlen, strcmp, etc.
+│       └── ...              # ctype.h, errno.h, assert.h, etc.
+├── src/
+│   ├── hello/
+│   │   └── main.cpp         # Hello world example
+│   ├── shell/
+│   │   └── main.cpp         # Interactive shell
+│   ├── man/
+│   │   └── main.cpp         # Manual page viewer
+│   └── doom/
+│       ├── Makefile             # DOOM build system
+│       ├── doomgeneric_zenith.c # ZenithOS platform layer
+│       └── libc.c               # C library implementation
+├── bin/                     # Compiled .elf binaries
+└── obj/                     # Intermediate object files
+

+ Each subdirectory under src/ is treated as a separate + program. The build system discovers them automatically. +

+ +

Toolchain

+

+ Programs are compiled with a freestanding x86_64-elf + cross-compiler. The build system looks for one in + toolchain/local/bin/; if not found, it falls back to the + host g++. +

+

Key compiler flags:

+ +
+ SSE: The default C++ build disables SSE. Programs that need + floating-point support (e.g. C programs using float/double) + should compile with -msse -msse2 instead. The kernel enables SSE in + CR0/CR4 at boot, so SSE instructions are safe in userspace. +
+ +

Build System

+

To build all programs:

+
cd programs/
+make          # or: make -j$(nproc)
+

+ This produces one ELF binary per program in bin/ + (e.g. bin/hello.elf, bin/shell.elf). + The binaries are then typically packed into the ramdisk + (ramdisk.tar) and loaded by the kernel at boot. +

+

To add a new program, create src/<name>/main.cpp and run make.

+ +

Linker Script

+

programs/link.ld

+

+ All userspace programs are linked at virtual address + 0x400000. The linker script defines four standard + sections: +

+ + + + + + +
SectionContentsAlignment
.textExecutable code— (base)
.rodataRead-only data, string literals4 KiB
.dataInitialized read/write data4 KiB
.bssZero-initialized data
+

+ Debug frames (.eh_frame), notes, and comments are + discarded to keep binaries small. +

+ + +

Program Anatomy

+ +

Entry Point: _start

+

+ Because there is no C runtime, every program must define + extern "C" void _start() as its entry point. There is no + main(), no argc/argv, and no + atexit handlers. +

+
+ Auto-exit: If _start() returns normally, + the kernel's exit stub (mapped at 0x3FF000) automatically + calls SYS_EXIT(0). You can also call + zenith::exit(code) explicitly at any point. +
+ +

Hello World

+

programs/src/hello/main.cpp

+
// Minimal ZenithOS userspace program
+#include <zenith/syscall.h>
+
+extern "C" void _start() {
+    zenith::print("Hello from userspace!\n");
+}
+

+ Include <zenith/syscall.h> for the full typed API. + That header pulls in <Api/Syscall.hpp> for + constants and data structures. +

+ + +

Syscall Architecture

+ +

Calling Convention

+

+ ZenithOS uses the hardware SYSCALL instruction on + x86-64. The kernel sets up the required MSRs + (IA32_STAR, IA32_LSTAR, + IA32_FMASK) during boot. +

+ + + + + + + + + +
RegisterPurpose
RAXSyscall number (in) / return value (out)
RDIArgument 1
RSIArgument 2
RDXArgument 3
R10Argument 4 (not RCX — SYSCALL clobbers it)
R8Argument 5
R9Argument 6
+

+ The SYSCALL instruction saves RIP in + RCX and RFLAGS in R11; both + registers are clobbered. The kernel masks IF on entry + (via IA32_FMASK) so interrupts are disabled during the + transition. +

+

+ On the kernel side, SyscallEntry (assembly) saves + callee-saved registers and all arguments into a + SyscallFrame, then calls + SyscallDispatch(SyscallFrame*) which dispatches by + syscall number. +

+ +

Raw Syscall Wrappers

+

programs/include/zenith/syscall.h

+

+ Seven inline functions cover 0–6 argument syscalls: +

+
int64_t syscall0(uint64_t nr);
+int64_t syscall1(uint64_t nr, uint64_t a1);
+int64_t syscall2(uint64_t nr, uint64_t a1, uint64_t a2);
+int64_t syscall3(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3);
+int64_t syscall4(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4);
+int64_t syscall5(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5);
+int64_t syscall6(uint64_t nr, uint64_t a1, ..., uint64_t a6);
+

+ Each wrapper uses inline assembly with explicit register moves and a + full clobber list to ensure correctness. You can use these directly if + you need a syscall not yet covered by the typed API. +

+ +

Typed API Wrappers

+

+ The zenith:: namespace provides type-safe wrappers around + the raw syscalls. These are the recommended interface for application + code. The full reference follows below. +

+ + +

Syscall Reference

+

+ ZenithOS v0.1.0 exposes 26 syscalls (numbers + 0–25), organized into 10 categories. +

+ + +

Process Process Management

+ +

SYS_EXIT (0) — Terminate the current process

+
[[noreturn]] void zenith::exit(int code = 0);
+

+ Terminates the calling process. The exit code is currently unused by + the kernel but reserved for future wait/status support. + Control never returns to the caller. +

+ +

SYS_YIELD (1) — Yield the CPU

+
void zenith::yield();
+

+ Voluntarily yields the remainder of the current time slice, allowing + the scheduler to run another ready process immediately. +

+ +

SYS_SLEEP_MS (2) — Sleep for a duration

+
void zenith::sleep_ms(uint64_t ms);
+

+ Suspends the calling process for at least ms + milliseconds. Resolution depends on the APIC timer tick rate. +

+ +

SYS_GETPID (3) — Get process ID

+
int zenith::getpid();
+

+ Returns the PID of the calling process. Returns -1 if + called from the idle context (should not happen in userspace). +

+ +

SYS_SPAWN (20) — Spawn a new process

+
int zenith::spawn(const char* path, const char* args = nullptr);
+

+ Loads the ELF binary at path (a VFS path like + "0:/hello.elf") and spawns it as a new process. + Returns the new process's PID on success, or -1 on + failure (no free slots, invalid ELF, file not found). + The optional args string (up to 255 characters) is + copied into the new process and can be retrieved with + SYS_GETARGS. +

+ +

SYS_WAITPID (23) — Wait for a process to exit

+
void zenith::waitpid(int pid);
+

+ Blocks the calling process until the process identified by + pid has exited. Internally yields the CPU in a loop + until the target process is no longer alive. This is used by the + shell to wait for foreground processes (e.g. run + command) so that keyboard input and the terminal are not shared + simultaneously. +

+ +

SYS_GETARGS (25) — Get process arguments

+
int zenith::getargs(char* buf, uint64_t maxLen);
+

+ Copies the argument string passed to the current process (via + spawn()) into buf, writing at most + maxLen - 1 characters plus a null terminator. + Returns the number of characters copied, or -1 on error. + If no arguments were provided at spawn time, the buffer will be empty. +

+ + +

Console Console I/O

+ +

SYS_PRINT (4) — Print a string

+
void zenith::print(const char* text);
+

+ Writes a null-terminated string to the kernel terminal. Supports + newlines (\n) and standard printable ASCII. +

+ +

SYS_PUTCHAR (5) — Print a single character

+
void zenith::putchar(char c);
+

+ Writes a single character to the kernel terminal. Useful for building + output character by character (e.g. printing integers). +

+ + +

File I/O File I/O

+ +

SYS_OPEN (6) — Open a file

+
int zenith::open(const char* path);
+

+ Opens a file on the VFS. Paths use the format + "<device>:/<name>" (e.g. "0:/hello.elf" + for the ramdisk). Returns a non-negative handle on success, or a + negative value on error. +

+ +

SYS_READ (7) — Read from a file

+
int zenith::read(int handle, uint8_t* buf, uint64_t offset, uint64_t size);
+

+ Reads up to size bytes from the file at the given byte + offset into buf. Returns the number of bytes + actually read, or a negative value on error. Does not maintain an + implicit file position — the offset is explicit on every call. +

+ +

SYS_GETSIZE (8) — Get file size

+
uint64_t zenith::getsize(int handle);
+

Returns the total size (in bytes) of the file associated with handle.

+ +

SYS_CLOSE (9) — Close a file

+
void zenith::close(int handle);
+

Closes the file handle and releases associated kernel resources.

+ +

SYS_READDIR (10) — List directory entries

+
int zenith::readdir(const char* path, const char** names, int max);
+

+ Reads up to max directory entries from path. + Entry name pointers are written into the names array. + The kernel allocates a user-accessible page for the string data + automatically. Returns the number of entries read, or + ≤ 0 on error/empty directory. Maximum 64 entries per call. +

+ + +

Memory Memory Management

+ +

+ Memory allocation has two layers: low-level syscalls + that map pages from the kernel, and a userspace heap + that provides malloc/mfree on top. +

+ +

Userspace Heap (recommended)

+

programs/include/zenith/heap.h

+

+ Include <zenith/heap.h> for a proper free-list + allocator that runs entirely in userspace. It calls + SYS_ALLOC internally to obtain pages and manages + sub-page allocations with a linked free list — adapted from + the kernel's own HeapAllocator. +

+ +
void* zenith::malloc(uint64_t size);
+

+ Allocates size bytes from the userspace free list. + Returns a 16-byte-aligned pointer, or nullptr on failure. + When the free list is exhausted, it transparently requests more pages + from the kernel via SYS_ALLOC (minimum 16 KiB growth). +

+ +
void zenith::mfree(void* ptr);
+

+ Returns the block to the userspace free list. + No syscall is made — the memory stays mapped + and is immediately available for future malloc calls. + Passing nullptr is a safe no-op. +

+ +
void* zenith::realloc(void* ptr, uint64_t size);
+

+ Resizes the allocation at ptr 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. +

+ +

Low-Level Page Syscalls

+ +

SYS_ALLOC (11) — Map pages

+
void* zenith::alloc(uint64_t size);
+

+ Maps size bytes of zeroed physical pages into the + process's address space (starting at 0x40000000). + The size is rounded up to the nearest page boundary (4 KiB). + Returns a pointer to the mapped region, or nullptr on + failure. This is the backing primitive for + zenith::malloc — most programs should use the heap + API instead of calling this directly. +

+ +

SYS_FREE (12) — Unmap pages (no-op)

+
void zenith::free(void* ptr);
+

+ Reserved for future page-level unmapping. Currently a no-op — + pages are reclaimed when the process exits. Use + zenith::mfree for heap allocations. +

+ + +

Time Timekeeping

+ +

SYS_GETTICKS (13) — Get tick count

+
uint64_t zenith::get_ticks();
+

+ Returns the number of APIC timer ticks since boot. The tick rate + depends on the hardware and APIC timer calibration. +

+ +

SYS_GETMILLISECONDS (14) — Get milliseconds since boot

+
uint64_t zenith::get_milliseconds();
+

+ Returns wall-clock milliseconds elapsed since boot. Useful for + calculating uptime or measuring durations. +

+ + +

System System Information

+ +

SYS_GETINFO (15) — Get OS information

+
void zenith::get_info(Zenith::SysInfo* info);
+

+ Fills in a SysInfo structure + with the OS name, version string, API version number, and maximum + process count. +

+ + +

Keyboard Keyboard Input

+ +

SYS_ISKEYAVAILABLE (16) — Check for pending key

+
bool zenith::is_key_available();
+

+ Returns true if a key event is available in the PS/2 + keyboard buffer. Non-blocking. +

+ +

SYS_GETKEY (17) — Get a key event

+
void zenith::getkey(Zenith::KeyEvent* out);
+

+ Fills in a KeyEvent + structure with the next keyboard event (press or release), including + scancode, ASCII translation, and modifier state (Shift, Ctrl, Alt). +

+ +

SYS_GETCHAR (18) — Read a character (blocking)

+
char zenith::getchar();
+

+ Blocks until a printable character key-press is available, then + returns the ASCII character. This is the simplest way to read + interactive text input. +

+ + +

Network Networking

+ +

SYS_PING (19) — Send an ICMP echo request

+
int32_t zenith::ping(uint32_t ip, uint32_t timeoutMs = 3000);
+

+ Sends an ICMP echo request to ip and waits up to + timeoutMs milliseconds for a reply. The IP address is in + little-endian byte order (e.g. 10.0.2.2 → + 0x0202000A). + Returns the round-trip time in milliseconds on success, or + -1 on timeout. +

+ + +

Framebuffer Framebuffer Access

+ +

SYS_FBINFO (21) — Get framebuffer information

+
void zenith::fb_info(Zenith::FbInfo* info);
+

+ Fills in a FbInfo structure + with the framebuffer dimensions, pitch (bytes per scanline), and + bits per pixel. Call this before SYS_FBMAP to learn the + framebuffer geometry. +

+ +

SYS_FBMAP (22) — Map framebuffer into process memory

+
void* zenith::fb_map();
+

+ Maps the physical framebuffer into the calling process's address + space at 0x50000000 and returns the user virtual address. + The mapped region covers height × pitch bytes. Each + pixel is a 32-bit value in 0xAARRGGBB format (blue in + the low byte). Writing to this memory directly updates the screen. +

+
+ Note: After mapping, the cursor overlay is not composited + automatically. Programs that use the framebuffer take full control of + screen output for the mapped region. +
+ + +

Terminal Terminal

+ +

SYS_TERMSIZE (24) — Get terminal dimensions

+
void zenith::termsize(int* cols, int* rows);
+

+ Returns the current terminal dimensions (character grid) via the + two output pointers. Columns are packed in the low 32 bits and + rows in the high 32 bits of the raw return value; the typed + wrapper unpacks them for you. Either pointer may be + nullptr if you only need one dimension. +

+ + +

Data Structures

+ +

Zenith::SysInfo

+

programs/include/Api/Syscall.hpp

+
struct SysInfo {
+    char     osName[32];       // e.g. "ZenithOS"
+    char     osVersion[32];    // e.g. "0.1.0"
+    uint32_t apiVersion;       // Current: 2
+    uint32_t maxProcesses;     // Current: 16
+};
+ +

Zenith::FbInfo

+
struct FbInfo {
+    uint64_t width;       // Framebuffer width in pixels
+    uint64_t height;      // Framebuffer height in pixels
+    uint64_t pitch;       // Bytes per scanline
+    uint64_t bpp;         // Bits per pixel (always 32)
+    uint64_t userAddr;    // Reserved (0 until mapped via SYS_FBMAP)
+};
+ +

Zenith::KeyEvent

+
struct KeyEvent {
+    uint8_t scancode;   // Raw PS/2 scancode
+    char    ascii;      // Translated ASCII character (0 if non-printable)
+    bool    pressed;    // true = key down, false = key up
+    bool    shift;      // Shift modifier active
+    bool    ctrl;       // Ctrl modifier active
+    bool    alt;        // Alt modifier active
+};
+ +

Zenith::SyscallFrame (kernel only)

+
struct SyscallFrame {
+    uint64_t r15, r14, r13, r12, rbp, rbx;   // callee-saved
+    uint64_t arg6, arg5, arg4, arg3, arg2, arg1;
+    uint64_t syscall_nr;
+    uint64_t user_rflags, user_rip, user_rsp;
+};
+

+ This is the stack frame pushed by SyscallEntry.asm and + passed to SyscallDispatch. Userspace code never sees this + directly. +

+ + +

Shell Application Walkthrough

+

programs/src/shell/main.cpp

+

+ The built-in shell is the best example of a real ZenithOS application. + It demonstrates most of the available syscalls. +

+ +

Initialization

+
extern "C" void _start() {
+    zenith::print("\n  ZenithOS Shell v0.1\n");
+    zenith::print("  Type 'help' for available commands.\n\n");
+
+    char line[256];
+    int pos = 0;
+    prompt();
+
+    while (true) {
+        char c = zenith::getchar();       // blocking read
+        // ... handle input, echo, backspace ...
+    }
+}
+

+ The shell uses zenith::getchar() in a loop for blocking + character-by-character input, manually handling echo and backspace. +

+ +

Shell Commands & Syscalls Used

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescriptionSyscalls Used
helpPrint available commandsSYS_PRINT
infoShow OS name, version, API versionSYS_GETINFO, SYS_PRINT, SYS_PUTCHAR
man <topic>Fullscreen manual page viewerSYS_OPEN, SYS_GETSIZE, SYS_READ, SYS_CLOSE, SYS_ALLOC, SYS_TERMSIZE, SYS_GETKEY, SYS_PRINT, SYS_PUTCHAR
lsList ramdisk filesSYS_READDIR, SYS_PRINT
cat <file>Display file contents in 512-byte chunksSYS_OPEN, SYS_GETSIZE, SYS_READ, SYS_CLOSE, SYS_PRINT
run <file>Spawn a new process and wait for it to exitSYS_SPAWN, SYS_WAITPID, SYS_PRINT
ping <ip>Send 4 ICMP echo requestsSYS_PING, SYS_SLEEP_MS, SYS_PRINT, SYS_PUTCHAR
uptimeShow uptime in minutes, seconds, msSYS_GETMILLISECONDS, SYS_PRINT, SYS_PUTCHAR
clearScroll past visible contentSYS_PUTCHAR
exitTerminate the shellSYS_PRINT, SYS_EXIT
+ +

Pattern: Reading a File

+

The shell's cat implementation shows the standard file-reading pattern:

+
// 1. Build VFS path
+char path[128];
+// ... copy "0:/" + filename into path ...
+
+// 2. Open
+int handle = zenith::open(path);
+if (handle < 0) { /* error */ return; }
+
+// 3. Get size
+uint64_t size = zenith::getsize(handle);
+
+// 4. Read in chunks
+uint8_t buf[512];
+uint64_t offset = 0;
+while (offset < size) {
+    uint64_t chunk = size - offset;
+    if (chunk > sizeof(buf) - 1) chunk = sizeof(buf) - 1;
+    int bytesRead = zenith::read(handle, buf, offset, chunk);
+    if (bytesRead <= 0) break;
+    buf[bytesRead] = '\0';
+    zenith::print((const char*)buf);
+    offset += bytesRead;
+}
+
+// 5. Close
+zenith::close(handle);
+ +

Pattern: IP Networking

+
// Parse "10.0.2.2" into uint32_t in little-endian order
+uint32_t ip;
+parse_ip("10.0.2.2", &ip);  // ip = 0x0202000A
+
+// Send 4 pings with 1-second intervals
+for (int i = 0; i < 4; i++) {
+    int32_t rtt = zenith::ping(ip, 3000);
+    if (rtt < 0) { /* timeout */ }
+    else { /* reply in rtt ms */ }
+    if (i < 3) zenith::sleep_ms(1000);
+}
+ + +

Adding New Syscalls

+

+ Adding a syscall requires changes in 3 files + (kernel-side) and 2 files (userspace-side). Follow + these steps: +

+ +

Step 1: Assign a Syscall Number

+

+ Add a new constant in both copies of the syscall + number definitions. The numbers must match exactly. +

+

kernel/src/Api/Syscall.hpp

+

programs/include/Api/Syscall.hpp

+
static constexpr uint64_t SYS_MYFUNC = 26;  // next available number
+
+ Keep both files in sync. The kernel and userspace + headers define syscall numbers independently. If they disagree, the + wrong handler runs. +
+ +

Step 2: Implement the Kernel Handler

+

kernel/src/Api/Syscall.cpp

+

Add a static function implementing your syscall's logic:

+
static int64_t Sys_MyFunc(uint64_t arg1, const char* arg2) {
+    // Your kernel-side implementation here.
+    // You have full access to kernel subsystems.
+    return 0;
+}
+ +

Step 3: Add the Dispatch Case

+

kernel/src/Api/Syscall.cpp — SyscallDispatch()

+
case SYS_MYFUNC:
+    return (int64_t)Sys_MyFunc(frame->arg1, (const char*)frame->arg2);
+

+ Arguments are accessed through frame->arg1 through + frame->arg6, corresponding to RDI, RSI, RDX, R10, R8, + R9 respectively. +

+ +

Step 4: Add a Typed Userspace Wrapper

+

programs/include/zenith/syscall.h

+
inline int64_t my_func(uint64_t arg1, const char* arg2) {
+    return syscall2(Zenith::SYS_MYFUNC, arg1, (uint64_t)arg2);
+}
+

+ Choose the appropriate syscallN variant based on the + number of arguments. Cast pointer types to uint64_t. +

+ +

Step 5: Update the Log Message (optional)

+

kernel/src/Api/Syscall.cpp — InitializeSyscalls()

+

+ Update the boot log to reflect the new syscall count: +

+
// Change "26 syscalls" to "27 syscalls"
+Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
+    << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 27 syscalls)";
+ +

Complete Checklist

+ + + + + + + +
#FileChange
1kernel/src/Api/Syscall.hppAdd SYS_MYFUNC constant (+ any new structs)
2kernel/src/Api/Syscall.cppAdd Sys_MyFunc() implementation + dispatch case
3programs/include/Api/Syscall.hppAdd matching SYS_MYFUNC constant (+ any new structs)
4programs/include/zenith/syscall.hAdd typed wrapper in zenith:: namespace
5(optional)Update syscall count in boot log
+ + +

Process Memory Model

+

+ Each process gets its own PML4 page table. The kernel half + (upper 256 entries) is shared; the lower half is per-process. +

+ + + + + + + +
RegionVirtual AddressSizePurpose
Exit stub0x3FF0004 KiBAuto-exit trampoline (calls SYS_EXIT(0) if _start returns)
Program code0x400000+VariesELF .text, .rodata, .data, .bss
User heap0x40000000+Grows upPage pool (SYS_ALLOC); managed by userspace free-list heap
Framebuffer0x50000000+height × pitchMapped by SYS_FBMAP; direct pixel access (32-bit ARGB)
User stack0x7FFFFEF0000x7FFFFFF00016 KiB (4 pages)Grows down from 0x7FFFFFF000
+

+ The ELF loader maps PT_LOAD segments with user-accessible page flags. + BSS is zero-initialized automatically (pages are allocated zeroed). +

+ + +

Current Limitations

+ + +
+

+ ZenithOS Documentation — Copyright © 2025-2026 Daniel Hammer +

+ +
+ + diff --git a/doomgeneric b/doomgeneric new file mode 160000 index 0000000..fc60163 --- /dev/null +++ b/doomgeneric @@ -0,0 +1 @@ +Subproject commit fc601639494e089702a1ada082eb51aaafc03722 diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index a6522b6..e72d142 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include "../Libraries/flanterm/src/flanterm.h" // Assembly entry point extern "C" void SyscallEntry(); @@ -175,6 +177,47 @@ namespace Zenith { static uint16_t g_pingSeq = 0; 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) { uint16_t seq = g_pingSeq++; @@ -192,6 +235,27 @@ namespace Zenith { 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 ---- extern "C" int64_t SyscallDispatch(SyscallFrame* frame) { @@ -248,6 +312,20 @@ namespace Zenith { return (int64_t)Sys_GetChar(); case SYS_PING: 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: return -1; } @@ -274,7 +352,7 @@ namespace Zenith { Hal::WriteMSR(Hal::IA32_FMASK, 0x200); 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)"; } } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 59c4c96..43447a3 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -31,6 +31,20 @@ namespace Zenith { static constexpr uint64_t SYS_GETKEY = 17; static constexpr uint64_t SYS_GETCHAR = 18; 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 { char osName[32]; diff --git a/kernel/src/Drivers/PS2/Keyboard.cpp b/kernel/src/Drivers/PS2/Keyboard.cpp index e597d3a..4a4b061 100644 --- a/kernel/src/Drivers/PS2/Keyboard.cpp +++ b/kernel/src/Drivers/PS2/Keyboard.cpp @@ -176,39 +176,66 @@ namespace Drivers::PS2::Keyboard { return; } - // Handle modifier keys + // Handle modifier keys (update state, but still push event to buffer) + bool isModifier = false; switch (keycode) { case ScLeftShift: g_Modifiers.LeftShift = !released; - return; + isModifier = true; + break; case ScRightShift: g_Modifiers.RightShift = !released; - return; + isModifier = true; + break; case ScLeftCtrl: g_Modifiers.LeftCtrl = !released; - return; + isModifier = true; + break; case ScLeftAlt: g_Modifiers.LeftAlt = !released; - return; + isModifier = true; + break; case ScCapsLock: if (!released) { g_Modifiers.CapsLock = !g_Modifiers.CapsLock; } - return; + isModifier = true; + break; case ScNumLock: if (!released) { g_Modifiers.NumLock = !g_Modifiers.NumLock; } - return; + isModifier = true; + break; case ScScrollLock: if (!released) { g_Modifiers.ScrollLock = !g_Modifiers.ScrollLock; } - return; + isModifier = true; + break; default: 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 char ascii = 0; if (keycode < 128) { diff --git a/kernel/src/Graphics/Cursor.cpp b/kernel/src/Graphics/Cursor.cpp index fd560b7..296fd40 100644 --- a/kernel/src/Graphics/Cursor.cpp +++ b/kernel/src/Graphics/Cursor.cpp @@ -162,4 +162,9 @@ namespace Graphics::Cursor { 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; } + }; diff --git a/kernel/src/Graphics/Cursor.hpp b/kernel/src/Graphics/Cursor.hpp index 86d26f1..aa2b91c 100644 --- a/kernel/src/Graphics/Cursor.hpp +++ b/kernel/src/Graphics/Cursor.hpp @@ -13,4 +13,9 @@ namespace Graphics::Cursor { void Initialize(limine_framebuffer* framebuffer); void Update(); + uint32_t* GetFramebufferBase(); + uint64_t GetFramebufferWidth(); + uint64_t GetFramebufferHeight(); + uint64_t GetFramebufferPitch(); + }; diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index e56ac4f..7fcd286 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -93,6 +93,23 @@ extern "C" void kmain() { #if defined (__x86_64__) Hal::PrepareGDT(); 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 uint64_t hhdm_offset = hhdm_request.response->offset; diff --git a/kernel/src/Memory/PageFrameAllocator.cpp b/kernel/src/Memory/PageFrameAllocator.cpp index 27d41df..316833b 100644 --- a/kernel/src/Memory/PageFrameAllocator.cpp +++ b/kernel/src/Memory/PageFrameAllocator.cpp @@ -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) { - memcpy(first, ptr, n); + memcpy(base, ptr, (uint64_t)n * 0x1000); Free(ptr); } - return first; + return base; } void PageFrameAllocator::Free(void* ptr) { diff --git a/kernel/src/Sched/ElfLoader.cpp b/kernel/src/Sched/ElfLoader.cpp index 97521fb..d2e4d84 100644 --- a/kernel/src/Sched/ElfLoader.cpp +++ b/kernel/src/Sched/ElfLoader.cpp @@ -52,8 +52,6 @@ namespace Sched { } uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys) { - Kt::KernelLogStream(Kt::INFO, "ELF") << "Loading " << vfsPath; - int handle = Fs::Vfs::VfsOpen(vfsPath); if (handle < 0) { Kt::KernelLogStream(Kt::ERROR, "ELF") << "Failed to open " << vfsPath; @@ -78,6 +76,10 @@ namespace Sched { Fs::Vfs::VfsRead(handle, fileData, 0, fileSize); 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 Elf64Header* hdr = (Elf64Header*)fileData; if (!ValidateElfHeader(hdr)) { @@ -85,9 +87,6 @@ namespace Sched { 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 for (uint16_t i = 0; i < hdr->e_phnum; i++) { Elf64ProgramHeader* phdr = (Elf64ProgramHeader*)(fileData + hdr->e_phoff + i * hdr->e_phentsize); @@ -100,9 +99,6 @@ namespace Sched { 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 uint64_t segBase = phdr->p_vaddr & ~0xFFFULL; uint64_t segEnd = (phdr->p_vaddr + phdr->p_memsz + 0xFFF) & ~0xFFFULL; @@ -147,7 +143,6 @@ namespace Sched { uint64_t entryPoint = hdr->e_entry; Memory::g_heap->Free(fileData); - Kt::KernelLogStream(Kt::OK, "ELF") << "Loaded successfully, entry=" << kcp::hex << entryPoint << kcp::dec; return entryPoint; } diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 7aa0e93..6aeb8f9 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -75,6 +75,7 @@ namespace Sched { processTable[i].kernelStackTop = 0; processTable[i].userStackTop = 0; processTable[i].heapNext = 0; + processTable[i].args[0] = '\0'; } currentPid = -1; @@ -85,7 +86,7 @@ namespace Sched { << " process slots, " << (uint64_t)TimeSliceMs << " ms time slice)"; } - void Spawn(const char* vfsPath) { + int Spawn(const char* vfsPath, const char* args) { int slot = -1; for (int i = 0; i < MaxProcesses; i++) { if (processTable[i].state == ProcessState::Free) { @@ -96,7 +97,7 @@ namespace Sched { if (slot < 0) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "No free process slots"; - return; + return -1; } // Create per-process PML4 with kernel-half copied @@ -106,20 +107,20 @@ namespace Sched { uint64_t entry = ElfLoad(vfsPath, pml4Phys); if (entry == 0) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to load ELF: " << vfsPath; - return; + return -1; } // Allocate kernel stack (used during syscalls and interrupts) void* firstPage = Memory::g_pfa->AllocateZeroed(); if (firstPage == nullptr) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for kernel stack"; - return; + return -1; } void* stackMem = Memory::g_pfa->ReallocConsecutive(firstPage, StackPages); if (stackMem == nullptr) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to allocate contiguous kernel stack"; Memory::g_pfa->Free(firstPage); - return; + return -1; } uint8_t* kernelStackBase = (uint8_t*)stackMem; @@ -132,7 +133,7 @@ namespace Sched { void* page = Memory::g_pfa->AllocateZeroed(); if (page == nullptr) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for user stack"; - return; + return -1; } uint64_t physAddr = Memory::SubHHDM((uint64_t)page); Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, userStackBase + i * 0x1000); @@ -145,7 +146,7 @@ namespace Sched { void* stubPage = Memory::g_pfa->AllocateZeroed(); if (stubPage == nullptr) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for exit stub"; - return; + return -1; } uint64_t stubPhys = Memory::SubHHDM((uint64_t)stubPage); 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.heapNext = UserHeapBase; - Kt::KernelLogStream(Kt::OK, "Sched") << "Spawned process " << (uint64_t)proc.pid - << " (" << vfsPath << ") entry=" << kcp::hex << entry - << " kstack=" << (uint64_t)kernelStackBase << "-" << kernelStackTop - << " ustack=" << userStackBase << "-" << UserStackTop - << " pml4=" << pml4Phys << kcp::dec; + // Copy arguments string into process + proc.args[0] = '\0'; + if (args != nullptr) { + int i = 0; + for (; i < 255 && args[i]; i++) { + proc.args[i] = args[i]; + } + proc.args[i] = '\0'; + } + + return proc.pid; } void Schedule() { @@ -271,8 +278,6 @@ namespace Sched { return; } - Kt::KernelLogStream(Kt::OK, "Sched") << "Process " << (uint64_t)processTable[currentPid].pid << " terminated"; - processTable[currentPid].state = ProcessState::Terminated; 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; + } + } diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index 05cabbd..d617953 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -38,10 +38,11 @@ namespace Sched { uint64_t kernelStackTop; // Top of kernel stack (for TSS RSP0 / SYSCALL) uint64_t userStackTop; // User-space stack top uint64_t heapNext; // Simple bump allocator for user heap + char args[256]; // Command-line arguments (set by parent via Spawn) }; void Initialize(); - void Spawn(const char* vfsPath); + int Spawn(const char* vfsPath, const char* args = nullptr); void Schedule(); // Called from the APIC timer handler on every tick. @@ -56,4 +57,7 @@ namespace Sched { // Called by terminated processes to mark themselves done void ExitProcess(); + // Check if a process is still alive (Ready or Running) + bool IsAlive(int pid); + } diff --git a/kernel/src/Terminal/Terminal.hpp b/kernel/src/Terminal/Terminal.hpp index 83e1a21..829b444 100644 --- a/kernel/src/Terminal/Terminal.hpp +++ b/kernel/src/Terminal/Terminal.hpp @@ -165,4 +165,10 @@ namespace Kt }; }; -extern Kt::KernelErrorStream kerr; \ No newline at end of file +extern Kt::KernelErrorStream kerr; + +// Forward-declare flanterm context for syscall access +struct flanterm_context; +namespace Kt { + extern flanterm_context *ctx; +} \ No newline at end of file diff --git a/programs/GNUmakefile b/programs/GNUmakefile index f9edb1c..93a810a 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,9 +59,19 @@ PROGRAMS := $(notdir $(wildcard src/*)) # Build targets: one ELF per program. TARGETS := $(addprefix $(BINDIR)/,$(addsuffix .elf,$(PROGRAMS))) +# Man pages source directory. +MANDIR := man +MANSRC := $(wildcard $(MANDIR)/*.*) +MANDST := $(patsubst $(MANDIR)/%,$(BINDIR)/man/%,$(MANSRC)) + .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. # For now each program is a single .cpp file compiled and linked directly. diff --git a/programs/bin/hello.elf b/programs/bin/hello.elf index 844af02..2db5c72 100755 Binary files a/programs/bin/hello.elf and b/programs/bin/hello.elf differ diff --git a/programs/bin/shell.elf b/programs/bin/shell.elf index 6a5f982..faab846 100755 Binary files a/programs/bin/shell.elf and b/programs/bin/shell.elf differ diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index a66e7a9..7230c21 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -31,6 +31,20 @@ namespace Zenith { static constexpr uint64_t SYS_GETKEY = 17; static constexpr uint64_t SYS_GETCHAR = 18; 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 { char osName[32]; diff --git a/programs/include/libc/assert.h b/programs/include/libc/assert.h new file mode 100644 index 0000000..b1f703e --- /dev/null +++ b/programs/include/libc/assert.h @@ -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 */ diff --git a/programs/include/libc/ctype.h b/programs/include/libc/ctype.h new file mode 100644 index 0000000..efca6ed --- /dev/null +++ b/programs/include/libc/ctype.h @@ -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 */ diff --git a/programs/include/libc/errno.h b/programs/include/libc/errno.h new file mode 100644 index 0000000..cdbda4d --- /dev/null +++ b/programs/include/libc/errno.h @@ -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 */ diff --git a/programs/include/libc/fcntl.h b/programs/include/libc/fcntl.h new file mode 100644 index 0000000..46890eb --- /dev/null +++ b/programs/include/libc/fcntl.h @@ -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 */ diff --git a/programs/include/libc/inttypes.h b/programs/include/libc/inttypes.h new file mode 100644 index 0000000..398a5fc --- /dev/null +++ b/programs/include/libc/inttypes.h @@ -0,0 +1,27 @@ +#ifndef _LIBC_INTTYPES_H +#define _LIBC_INTTYPES_H + +#include + +#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 */ diff --git a/programs/include/libc/limits.h b/programs/include/libc/limits.h new file mode 100644 index 0000000..c9e9c6b --- /dev/null +++ b/programs/include/libc/limits.h @@ -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 */ diff --git a/programs/include/libc/math.h b/programs/include/libc/math.h new file mode 100644 index 0000000..b4522b2 --- /dev/null +++ b/programs/include/libc/math.h @@ -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 */ diff --git a/programs/include/libc/stdio.h b/programs/include/libc/stdio.h new file mode 100644 index 0000000..93bb94b --- /dev/null +++ b/programs/include/libc/stdio.h @@ -0,0 +1,77 @@ +#ifndef _LIBC_STDIO_H +#define _LIBC_STDIO_H + +#pragma once + +#include +#include + +#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 */ diff --git a/programs/include/libc/stdlib.h b/programs/include/libc/stdlib.h new file mode 100644 index 0000000..0ffaf2a --- /dev/null +++ b/programs/include/libc/stdlib.h @@ -0,0 +1,66 @@ +#ifndef _LIBC_STDLIB_H +#define _LIBC_STDLIB_H + +#pragma once + +#include + +#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 */ diff --git a/programs/include/libc/string.h b/programs/include/libc/string.h new file mode 100644 index 0000000..180247f --- /dev/null +++ b/programs/include/libc/string.h @@ -0,0 +1,35 @@ +#ifndef _LIBC_STRING_H +#define _LIBC_STRING_H + +#pragma once + +#include + +#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 */ diff --git a/programs/include/libc/strings.h b/programs/include/libc/strings.h new file mode 100644 index 0000000..fba3df7 --- /dev/null +++ b/programs/include/libc/strings.h @@ -0,0 +1,17 @@ +#ifndef _LIBC_STRINGS_H +#define _LIBC_STRINGS_H + +#include + +#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 */ diff --git a/programs/include/libc/sys/stat.h b/programs/include/libc/sys/stat.h new file mode 100644 index 0000000..e0f9f5b --- /dev/null +++ b/programs/include/libc/sys/stat.h @@ -0,0 +1,24 @@ +#ifndef _LIBC_SYS_STAT_H +#define _LIBC_SYS_STAT_H + +#pragma once + +#include + +#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 */ diff --git a/programs/include/libc/sys/time.h b/programs/include/libc/sys/time.h new file mode 100644 index 0000000..33de4ae --- /dev/null +++ b/programs/include/libc/sys/time.h @@ -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 */ diff --git a/programs/include/libc/sys/types.h b/programs/include/libc/sys/types.h new file mode 100644 index 0000000..350e0fb --- /dev/null +++ b/programs/include/libc/sys/types.h @@ -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 */ diff --git a/programs/include/libc/unistd.h b/programs/include/libc/unistd.h new file mode 100644 index 0000000..fe56976 --- /dev/null +++ b/programs/include/libc/unistd.h @@ -0,0 +1,20 @@ +#ifndef _LIBC_UNISTD_H +#define _LIBC_UNISTD_H + +#pragma once + +#include + +#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 */ diff --git a/programs/include/zenith/heap.h b/programs/include/zenith/heap.h new file mode 100644 index 0000000..cefd9a9 --- /dev/null +++ b/programs/include/zenith/heap.h @@ -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 + +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 diff --git a/programs/include/zenith/syscall.h b/programs/include/zenith/syscall.h index 1ba0705..51cd40c 100644 --- a/programs/include/zenith/syscall.h +++ b/programs/include/zenith/syscall.h @@ -117,6 +117,9 @@ namespace zenith { inline void yield() { syscall0(Zenith::SYS_YIELD); } inline void sleep_ms(uint64_t ms) { syscall1(Zenith::SYS_SLEEP_MS, ms); } 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 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); } + // 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); + } + } diff --git a/programs/man/file.2 b/programs/man/file.2 new file mode 100644 index 0000000..dc72f17 --- /dev/null +++ b/programs/man/file.2 @@ -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 ":/", 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) diff --git a/programs/man/framebuffer.2 b/programs/man/framebuffer.2 new file mode 100644 index 0000000..856dc46 --- /dev/null +++ b/programs/man/framebuffer.2 @@ -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) diff --git a/programs/man/intro.1 b/programs/man/intro.1 new file mode 100644 index 0000000..6c581cb --- /dev/null +++ b/programs/man/intro.1 @@ -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 for the full + typed syscall API. Include 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) diff --git a/programs/man/malloc.3 b/programs/man/malloc.3 new file mode 100644 index 0000000..46687c9 --- /dev/null +++ b/programs/man/malloc.3 @@ -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 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) diff --git a/programs/man/man.1 b/programs/man/man.1 new file mode 100644 index 0000000..ec65e0d --- /dev/null +++ b/programs/man/man.1 @@ -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/.
+ + 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) diff --git a/programs/man/shell.1 b/programs/man/shell.1 new file mode 100644 index 0000000..0207f8b --- /dev/null +++ b/programs/man/shell.1 @@ -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 + Open a manual page in the fullscreen pager. See man(1). + +.SS ls + List all files on the ramdisk (drive 0:/). + +.SS cat + Print the contents of a ramdisk file to the terminal. + Example: cat hello.elf + +.SS run + 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 + 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) diff --git a/programs/man/spawn.2 b/programs/man/spawn.2 new file mode 100644 index 0000000..2be6d18 --- /dev/null +++ b/programs/man/spawn.2 @@ -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) diff --git a/programs/man/syscalls.2 b/programs/man/syscalls.2 new file mode 100644 index 0000000..32085fe --- /dev/null +++ b/programs/man/syscalls.2 @@ -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 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) diff --git a/programs/obj/hello/main.o b/programs/obj/hello/main.o index 781ee69..bead10b 100644 Binary files a/programs/obj/hello/main.o and b/programs/obj/hello/main.o differ diff --git a/programs/obj/shell/main.o b/programs/obj/shell/main.o index 86b80f6..72939d8 100644 Binary files a/programs/obj/shell/main.o and b/programs/obj/shell/main.o differ diff --git a/programs/src/doom/Makefile b/programs/src/doom/Makefile new file mode 100644 index 0000000..c1485cd --- /dev/null +++ b/programs/src/doom/Makefile @@ -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) diff --git a/programs/src/doom/doomgeneric_zenith.c b/programs/src/doom/doomgeneric_zenith.c new file mode 100644 index 0000000..ee47db7 --- /dev/null +++ b/programs/src/doom/doomgeneric_zenith.c @@ -0,0 +1,230 @@ +/* + * doomgeneric_zenith.c + * DOOM platform implementation for ZenithOS + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "doomgeneric.h" +#include "doomkeys.h" + +#include +#include + +/* ---- 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(); + } +} diff --git a/programs/src/doom/libc.c b/programs/src/doom/libc.c new file mode 100644 index 0000000..274e04a --- /dev/null +++ b/programs/src/doom/libc.c @@ -0,0 +1,1158 @@ +/* + * libc.c + * Minimal C standard library for ZenithOS userspace (DOOM port) + * Copyright (c) 2025 Daniel Hammer +*/ + +#include +#include +#include +#include + +/* ======================================================================== + Raw syscall wrappers (C versions matching kernel ABI) + ======================================================================== */ + +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; +} + +static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) { + long ret; + __asm__ volatile( + "mov %[a1], %%rdi\n\t" + "mov %[a2], %%rsi\n\t" + "mov %[a3], %%rdx\n\t" + "mov %[a4], %%r10\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3), [a4] "r"(a4) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +/* Syscall numbers */ +#define SYS_EXIT 0 +#define SYS_PRINT 4 +#define SYS_PUTCHAR 5 +#define SYS_OPEN 6 +#define SYS_READ 7 +#define SYS_GETSIZE 8 +#define SYS_CLOSE 9 +#define SYS_ALLOC 11 +#define SYS_FREE 12 + +/* ======================================================================== + errno + ======================================================================== */ + +int errno = 0; + +/* ======================================================================== + string.h functions + ======================================================================== */ + +void *memcpy(void *dest, const void *src, size_t n) { + unsigned char *d = (unsigned char *)dest; + const unsigned char *s = (const unsigned char *)src; + for (size_t i = 0; i < n; i++) + d[i] = s[i]; + return dest; +} + +void *memset(void *s, int c, size_t n) { + unsigned char *p = (unsigned char *)s; + for (size_t i = 0; i < n; i++) + p[i] = (unsigned char)c; + return s; +} + +void *memmove(void *dest, const void *src, size_t n) { + unsigned char *d = (unsigned char *)dest; + const unsigned char *s = (const unsigned char *)src; + if (s < d && d < s + n) { + for (size_t i = n; i > 0; i--) + d[i - 1] = s[i - 1]; + } else { + for (size_t i = 0; i < n; i++) + d[i] = s[i]; + } + return dest; +} + +int memcmp(const void *s1, const void *s2, size_t n) { + const unsigned char *a = (const unsigned char *)s1; + const unsigned char *b = (const unsigned char *)s2; + for (size_t i = 0; i < n; i++) { + if (a[i] != b[i]) + return a[i] < b[i] ? -1 : 1; + } + return 0; +} + +size_t strlen(const char *s) { + size_t len = 0; + while (s[len]) len++; + return len; +} + +int strcmp(const char *a, const char *b) { + while (*a && *a == *b) { a++; b++; } + return (unsigned char)*a - (unsigned char)*b; +} + +int strncmp(const char *a, const char *b, size_t n) { + for (size_t i = 0; i < n; i++) { + if (a[i] != b[i] || a[i] == 0) + return (unsigned char)a[i] - (unsigned char)b[i]; + } + return 0; +} + +char *strcpy(char *dest, const char *src) { + char *d = dest; + while ((*d++ = *src++)); + return dest; +} + +char *strncpy(char *dest, const char *src, size_t n) { + size_t i; + for (i = 0; i < n && src[i]; i++) + dest[i] = src[i]; + for (; i < n; i++) + dest[i] = '\0'; + return dest; +} + +char *strcat(char *dest, const char *src) { + char *d = dest + strlen(dest); + while ((*d++ = *src++)); + return dest; +} + +char *strncat(char *dest, const char *src, size_t n) { + char *d = dest + strlen(dest); + size_t i; + for (i = 0; i < n && src[i]; i++) + d[i] = src[i]; + d[i] = '\0'; + return dest; +} + +char *strchr(const char *s, int c) { + while (*s) { + if (*s == (char)c) return (char *)s; + s++; + } + return (c == 0) ? (char *)s : NULL; +} + +char *strrchr(const char *s, int c) { + const char *last = NULL; + while (*s) { + if (*s == (char)c) last = s; + s++; + } + if (c == 0) return (char *)s; + return (char *)last; +} + +static int _tolower(int c) { + return (c >= 'A' && c <= 'Z') ? c + 32 : c; +} + +int strcasecmp(const char *a, const char *b) { + while (*a && _tolower((unsigned char)*a) == _tolower((unsigned char)*b)) { a++; b++; } + return _tolower((unsigned char)*a) - _tolower((unsigned char)*b); +} + +int strncasecmp(const char *a, const char *b, size_t n) { + for (size_t i = 0; i < n; i++) { + int ca = _tolower((unsigned char)a[i]); + int cb = _tolower((unsigned char)b[i]); + if (ca != cb || ca == 0) return ca - cb; + } + return 0; +} + +char *strstr(const char *haystack, const char *needle) { + if (!*needle) return (char *)haystack; + size_t nlen = strlen(needle); + while (*haystack) { + if (strncmp(haystack, needle, nlen) == 0) + return (char *)haystack; + haystack++; + } + return NULL; +} + +/* ======================================================================== + ctype.h functions + ======================================================================== */ + +int isalpha(int c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } +int isdigit(int c) { return c >= '0' && c <= '9'; } +int isalnum(int c) { return isalpha(c) || isdigit(c); } +int isspace(int c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; } +int isupper(int c) { return c >= 'A' && c <= 'Z'; } +int islower(int c) { return c >= 'a' && c <= 'z'; } +int isprint(int c) { return c >= 0x20 && c <= 0x7E; } +int ispunct(int c) { return isprint(c) && !isalnum(c) && c != ' '; } +int isxdigit(int c) { return isdigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } +int iscntrl(int c) { return (c >= 0 && c < 0x20) || c == 0x7F; } +int isgraph(int c) { return c > 0x20 && c <= 0x7E; } +int toupper(int c) { return (c >= 'a' && c <= 'z') ? c - 32 : c; } +int tolower(int c) { return (c >= 'A' && c <= 'Z') ? c + 32 : c; } + +/* ======================================================================== + Heap allocator (free-list, backed by SYS_ALLOC) + ======================================================================== */ + +#define HEAP_MAGIC 0x5A484541ULL /* "ZHEA" */ + +struct HeapHeader { + uint64_t magic; + uint64_t size; +} __attribute__((packed)); + +struct FreeNode { + uint64_t size; + struct FreeNode *next; +}; + +static struct FreeNode g_heapHead = { 0, NULL }; +static int g_heapInit = 0; + +static void heap_insert_free(void *ptr, uint64_t size) { + struct FreeNode *node = (struct FreeNode *)ptr; + node->size = size; + node->next = g_heapHead.next; + g_heapHead.next = node; +} + +static void heap_grow(uint64_t bytes) { + uint64_t pages = (bytes + 0xFFF) / 0x1000; + if (pages < 4) pages = 4; + void *mem = (void *)_zos_syscall1(SYS_ALLOC, (long)(pages * 0x1000)); + if (mem != NULL) + heap_insert_free(mem, pages * 0x1000); +} + +void *malloc(size_t size) { + if (!g_heapInit) { + heap_grow(16 * 0x1000); + g_heapInit = 1; + } + + uint64_t needed = size + sizeof(struct HeapHeader); + needed = (needed + 15) & ~15ULL; + + struct FreeNode *prev = &g_heapHead; + struct FreeNode *cur = g_heapHead.next; + + while (cur != NULL) { + if (cur->size >= needed) { + uint64_t blockSize = cur->size; + prev->next = cur->next; + + if (blockSize > needed + sizeof(struct FreeNode) + 16) { + void *rest = (void *)((uint8_t *)cur + needed); + heap_insert_free(rest, blockSize - needed); + } + + struct HeapHeader *hdr = (struct HeapHeader *)cur; + hdr->magic = HEAP_MAGIC; + hdr->size = size; + return (void *)((uint8_t *)hdr + sizeof(struct HeapHeader)); + } + prev = cur; + cur = cur->next; + } + + heap_grow(needed); + return malloc(size); +} + +void free(void *ptr) { + if (ptr == NULL) return; + + struct HeapHeader *hdr = (struct HeapHeader *)((uint8_t *)ptr - sizeof(struct HeapHeader)); + uint64_t blockSize = hdr->size + sizeof(struct HeapHeader); + blockSize = (blockSize + 15) & ~15ULL; + heap_insert_free((void *)hdr, blockSize); +} + +void *calloc(size_t nmemb, size_t size) { + size_t total = nmemb * size; + void *p = malloc(total); + if (p) memset(p, 0, total); + return p; +} + +void *realloc(void *ptr, size_t size) { + if (ptr == NULL) return malloc(size); + if (size == 0) { free(ptr); return NULL; } + + struct HeapHeader *hdr = (struct HeapHeader *)((uint8_t *)ptr - sizeof(struct HeapHeader)); + uint64_t old = hdr->size; + + void *newp = malloc(size); + if (newp == NULL) return NULL; + + size_t copySize = old < size ? old : size; + memcpy(newp, ptr, copySize); + free(ptr); + return newp; +} + +char *strdup(const char *s) { + size_t len = strlen(s) + 1; + char *d = (char *)malloc(len); + if (d) memcpy(d, s, len); + return d; +} + +/* ======================================================================== + stdlib.h functions + ======================================================================== */ + +int abs(int x) { return x < 0 ? -x : x; } +long labs(long x) { return x < 0 ? -x : x; } + +int atoi(const char *s) { + int neg = 0, val = 0; + while (isspace((unsigned char)*s)) s++; + if (*s == '-') { neg = 1; s++; } + else if (*s == '+') { s++; } + while (isdigit((unsigned char)*s)) { + val = val * 10 + (*s - '0'); + s++; + } + return neg ? -val : val; +} + +long atol(const char *s) { + long neg = 0, val = 0; + while (isspace((unsigned char)*s)) s++; + if (*s == '-') { neg = 1; s++; } + else if (*s == '+') { s++; } + while (isdigit((unsigned char)*s)) { + val = val * 10 + (*s - '0'); + s++; + } + return neg ? -val : val; +} + +long strtol(const char *nptr, char **endptr, int base) { + const char *s = nptr; + long val = 0; + int neg = 0; + + while (isspace((unsigned char)*s)) s++; + if (*s == '-') { neg = 1; s++; } + else if (*s == '+') { s++; } + + if (base == 0) { + if (*s == '0' && (s[1] == 'x' || s[1] == 'X')) { base = 16; s += 2; } + else if (*s == '0') { base = 8; s++; } + else base = 10; + } else if (base == 16 && *s == '0' && (s[1] == 'x' || s[1] == 'X')) { + s += 2; + } + + while (*s) { + int digit; + if (*s >= '0' && *s <= '9') digit = *s - '0'; + else if (*s >= 'a' && *s <= 'z') digit = *s - 'a' + 10; + else if (*s >= 'A' && *s <= 'Z') digit = *s - 'A' + 10; + else break; + if (digit >= base) break; + val = val * base + digit; + s++; + } + + if (endptr) *endptr = (char *)s; + return neg ? -val : val; +} + +unsigned long strtoul(const char *nptr, char **endptr, int base) { + return (unsigned long)strtol(nptr, endptr, base); +} + +void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)) { + /* Simple insertion sort — sufficient for doom's usage */ + char *arr = (char *)base; + char tmp[256]; /* doom's qsort elements are small */ + for (size_t i = 1; i < nmemb; i++) { + memcpy(tmp, arr + i * size, size); + size_t j = i; + while (j > 0 && compar(arr + (j - 1) * size, tmp) > 0) { + memcpy(arr + j * size, arr + (j - 1) * size, size); + j--; + } + memcpy(arr + j * size, tmp, size); + } +} + +static unsigned int _rand_seed = 1; + +int rand(void) { + _rand_seed = _rand_seed * 1103515245 + 12345; + return (int)((_rand_seed >> 16) & 0x7FFF); +} + +void srand(unsigned int seed) { + _rand_seed = seed; +} + +char *getenv(const char *name) { + (void)name; + return NULL; +} + +static void (*_atexit_funcs[32])(void); +static int _atexit_count = 0; + +int atexit(void (*func)(void)) { + if (_atexit_count >= 32) return -1; + _atexit_funcs[_atexit_count++] = func; + return 0; +} + +void exit(int status) { + for (int i = _atexit_count - 1; i >= 0; i--) + _atexit_funcs[i](); + _zos_syscall1(SYS_EXIT, (long)status); + __builtin_unreachable(); +} + +void abort(void) { + _zos_syscall1(SYS_PRINT, (long)"abort() called\n"); + _zos_syscall1(SYS_EXIT, 1); + __builtin_unreachable(); +} + +/* ======================================================================== + assert.h support + ======================================================================== */ + +void __assert_fail(const char *expr, const char *file, int line, const char *func) { + _zos_syscall1(SYS_PRINT, (long)"Assertion failed: "); + _zos_syscall1(SYS_PRINT, (long)expr); + _zos_syscall1(SYS_PRINT, (long)" at "); + _zos_syscall1(SYS_PRINT, (long)file); + _zos_syscall1(SYS_PRINT, (long)"\n"); + (void)line; (void)func; + abort(); +} + +/* ======================================================================== + printf family — vsnprintf core + ======================================================================== */ + +/* Internal: write a char to buffer if space remains */ +struct _pf_state { + char *buf; + size_t pos; + size_t max; +}; + +static void _pf_putc(struct _pf_state *st, char c) { + if (st->pos < st->max) + st->buf[st->pos] = c; + st->pos++; +} + +static void _pf_puts(struct _pf_state *st, const char *s) { + while (*s) _pf_putc(st, *s++); +} + +static void _pf_putnum(struct _pf_state *st, unsigned long val, int base, int upper, int width, char pad, int neg, int precision) { + char tmp[24]; + int i = 0; + const char *digits = upper ? "0123456789ABCDEF" : "0123456789abcdef"; + + if (val == 0) { + tmp[i++] = '0'; + } else { + while (val > 0) { + tmp[i++] = digits[val % base]; + val /= base; + } + } + + /* Precision: minimum number of digits (zero-pad the digits themselves) */ + int digitCount = i; + int precPad = 0; + if (precision > digitCount) + precPad = precision - digitCount; + + /* Total output length: sign + precPad + digits */ + int total = (neg ? 1 : 0) + precPad + digitCount; + + /* Width padding (if pad is '0' and no precision, treat as digit padding) */ + if (neg && pad == '0' && precision < 0) { + _pf_putc(st, '-'); + } + if (precision < 0 && pad == '0') { + /* Old behavior: '0' flag pads digits */ + for (int w = total; w < width; w++) + _pf_putc(st, '0'); + } else { + /* Space padding on the left */ + for (int w = total; w < width; w++) + _pf_putc(st, ' '); + } + if (neg && !(pad == '0' && precision < 0)) { + _pf_putc(st, '-'); + } + + /* Precision zero-padding */ + for (int p = 0; p < precPad; p++) + _pf_putc(st, '0'); + + while (i > 0) + _pf_putc(st, tmp[--i]); +} + +int vsnprintf(char *buf, size_t size, const char *fmt, va_list ap) { + struct _pf_state st; + st.buf = buf; + st.pos = 0; + st.max = size > 0 ? size - 1 : 0; + + while (*fmt) { + if (*fmt != '%') { + _pf_putc(&st, *fmt++); + continue; + } + fmt++; /* skip '%' */ + + /* Flags */ + char pad = ' '; + int left_align = 0; + while (*fmt == '0' || *fmt == '-' || *fmt == '+' || *fmt == ' ') { + if (*fmt == '0') pad = '0'; + if (*fmt == '-') { left_align = 1; pad = ' '; } + fmt++; + } + + /* Width */ + int width = 0; + if (*fmt == '*') { + width = va_arg(ap, int); + fmt++; + } else { + while (*fmt >= '0' && *fmt <= '9') { + width = width * 10 + (*fmt - '0'); + fmt++; + } + } + + /* Precision (consumed but mostly ignored) */ + int precision = -1; + if (*fmt == '.') { + fmt++; + precision = 0; + if (*fmt == '*') { + precision = va_arg(ap, int); + fmt++; + } else { + while (*fmt >= '0' && *fmt <= '9') { + precision = precision * 10 + (*fmt - '0'); + fmt++; + } + } + } + + /* Length modifier */ + int is_long = 0; + if (*fmt == 'l') { is_long = 1; fmt++; if (*fmt == 'l') { is_long = 2; fmt++; } } + else if (*fmt == 'h') { fmt++; if (*fmt == 'h') fmt++; } + else if (*fmt == 'z') { is_long = 1; fmt++; } + + /* Conversion */ + switch (*fmt) { + case 'd': case 'i': { + long val; + if (is_long >= 1) val = va_arg(ap, long); + else val = va_arg(ap, int); + int neg = 0; + unsigned long uval; + if (val < 0) { neg = 1; uval = (unsigned long)(-val); } + else uval = (unsigned long)val; + if (left_align) { + size_t before = st.pos; + _pf_putnum(&st, uval, 10, 0, 0, pad, neg, precision); + size_t len = st.pos - before; + for (size_t w = len; (int)w < width; w++) _pf_putc(&st, ' '); + } else { + _pf_putnum(&st, uval, 10, 0, width, pad, neg, precision); + } + break; + } + case 'u': { + unsigned long val; + if (is_long >= 1) val = va_arg(ap, unsigned long); + else val = va_arg(ap, unsigned int); + if (left_align) { + size_t before = st.pos; + _pf_putnum(&st, val, 10, 0, 0, ' ', 0, precision); + size_t len = st.pos - before; + for (size_t w = len; (int)w < width; w++) _pf_putc(&st, ' '); + } else { + _pf_putnum(&st, val, 10, 0, width, pad, 0, precision); + } + break; + } + case 'x': case 'X': { + unsigned long val; + if (is_long >= 1) val = va_arg(ap, unsigned long); + else val = va_arg(ap, unsigned int); + int upper = (*fmt == 'X'); + if (left_align) { + size_t before = st.pos; + _pf_putnum(&st, val, 16, upper, 0, pad, 0, precision); + size_t len = st.pos - before; + for (size_t w = len; (int)w < width; w++) _pf_putc(&st, ' '); + } else { + _pf_putnum(&st, val, 16, upper, width, pad, 0, precision); + } + break; + } + case 'o': { + unsigned long val; + if (is_long >= 1) val = va_arg(ap, unsigned long); + else val = va_arg(ap, unsigned int); + _pf_putnum(&st, val, 8, 0, width, pad, 0, precision); + break; + } + case 'p': { + void *val = va_arg(ap, void *); + _pf_puts(&st, "0x"); + _pf_putnum(&st, (unsigned long)val, 16, 0, 0, '0', 0, -1); + break; + } + case 's': { + const char *s = va_arg(ap, const char *); + if (s == NULL) s = "(null)"; + int slen = (int)strlen(s); + if (precision >= 0 && precision < slen) slen = precision; + if (!left_align) { + for (int w = slen; w < width; w++) _pf_putc(&st, ' '); + } + for (int i = 0; i < slen; i++) _pf_putc(&st, s[i]); + if (left_align) { + for (int w = slen; w < width; w++) _pf_putc(&st, ' '); + } + break; + } + case 'c': { + char c = (char)va_arg(ap, int); + _pf_putc(&st, c); + break; + } + case '%': + _pf_putc(&st, '%'); + break; + default: + _pf_putc(&st, '%'); + _pf_putc(&st, *fmt); + break; + } + if (*fmt) fmt++; + } + + /* Null terminate */ + if (size > 0) { + if (st.pos < size) + st.buf[st.pos] = '\0'; + else + st.buf[size - 1] = '\0'; + } + + return (int)st.pos; +} + +int vsprintf(char *buf, const char *fmt, va_list ap) { + return vsnprintf(buf, (size_t)-1, fmt, ap); +} + +int snprintf(char *buf, size_t size, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vsnprintf(buf, size, fmt, ap); + va_end(ap); + return ret; +} + +int sprintf(char *buf, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vsnprintf(buf, (size_t)-1, fmt, ap); + va_end(ap); + return ret; +} + +/* Print buffer for printf/fprintf */ +static char _printbuf[4096]; + +int vprintf(const char *fmt, va_list ap) { + int ret = vsnprintf(_printbuf, sizeof(_printbuf), fmt, ap); + _zos_syscall1(SYS_PRINT, (long)_printbuf); + return ret; +} + +int printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vprintf(fmt, ap); + va_end(ap); + return ret; +} + +int puts(const char *s) { + _zos_syscall1(SYS_PRINT, (long)s); + _zos_syscall1(SYS_PUTCHAR, (long)'\n'); + return 0; +} + +int putchar(int c) { + _zos_syscall1(SYS_PUTCHAR, (long)c); + return c; +} + +/* ======================================================================== + FILE I/O + ======================================================================== */ + +/* Simple FILE struct */ +struct _FILE { + int handle; + uint64_t pos; + uint64_t size; + int eof; + int error; + int is_std; /* 1 = stdout, 2 = stderr */ + int ungetc_buf; /* -1 if empty */ +}; + +typedef struct _FILE FILE; + +/* Up to 16 open files */ +#define MAX_FILES 16 +static FILE _file_pool[MAX_FILES]; +static int _file_pool_init = 0; + +/* Standard streams */ +static FILE _stdout_file = { -1, 0, 0, 0, 0, 1, -1 }; +static FILE _stderr_file = { -1, 0, 0, 0, 0, 2, -1 }; +static FILE _stdin_file = { -1, 0, 0, 0, 0, 3, -1 }; + +FILE *stdout = &_stdout_file; +FILE *stderr = &_stderr_file; +FILE *stdin = &_stdin_file; + +static FILE *alloc_file(void) { + if (!_file_pool_init) { + memset(_file_pool, 0, sizeof(_file_pool)); + for (int i = 0; i < MAX_FILES; i++) { + _file_pool[i].handle = -1; + _file_pool[i].ungetc_buf = -1; + } + _file_pool_init = 1; + } + for (int i = 0; i < MAX_FILES; i++) { + if (_file_pool[i].handle == -1 && _file_pool[i].is_std == 0) { + return &_file_pool[i]; + } + } + return NULL; +} + +FILE *fopen(const char *path, const char *mode) { + (void)mode; + + /* Build VFS path: prepend "0:/" if not already prefixed */ + char vfspath[256]; + if (path[0] == '0' && path[1] == ':') { + /* Already has VFS prefix */ + size_t len = strlen(path); + if (len >= sizeof(vfspath)) len = sizeof(vfspath) - 1; + memcpy(vfspath, path, len); + vfspath[len] = '\0'; + } else { + /* Prepend "0:/" */ + const char *prefix = "0:/"; + int i = 0; + while (prefix[i]) { vfspath[i] = prefix[i]; i++; } + int j = 0; + while (path[j] && i < 254) { vfspath[i++] = path[j++]; } + vfspath[i] = '\0'; + } + + int handle = (int)_zos_syscall1(SYS_OPEN, (long)vfspath); + if (handle < 0) { + errno = 2; /* ENOENT */ + return NULL; + } + + FILE *fp = alloc_file(); + if (fp == NULL) { + _zos_syscall1(SYS_CLOSE, (long)handle); + errno = 12; /* ENOMEM */ + return NULL; + } + + fp->handle = handle; + fp->pos = 0; + fp->size = (uint64_t)_zos_syscall1(SYS_GETSIZE, (long)handle); + fp->eof = 0; + fp->error = 0; + fp->is_std = 0; + fp->ungetc_buf = -1; + + return fp; +} + +int fclose(FILE *fp) { + if (fp == NULL || fp->is_std) return -1; + _zos_syscall1(SYS_CLOSE, (long)fp->handle); + fp->handle = -1; + fp->is_std = 0; + return 0; +} + +size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp) { + if (fp == NULL || fp->is_std || size == 0 || nmemb == 0) return 0; + + size_t total = size * nmemb; + size_t remaining = 0; + if (fp->pos < fp->size) + remaining = (size_t)(fp->size - fp->pos); + + if (total > remaining) total = remaining; + if (total == 0) { fp->eof = 1; return 0; } + + int bytes = (int)_zos_syscall4(SYS_READ, + (long)fp->handle, (long)ptr, (long)fp->pos, (long)total); + + if (bytes <= 0) { fp->eof = 1; return 0; } + + fp->pos += (uint64_t)bytes; + if (fp->pos >= fp->size) fp->eof = 1; + + return (size_t)bytes / size; +} + +size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp) { + if (fp == NULL) return 0; + + /* stdout/stderr: write to terminal */ + if (fp->is_std == 1 || fp->is_std == 2) { + /* Write as string to terminal */ + size_t total = size * nmemb; + const char *s = (const char *)ptr; + char buf[512]; + while (total > 0) { + size_t chunk = total > 511 ? 511 : total; + memcpy(buf, s, chunk); + buf[chunk] = '\0'; + _zos_syscall1(SYS_PRINT, (long)buf); + s += chunk; + total -= chunk; + } + return nmemb; + } + + /* Read-only filesystem, no-op */ + return 0; +} + +int fseek(FILE *fp, long offset, int whence) { + if (fp == NULL || fp->is_std) return -1; + + uint64_t newpos; + switch (whence) { + case 0: /* SEEK_SET */ + newpos = (uint64_t)offset; + break; + case 1: /* SEEK_CUR */ + newpos = fp->pos + (uint64_t)offset; + break; + case 2: /* SEEK_END */ + newpos = fp->size + (uint64_t)offset; + break; + default: + return -1; + } + + fp->pos = newpos; + fp->eof = 0; + return 0; +} + +long ftell(FILE *fp) { + if (fp == NULL || fp->is_std) return -1; + return (long)fp->pos; +} + +int fflush(FILE *fp) { + (void)fp; + return 0; +} + +int feof(FILE *fp) { + if (fp == NULL) return 1; + return fp->eof; +} + +int ferror(FILE *fp) { + if (fp == NULL) return 1; + return fp->error; +} + +void clearerr(FILE *fp) { + if (fp == NULL) return; + fp->eof = 0; + fp->error = 0; +} + +int fgetc(FILE *fp) { + if (fp == NULL) return -1; + if (fp->ungetc_buf >= 0) { + int c = fp->ungetc_buf; + fp->ungetc_buf = -1; + return c; + } + unsigned char c; + size_t n = fread(&c, 1, 1, fp); + return n == 1 ? (int)c : -1; +} + +int getc(FILE *fp) { + return fgetc(fp); +} + +int ungetc(int c, FILE *fp) { + if (fp == NULL || c == -1) return -1; + fp->ungetc_buf = c; + fp->eof = 0; + return c; +} + +char *fgets(char *s, int size, FILE *fp) { + if (size <= 0) return NULL; + int i = 0; + while (i < size - 1) { + int c = fgetc(fp); + if (c == -1) break; + s[i++] = (char)c; + if (c == '\n') break; + } + if (i == 0) return NULL; + s[i] = '\0'; + return s; +} + +int fputs(const char *s, FILE *fp) { + size_t len = strlen(s); + return fwrite(s, 1, len, fp) > 0 ? 0 : -1; +} + +int fprintf(FILE *fp, const char *fmt, ...) { + char buf[4096]; + va_list ap; + va_start(ap, fmt); + int ret = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + if (fp == stdout || fp == stderr || (fp && fp->is_std)) { + _zos_syscall1(SYS_PRINT, (long)buf); + } + return ret; +} + +int vfprintf(FILE *fp, const char *fmt, va_list ap) { + char buf[4096]; + int ret = vsnprintf(buf, sizeof(buf), fmt, ap); + + if (fp == stdout || fp == stderr || (fp && fp->is_std)) { + _zos_syscall1(SYS_PRINT, (long)buf); + } + return ret; +} + +int sscanf(const char *str, const char *fmt, ...) { + /* Minimal sscanf: supports %d, %s, %x, %u only */ + va_list ap; + va_start(ap, fmt); + int count = 0; + const char *s = str; + + while (*fmt && *s) { + if (*fmt == '%') { + fmt++; + if (*fmt == 'd' || *fmt == 'i') { + int *out = va_arg(ap, int *); + int neg = 0; + int val = 0; + while (isspace((unsigned char)*s)) s++; + if (*s == '-') { neg = 1; s++; } + else if (*s == '+') s++; + if (!isdigit((unsigned char)*s)) break; + while (isdigit((unsigned char)*s)) { val = val * 10 + (*s - '0'); s++; } + *out = neg ? -val : val; + count++; + fmt++; + } else if (*fmt == 'u') { + unsigned int *out = va_arg(ap, unsigned int *); + unsigned int val = 0; + while (isspace((unsigned char)*s)) s++; + if (!isdigit((unsigned char)*s)) break; + while (isdigit((unsigned char)*s)) { val = val * 10 + (*s - '0'); s++; } + *out = val; + count++; + fmt++; + } else if (*fmt == 'x' || *fmt == 'X') { + unsigned int *out = va_arg(ap, unsigned int *); + unsigned int val = 0; + while (isspace((unsigned char)*s)) s++; + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2; + while (isxdigit((unsigned char)*s)) { + int d; + if (*s >= '0' && *s <= '9') d = *s - '0'; + else if (*s >= 'a' && *s <= 'f') d = *s - 'a' + 10; + else d = *s - 'A' + 10; + val = val * 16 + d; + s++; + } + *out = val; + count++; + fmt++; + } else if (*fmt == 's') { + char *out = va_arg(ap, char *); + while (isspace((unsigned char)*s)) s++; + while (*s && !isspace((unsigned char)*s)) *out++ = *s++; + *out = '\0'; + count++; + fmt++; + } else { + break; + } + } else if (isspace((unsigned char)*fmt)) { + while (isspace((unsigned char)*s)) s++; + while (isspace((unsigned char)*fmt)) fmt++; + } else { + if (*s != *fmt) break; + s++; + fmt++; + } + } + + va_end(ap); + return count; +} + +void perror(const char *s) { + if (s && *s) { + _zos_syscall1(SYS_PRINT, (long)s); + _zos_syscall1(SYS_PRINT, (long)": "); + } + _zos_syscall1(SYS_PRINT, (long)"error\n"); +} + +int rename(const char *old, const char *new_) { + (void)old; (void)new_; + return -1; +} + +int remove(const char *path) { + (void)path; + return -1; +} + +FILE *tmpfile(void) { + return NULL; +} + +char *tmpnam(char *s) { + (void)s; + return NULL; +} + +/* ======================================================================== + Stubs for unneeded functions + ======================================================================== */ + +int mkdir(const char *path, unsigned int mode) { + (void)path; (void)mode; + return -1; +} + +/* math.h stubs — doom's fixed-point code doesn't use libm heavily, + but z_zone.c references some functions. Provide no-ops. */ +double fabs(double x) { return x < 0 ? -x : x; } +double floor(double x) { return (double)(long)x - (x < (double)(long)x ? 1.0 : 0.0); } +double ceil(double x) { double f = floor(x); return (x > f) ? f + 1.0 : f; } +double fmod(double x, double y) { if (y == 0.0) return 0.0; return x - (double)((long)(x/y)) * y; } +double sqrt(double x) { + if (x <= 0.0) return 0.0; + double guess = x; + for (int i = 0; i < 20; i++) + guess = (guess + x / guess) * 0.5; + return guess; +} +double pow(double base, double exp) { + if (exp == 0.0) return 1.0; + if (exp == 1.0) return base; + /* Integer exponent fast path */ + if (exp == (double)(long)exp && exp > 0) { + double r = 1.0; + long e = (long)exp; + while (e-- > 0) r *= base; + return r; + } + return 0.0; +} +double sin(double x) { (void)x; return 0.0; } +double cos(double x) { (void)x; return 1.0; } +double atan2(double y, double x) { (void)y; (void)x; return 0.0; } +double log(double x) { (void)x; return 0.0; } +double exp(double x) { (void)x; return 1.0; } +double round(double x) { return floor(x + 0.5); } +double atof(const char *s) { (void)s; return 0.0; } +float floorf(float x) { return (float)floor((double)x); } +float ceilf(float x) { return (float)ceil((double)x); } +float fabsf(float x) { return x < 0.0f ? -x : x; } + +/* ======================================================================== + Misc stubs that doom might reference + ======================================================================== */ + +/* Some doom code calls I_Error which ends up doing fprintf + exit */ +/* The signal/system stuff is behind ifdefs, but just in case: */ + +unsigned int sleep(unsigned int seconds) { + (void)seconds; + return 0; +} + +int system(const char *command) { + (void)command; + return -1; +} diff --git a/programs/src/man/main.cpp b/programs/src/man/main.cpp new file mode 100644 index 0000000..c935e2b --- /dev/null +++ b/programs/src/man/main.cpp @@ -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 +#include + +// ---- 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 \n"); + zenith::print(" man
\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); +} diff --git a/programs/src/shell/main.cpp b/programs/src/shell/main.cpp index bf5079e..5318ba9 100644 --- a/programs/src/shell/main.cpp +++ b/programs/src/shell/main.cpp @@ -44,15 +44,17 @@ static void print_int(uint64_t n) { } static void prompt() { - zenith::print("zenith> "); + zenith::print("% "); } static void cmd_help() { zenith::print("Available commands:\n"); zenith::print(" help Show this help message\n"); zenith::print(" info Show system information\n"); + zenith::print(" man View manual pages\n"); zenith::print(" ls List ramdisk files\n"); zenith::print(" cat Display file contents\n"); + zenith::print(" run Spawn a new process from an ELF file\n"); zenith::print(" ping Send ICMP echo requests\n"); zenith::print(" uptime Show uptime in milliseconds\n"); zenith::print(" clear Clear the screen\n"); @@ -223,11 +225,52 @@ static void cmd_ping(const char* arg) { } } -static void cmd_clear() { - // Print enough newlines to scroll past visible content - for (int i = 0; i < 50; i++) { - zenith::putchar('\n'); +static void cmd_run(const char* arg) { + arg = skip_spaces(arg); + if (*arg == '\0') { + zenith::print("Usage: run \n"); + return; } + + // Build path "0:/" + 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 \n"); + zenith::print(" man
\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) { @@ -241,10 +284,18 @@ static void process_command(const char* line) { cmd_info(); } else if (streq(line, "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 ")) { cmd_cat(line + 4); } else if (streq(line, "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 ")) { cmd_ping(line + 5); } else if (streq(line, "ping")) { @@ -265,7 +316,10 @@ static void process_command(const char* line) { extern "C" void _start() { 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("\n"); diff --git a/ramdisk.tar b/ramdisk.tar index c7658e0..28132ae 100644 Binary files a/ramdisk.tar and b/ramdisk.tar differ