Files
MontaukOS/docs/userspace.html
T

1063 lines
43 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MontaukOS &mdash; Userspace Application Development</title>
<style>
:root {
--bg: #ffffff;
--surface: #f6f8fa;
--border: #d0d7de;
--fg: #1f2328;
--fg-muted: #656d76;
--accent: #0969da;
--accent2: #8250df;
--green: #1a7f37;
--orange: #9a6700;
--red: #cf222e;
--code-bg: #f6f8fa;
--sidebar-w: 280px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; scroll-padding-top: 2rem; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--fg);
line-height: 1.6;
display: flex;
min-height: 100vh;
}
/* ---- Sidebar ---- */
nav.sidebar {
position: fixed;
top: 0; left: 0;
width: var(--sidebar-w);
height: 100vh;
overflow-y: auto;
background: var(--surface);
border-right: 1px solid var(--border);
padding: 1.5rem 1rem;
z-index: 10;
}
nav.sidebar h2 {
font-size: 1rem;
color: var(--accent);
margin-bottom: .5rem;
text-transform: uppercase;
letter-spacing: .08em;
}
nav.sidebar ul { list-style: none; }
nav.sidebar > ul > li { margin-bottom: .15rem; }
nav.sidebar a {
display: block;
color: var(--fg-muted);
text-decoration: none;
padding: .25rem .5rem;
border-radius: 4px;
font-size: .9rem;
transition: background .15s, color .15s;
}
nav.sidebar a:hover,
nav.sidebar a:focus { background: var(--border); color: var(--fg); }
nav.sidebar ul ul { padding-left: .85rem; }
nav.sidebar ul ul a { font-size: .82rem; }
/* ---- Main content ---- */
main {
margin-left: var(--sidebar-w);
max-width: 54rem;
padding: 2.5rem 3rem 6rem;
width: 100%;
}
h1 {
font-size: 2.2rem;
color: var(--accent);
border-bottom: 2px solid var(--border);
padding-bottom: .5rem;
margin-bottom: 1rem;
}
h2 {
font-size: 1.6rem;
color: var(--accent2);
margin-top: 3rem;
margin-bottom: .75rem;
border-bottom: 1px solid var(--border);
padding-bottom: .35rem;
}
h3 {
font-size: 1.2rem;
color: var(--fg);
margin-top: 2rem;
margin-bottom: .5rem;
}
h4 {
font-size: 1rem;
color: var(--fg-muted);
margin-top: 1.5rem;
margin-bottom: .35rem;
}
p, li { margin-bottom: .6rem; }
ul, ol { padding-left: 1.5rem; }
a { color: var(--accent); }
code {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: .88em;
background: var(--code-bg);
padding: .15em .35em;
border-radius: 4px;
}
pre {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem 1.25rem;
overflow-x: auto;
margin: .75rem 0 1.25rem;
line-height: 1.45;
}
pre code {
background: none;
padding: 0;
font-size: .85rem;
}
table {
width: 100%;
border-collapse: collapse;
margin: .75rem 0 1.25rem;
font-size: .9rem;
}
th, td {
text-align: left;
padding: .55rem .75rem;
border: 1px solid var(--border);
}
th {
background: var(--surface);
color: var(--accent);
font-weight: 600;
}
tr:nth-child(even) td { background: rgba(0,0,0,.02); }
.badge {
display: inline-block;
font-size: .72rem;
font-weight: 600;
padding: .15em .55em;
border-radius: 3px;
vertical-align: middle;
text-transform: uppercase;
letter-spacing: .04em;
}
.badge-process { background: #dafbe1; color: var(--green); }
.badge-console { background: #fff8c5; color: var(--orange); }
.badge-fileio { background: #ffebe9; color: var(--red); }
.badge-memory { background: #ddf4ff; color: var(--accent); }
.badge-time { background: #fbefff; color: var(--accent2); }
.badge-system { background: #ddf4ff; color: #0e6d5e; }
.badge-keyboard { background: #fff8c5; color: #7d6600; }
.badge-network { background: #ddf4ff; color: #0550ae; }
.badge-framebuf { background: #ffebe9; color: #953800; }
.note {
border-left: 3px solid var(--accent);
background: rgba(9,105,218,.06);
padding: .75rem 1rem;
margin: 1rem 0;
border-radius: 0 4px 4px 0;
font-size: .9rem;
}
.note strong { color: var(--accent); }
.warn {
border-left: 3px solid var(--orange);
background: rgba(154,103,0,.06);
padding: .75rem 1rem;
margin: 1rem 0;
border-radius: 0 4px 4px 0;
font-size: .9rem;
}
.warn strong { color: var(--orange); }
.sig {
color: #1a7f37;
}
.file-path {
color: var(--fg-muted);
font-size: .82rem;
font-style: italic;
}
hr {
border: none;
border-top: 1px solid var(--border);
margin: 2rem 0;
}
@media (max-width: 900px) {
nav.sidebar { display: none; }
main { margin-left: 0; padding: 1.5rem; }
}
</style>
</head>
<body>
<!-- ================================================================= -->
<!-- SIDEBAR -->
<!-- ================================================================= -->
<nav class="sidebar">
<h2>MontaukOS Docs</h2>
<ul>
<li><a href="#overview">Overview</a></li>
<li><a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#project-structure">Project Structure</a></li>
<li><a href="#toolchain">Toolchain</a></li>
<li><a href="#build-system">Build System</a></li>
<li><a href="#linker-script">Linker Script</a></li>
</ul>
</li>
<li><a href="#program-anatomy">Program Anatomy</a>
<ul>
<li><a href="#entry-point">Entry Point</a></li>
<li><a href="#hello-world">Hello World</a></li>
</ul>
</li>
<li><a href="#syscall-arch">Syscall Architecture</a>
<ul>
<li><a href="#calling-convention">Calling Convention</a></li>
<li><a href="#raw-wrappers">Raw Wrappers</a></li>
<li><a href="#typed-wrappers">Typed Wrappers</a></li>
</ul>
</li>
<li><a href="#syscall-ref">Syscall Reference</a>
<ul>
<li><a href="#cat-process">Process</a></li>
<li><a href="#cat-console">Console I/O</a></li>
<li><a href="#cat-fileio">File I/O</a></li>
<li><a href="#cat-memory">Memory</a></li>
<li><a href="#cat-time">Timekeeping</a></li>
<li><a href="#cat-system">System</a></li>
<li><a href="#cat-keyboard">Keyboard</a></li>
<li><a href="#cat-network">Networking</a></li>
<li><a href="#cat-framebuffer">Framebuffer</a></li>
<li><a href="#cat-terminal">Terminal</a></li>
</ul>
</li>
<li><a href="#data-structures">Data Structures</a></li>
<li><a href="#shell-walkthrough">Shell Walkthrough</a></li>
<li><a href="#adding-syscalls">Adding New Syscalls</a></li>
<li><a href="#memory-model">Memory Model</a></li>
<li><a href="#limitations">Limitations</a></li>
</ul>
</nav>
<!-- ================================================================= -->
<!-- MAIN CONTENT -->
<!-- ================================================================= -->
<main>
<!-- ------ Overview ------ -->
<h1 id="overview">Userspace Developer's Handbook</h1>
<p>
MontaukOS is a hobbyist 64-bit operating system written in C++20.
Userspace programs run in Ring&nbsp;3, are loaded as static ELF64
binaries, and communicate with the kernel through the x86-64
<code>SYSCALL</code>/<code>SYSRET</code> mechanism.
</p>
<p>
This document covers everything you need to write, build, and run
userspace applications &mdash; from a minimal "Hello World" to using
every available syscall &mdash; as well as how to extend the kernel
with new syscalls.
</p>
<!-- ================================================================= -->
<h2 id="getting-started">Getting Started</h2>
<h3 id="project-structure">Project Structure</h3>
<pre><code>programs/
&#x251C;&#x2500;&#x2500; GNUmakefile # Build system (C++ programs)
&#x251C;&#x2500;&#x2500; link.ld # Linker script (base address 0x400000)
&#x251C;&#x2500;&#x2500; include/
&#x2502; &#x251C;&#x2500;&#x2500; Api/
&#x2502; &#x2502; &#x2514;&#x2500;&#x2500; Syscall.hpp # Syscall numbers &amp; data structures
&#x2502; &#x251C;&#x2500;&#x2500; montauk/
&#x2502; &#x2502; &#x251C;&#x2500;&#x2500; syscall.h # Inline asm wrappers &amp; typed API
&#x2502; &#x2502; &#x2514;&#x2500;&#x2500; heap.h # Userspace heap allocator (malloc/mfree/realloc)
&#x2502; &#x2514;&#x2500;&#x2500; libc/ # Minimal C standard library headers
&#x2502; &#x251C;&#x2500;&#x2500; stdio.h # printf, FILE I/O
&#x2502; &#x251C;&#x2500;&#x2500; stdlib.h # malloc, free, atoi, exit
&#x2502; &#x251C;&#x2500;&#x2500; string.h # memcpy, strlen, strcmp, etc.
&#x2502; &#x2514;&#x2500;&#x2500; ... # ctype.h, errno.h, assert.h, etc.
&#x251C;&#x2500;&#x2500; src/
&#x2502; &#x251C;&#x2500;&#x2500; hello/
&#x2502; &#x2502; &#x2514;&#x2500;&#x2500; main.cpp # Hello world example
&#x2502; &#x251C;&#x2500;&#x2500; shell/
&#x2502; &#x2502; &#x2514;&#x2500;&#x2500; main.cpp # Interactive shell
&#x2502; &#x251C;&#x2500;&#x2500; man/
&#x2502; &#x2502; &#x2514;&#x2500;&#x2500; main.cpp # Manual page viewer
&#x2502; &#x2514;&#x2500;&#x2500; doom/
&#x2502; &#x251C;&#x2500;&#x2500; Makefile # DOOM build system
&#x2502; &#x251C;&#x2500;&#x2500; doomgeneric_montauk.c # MontaukOS platform layer
&#x2502; &#x2514;&#x2500;&#x2500; libc.c # C library implementation
&#x251C;&#x2500;&#x2500; bin/ # Compiled .elf binaries
&#x2514;&#x2500;&#x2500; obj/ # Intermediate object files</code></pre>
<p>
Each subdirectory under <code>src/</code> is treated as a separate
program. The build system discovers them automatically.
</p>
<h3 id="toolchain">Toolchain</h3>
<p>
Programs are compiled with a freestanding <code>x86_64-elf</code>
cross-compiler. The build system looks for one in
<code>toolchain/local/bin/</code>; if not found, it falls back to the
host <code>g++</code>.
</p>
<p>Key compiler flags:</p>
<ul>
<li><code>-std=gnu++20</code> &mdash; C++20 with GNU extensions</li>
<li><code>-ffreestanding -nostdinc -nostdlib</code> &mdash; no hosted standard library</li>
<li><code>-fno-rtti -fno-exceptions</code> &mdash; no RTTI or C++ exceptions</li>
<li><code>-fno-PIC -mcmodel=small</code> &mdash; static absolute addressing</li>
<li><code>-mno-sse -mno-mmx -mno-red-zone</code> &mdash; kernel-safe ABI (C++ programs)</li>
</ul>
<div class="note">
<strong>SSE:</strong> The default C++ build disables SSE. Programs that need
floating-point support (e.g.&nbsp;C programs using <code>float</code>/<code>double</code>)
should compile with <code>-msse -msse2</code> instead. The kernel enables SSE in
CR0/CR4 at boot, so SSE instructions are safe in userspace.
</div>
<h3 id="build-system">Build System</h3>
<p>To build all programs:</p>
<pre><code>cd programs/
make # or: make -j$(nproc)</code></pre>
<p>
This produces one ELF binary per program in <code>bin/</code>
(e.g.&nbsp;<code>bin/hello.elf</code>, <code>bin/shell.elf</code>).
The binaries are then typically packed into the ramdisk
(<code>ramdisk.tar</code>) and loaded by the kernel at boot.
</p>
<p>To add a new program, create <code>src/&lt;name&gt;/main.cpp</code> and run <code>make</code>.</p>
<h3 id="linker-script">Linker Script</h3>
<p class="file-path">programs/link.ld</p>
<p>
All userspace programs are linked at virtual address
<code>0x400000</code>. The linker script defines four standard
sections:
</p>
<table>
<tr><th>Section</th><th>Contents</th><th>Alignment</th></tr>
<tr><td><code>.text</code></td><td>Executable code</td><td>&mdash; (base)</td></tr>
<tr><td><code>.rodata</code></td><td>Read-only data, string literals</td><td>4 KiB</td></tr>
<tr><td><code>.data</code></td><td>Initialized read/write data</td><td>4 KiB</td></tr>
<tr><td><code>.bss</code></td><td>Zero-initialized data</td><td>&mdash;</td></tr>
</table>
<p>
Debug frames (<code>.eh_frame</code>), notes, and comments are
discarded to keep binaries small.
</p>
<!-- ================================================================= -->
<h2 id="program-anatomy">Program Anatomy</h2>
<h3 id="entry-point">Entry Point: <code>_start</code></h3>
<p>
Because there is no C runtime, every program must define
<code>extern "C" void _start()</code> as its entry point. There is no
<code>main()</code>, no <code>argc</code>/<code>argv</code>, and no
<code>atexit</code> handlers.
</p>
<div class="note">
<strong>Auto-exit:</strong> If <code>_start()</code> returns normally,
the kernel's exit stub (mapped at <code>0x3FF000</code>) automatically
calls <code>SYS_EXIT(0)</code>. You can also call
<code>montauk::exit(code)</code> explicitly at any point.
</div>
<h3 id="hello-world">Hello World</h3>
<p class="file-path">programs/src/hello/main.cpp</p>
<pre><code><span style="color:#8b949e">// Minimal MontaukOS userspace program</span>
#include &lt;montauk/syscall.h&gt;
extern "C" void _start() {
montauk::print("Hello from userspace!\n");
}</code></pre>
<p>
Include <code>&lt;montauk/syscall.h&gt;</code> for the full typed API.
That header pulls in <code>&lt;Api/Syscall.hpp&gt;</code> for
constants and data structures.
</p>
<!-- ================================================================= -->
<h2 id="syscall-arch">Syscall Architecture</h2>
<h3 id="calling-convention">Calling Convention</h3>
<p>
MontaukOS uses the hardware <code>SYSCALL</code> instruction on
x86-64. The kernel sets up the required MSRs
(<code>IA32_STAR</code>, <code>IA32_LSTAR</code>,
<code>IA32_FMASK</code>) during boot.
</p>
<table>
<tr><th>Register</th><th>Purpose</th></tr>
<tr><td><code>RAX</code></td><td>Syscall number (in) / return value (out)</td></tr>
<tr><td><code>RDI</code></td><td>Argument 1</td></tr>
<tr><td><code>RSI</code></td><td>Argument 2</td></tr>
<tr><td><code>RDX</code></td><td>Argument 3</td></tr>
<tr><td><code>R10</code></td><td>Argument 4 (not RCX &mdash; <code>SYSCALL</code> clobbers it)</td></tr>
<tr><td><code>R8</code></td><td>Argument 5</td></tr>
<tr><td><code>R9</code></td><td>Argument 6</td></tr>
</table>
<p>
The <code>SYSCALL</code> instruction saves <code>RIP</code> in
<code>RCX</code> and <code>RFLAGS</code> in <code>R11</code>; both
registers are clobbered. The kernel masks <code>IF</code> on entry
(via <code>IA32_FMASK</code>) so interrupts are disabled during the
transition.
</p>
<p>
On the kernel side, <code>SyscallEntry</code> (assembly) saves
callee-saved registers and all arguments into a
<code>SyscallFrame</code>, then calls
<code>SyscallDispatch(SyscallFrame*)</code> which dispatches by
syscall number.
</p>
<h3 id="raw-wrappers">Raw Syscall Wrappers</h3>
<p class="file-path">programs/include/montauk/syscall.h</p>
<p>
Seven inline functions cover 0&ndash;6 argument syscalls:
</p>
<pre><code>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);</code></pre>
<p>
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.
</p>
<h3 id="typed-wrappers">Typed API Wrappers</h3>
<p>
The <code>montauk::</code> namespace provides type-safe wrappers around
the raw syscalls. These are the recommended interface for application
code. The full reference follows below.
</p>
<!-- ================================================================= -->
<h2 id="syscall-ref">Syscall Reference</h2>
<p>
MontaukOS v0.1.0 exposes <strong>26 syscalls</strong> (numbers
0&ndash;25), organized into 10 categories.
</p>
<!-- ---- Process ---- -->
<h3 id="cat-process"><span class="badge badge-process">Process</span> Process Management</h3>
<h4 id="sys-exit"><code>SYS_EXIT</code> (0) &mdash; Terminate the current process</h4>
<pre><code><span class="sig">[[noreturn]] void montauk::exit(int code = 0);</span></code></pre>
<p>
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.
</p>
<h4 id="sys-yield"><code>SYS_YIELD</code> (1) &mdash; Yield the CPU</h4>
<pre><code><span class="sig">void montauk::yield();</span></code></pre>
<p>
Voluntarily yields the remainder of the current time slice, allowing
the scheduler to run another ready process immediately.
</p>
<h4 id="sys-sleep-ms"><code>SYS_SLEEP_MS</code> (2) &mdash; Sleep for a duration</h4>
<pre><code><span class="sig">void montauk::sleep_ms(uint64_t ms);</span></code></pre>
<p>
Suspends the calling process for at least <code>ms</code>
milliseconds. Resolution depends on the APIC timer tick rate.
</p>
<h4 id="sys-getpid"><code>SYS_GETPID</code> (3) &mdash; Get process ID</h4>
<pre><code><span class="sig">int montauk::getpid();</span></code></pre>
<p>
Returns the PID of the calling process. Returns <code>-1</code> if
called from the idle context (should not happen in userspace).
</p>
<h4 id="sys-spawn"><code>SYS_SPAWN</code> (20) &mdash; Spawn a new process</h4>
<pre><code><span class="sig">int montauk::spawn(const char* path, const char* args = nullptr);</span></code></pre>
<p>
Loads the ELF binary at <code>path</code> (a VFS path like
<code>"0:/hello.elf"</code>) and spawns it as a new process.
Returns the new process's PID on success, or <code>-1</code> on
failure (no free slots, invalid ELF, file not found).
The optional <code>args</code> string (up to 255 characters) is
copied into the new process and can be retrieved with
<code>SYS_GETARGS</code>.
</p>
<h4 id="sys-waitpid"><code>SYS_WAITPID</code> (23) &mdash; Wait for a process to exit</h4>
<pre><code><span class="sig">void montauk::waitpid(int pid);</span></code></pre>
<p>
Blocks the calling process until the process identified by
<code>pid</code> 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.&nbsp;<code>run</code>
command) so that keyboard input and the terminal are not shared
simultaneously.
</p>
<h4 id="sys-getargs"><code>SYS_GETARGS</code> (25) &mdash; Get process arguments</h4>
<pre><code><span class="sig">int montauk::getargs(char* buf, uint64_t maxLen);</span></code></pre>
<p>
Copies the argument string passed to the current process (via
<code>spawn()</code>) into <code>buf</code>, writing at most
<code>maxLen - 1</code> characters plus a null terminator.
Returns the number of characters copied, or <code>-1</code> on error.
If no arguments were provided at spawn time, the buffer will be empty.
</p>
<!-- ---- Console I/O ---- -->
<h3 id="cat-console"><span class="badge badge-console">Console</span> Console I/O</h3>
<h4 id="sys-print"><code>SYS_PRINT</code> (4) &mdash; Print a string</h4>
<pre><code><span class="sig">void montauk::print(const char* text);</span></code></pre>
<p>
Writes a null-terminated string to the kernel terminal. Supports
newlines (<code>\n</code>) and standard printable ASCII.
</p>
<h4 id="sys-putchar"><code>SYS_PUTCHAR</code> (5) &mdash; Print a single character</h4>
<pre><code><span class="sig">void montauk::putchar(char c);</span></code></pre>
<p>
Writes a single character to the kernel terminal. Useful for building
output character by character (e.g.&nbsp;printing integers).
</p>
<!-- ---- File I/O ---- -->
<h3 id="cat-fileio"><span class="badge badge-fileio">File I/O</span> File I/O</h3>
<h4 id="sys-open"><code>SYS_OPEN</code> (6) &mdash; Open a file</h4>
<pre><code><span class="sig">int montauk::open(const char* path);</span></code></pre>
<p>
Opens a file on the VFS. Paths use the format
<code>"&lt;device&gt;:/&lt;name&gt;"</code> (e.g.&nbsp;<code>"0:/hello.elf"</code>
for the ramdisk). Returns a non-negative handle on success, or a
negative value on error.
</p>
<h4 id="sys-read"><code>SYS_READ</code> (7) &mdash; Read from a file</h4>
<pre><code><span class="sig">int montauk::read(int handle, uint8_t* buf, uint64_t offset, uint64_t size);</span></code></pre>
<p>
Reads up to <code>size</code> bytes from the file at the given byte
<code>offset</code> into <code>buf</code>. Returns the number of bytes
actually read, or a negative value on error. Does not maintain an
implicit file position &mdash; the offset is explicit on every call.
</p>
<h4 id="sys-getsize"><code>SYS_GETSIZE</code> (8) &mdash; Get file size</h4>
<pre><code><span class="sig">uint64_t montauk::getsize(int handle);</span></code></pre>
<p>Returns the total size (in bytes) of the file associated with <code>handle</code>.</p>
<h4 id="sys-close"><code>SYS_CLOSE</code> (9) &mdash; Close a file</h4>
<pre><code><span class="sig">void montauk::close(int handle);</span></code></pre>
<p>Closes the file handle and releases associated kernel resources.</p>
<h4 id="sys-readdir"><code>SYS_READDIR</code> (10) &mdash; List directory entries</h4>
<pre><code><span class="sig">int montauk::readdir(const char* path, const char** names, int max);</span></code></pre>
<p>
Reads up to <code>max</code> directory entries from <code>path</code>.
Entry name pointers are written into the <code>names</code> array.
The kernel allocates a user-accessible page for the string data
automatically. Returns the number of entries read, or
<code>&le; 0</code> on error/empty directory. Maximum 64 entries per call.
</p>
<!-- ---- Memory ---- -->
<h3 id="cat-memory"><span class="badge badge-memory">Memory</span> Memory Management</h3>
<p>
Memory allocation has two layers: <strong>low-level syscalls</strong>
that map pages from the kernel, and a <strong>userspace heap</strong>
that provides <code>malloc</code>/<code>mfree</code> on top.
</p>
<h4>Userspace Heap (recommended)</h4>
<p class="file-path">programs/include/montauk/heap.h</p>
<p>
Include <code>&lt;montauk/heap.h&gt;</code> for a proper free-list
allocator that runs entirely in userspace. It calls
<code>SYS_ALLOC</code> internally to obtain pages and manages
sub-page allocations with a linked free list &mdash; adapted from
the kernel's own <code>HeapAllocator</code>.
</p>
<pre><code><span class="sig">void* montauk::malloc(uint64_t size);</span></code></pre>
<p>
Allocates <code>size</code> bytes from the userspace free list.
Returns a 16-byte-aligned pointer, or <code>nullptr</code> on failure.
When the free list is exhausted, it transparently requests more pages
from the kernel via <code>SYS_ALLOC</code> (minimum 16&nbsp;KiB growth).
</p>
<pre><code><span class="sig">void montauk::mfree(void* ptr);</span></code></pre>
<p>
Returns the block to the userspace free list.
<strong>No syscall is made</strong> &mdash; the memory stays mapped
and is immediately available for future <code>malloc</code> calls.
Passing <code>nullptr</code> is a safe no-op.
</p>
<pre><code><span class="sig">void* montauk::realloc(void* ptr, uint64_t size);</span></code></pre>
<p>
Resizes the allocation at <code>ptr</code> to <code>size</code> bytes.
Allocates a new block, copies the smaller of old/new sizes, and frees
the old block. If <code>ptr</code> is <code>nullptr</code>, behaves
like <code>malloc</code>.
</p>
<h4>Low-Level Page Syscalls</h4>
<h4 id="sys-alloc"><code>SYS_ALLOC</code> (11) &mdash; Map pages</h4>
<pre><code><span class="sig">void* montauk::alloc(uint64_t size);</span></code></pre>
<p>
Maps <code>size</code> bytes of zeroed physical pages into the
process's address space (starting at <code>0x40000000</code>).
The size is rounded up to the nearest page boundary (4&nbsp;KiB).
Returns a pointer to the mapped region, or <code>nullptr</code> on
failure. This is the backing primitive for
<code>montauk::malloc</code> &mdash; most programs should use the heap
API instead of calling this directly.
</p>
<h4 id="sys-free"><code>SYS_FREE</code> (12) &mdash; Unmap pages (no-op)</h4>
<pre><code><span class="sig">void montauk::free(void* ptr);</span></code></pre>
<p>
Reserved for future page-level unmapping. Currently a no-op &mdash;
pages are reclaimed when the process exits. Use
<code>montauk::mfree</code> for heap allocations.
</p>
<!-- ---- Timekeeping ---- -->
<h3 id="cat-time"><span class="badge badge-time">Time</span> Timekeeping</h3>
<h4 id="sys-getticks"><code>SYS_GETTICKS</code> (13) &mdash; Get tick count</h4>
<pre><code><span class="sig">uint64_t montauk::get_ticks();</span></code></pre>
<p>
Returns the number of APIC timer ticks since boot. The tick rate
depends on the hardware and APIC timer calibration.
</p>
<h4 id="sys-getmilliseconds"><code>SYS_GETMILLISECONDS</code> (14) &mdash; Get milliseconds since boot</h4>
<pre><code><span class="sig">uint64_t montauk::get_milliseconds();</span></code></pre>
<p>
Returns wall-clock milliseconds elapsed since boot. Useful for
calculating uptime or measuring durations.
</p>
<!-- ---- System ---- -->
<h3 id="cat-system"><span class="badge badge-system">System</span> System Information</h3>
<h4 id="sys-getinfo"><code>SYS_GETINFO</code> (15) &mdash; Get OS information</h4>
<pre><code><span class="sig">void montauk::get_info(Montauk::SysInfo* info);</span></code></pre>
<p>
Fills in a <a href="#struct-sysinfo"><code>SysInfo</code></a> structure
with the OS name, version string, API version number, and maximum
process count.
</p>
<!-- ---- Keyboard ---- -->
<h3 id="cat-keyboard"><span class="badge badge-keyboard">Keyboard</span> Keyboard Input</h3>
<h4 id="sys-iskeyavailable"><code>SYS_ISKEYAVAILABLE</code> (16) &mdash; Check for pending key</h4>
<pre><code><span class="sig">bool montauk::is_key_available();</span></code></pre>
<p>
Returns <code>true</code> if a key event is available in the PS/2
keyboard buffer. Non-blocking.
</p>
<h4 id="sys-getkey"><code>SYS_GETKEY</code> (17) &mdash; Get a key event</h4>
<pre><code><span class="sig">void montauk::getkey(Montauk::KeyEvent* out);</span></code></pre>
<p>
Fills in a <a href="#struct-keyevent"><code>KeyEvent</code></a>
structure with the next keyboard event (press or release), including
scancode, ASCII translation, and modifier state (Shift, Ctrl, Alt).
</p>
<h4 id="sys-getchar"><code>SYS_GETCHAR</code> (18) &mdash; Read a character (blocking)</h4>
<pre><code><span class="sig">char montauk::getchar();</span></code></pre>
<p>
Blocks until a printable character key-press is available, then
returns the ASCII character. This is the simplest way to read
interactive text input.
</p>
<!-- ---- Networking ---- -->
<h3 id="cat-network"><span class="badge badge-network">Network</span> Networking</h3>
<h4 id="sys-ping"><code>SYS_PING</code> (19) &mdash; Send an ICMP echo request</h4>
<pre><code><span class="sig">int32_t montauk::ping(uint32_t ip, uint32_t timeoutMs = 3000);</span></code></pre>
<p>
Sends an ICMP echo request to <code>ip</code> and waits up to
<code>timeoutMs</code> milliseconds for a reply. The IP address is in
little-endian byte order (e.g.&nbsp;<code>10.0.2.2</code> &rarr;
<code>0x0202000A</code>).
Returns the round-trip time in milliseconds on success, or
<code>-1</code> on timeout.
</p>
<!-- ---- Framebuffer ---- -->
<h3 id="cat-framebuffer"><span class="badge badge-framebuf">Framebuffer</span> Framebuffer Access</h3>
<h4 id="sys-fbinfo"><code>SYS_FBINFO</code> (21) &mdash; Get framebuffer information</h4>
<pre><code><span class="sig">void montauk::fb_info(Montauk::FbInfo* info);</span></code></pre>
<p>
Fills in a <a href="#struct-fbinfo"><code>FbInfo</code></a> structure
with the framebuffer dimensions, pitch (bytes per scanline), and
bits per pixel. Call this before <code>SYS_FBMAP</code> to learn the
framebuffer geometry.
</p>
<h4 id="sys-fbmap"><code>SYS_FBMAP</code> (22) &mdash; Map framebuffer into process memory</h4>
<pre><code><span class="sig">void* montauk::fb_map();</span></code></pre>
<p>
Maps the physical framebuffer into the calling process's address
space at <code>0x50000000</code> and returns the user virtual address.
The mapped region covers <code>height &times; pitch</code> bytes. Each
pixel is a 32-bit value in <code>0xAARRGGBB</code> format (blue in
the low byte). Writing to this memory directly updates the screen.
</p>
<div class="note">
<strong>Note:</strong> After mapping, the cursor overlay is not composited
automatically. Programs that use the framebuffer take full control of
screen output for the mapped region.
</div>
<!-- ---- Terminal ---- -->
<h3 id="cat-terminal"><span class="badge badge-system">Terminal</span> Terminal</h3>
<h4 id="sys-termsize"><code>SYS_TERMSIZE</code> (24) &mdash; Get terminal dimensions</h4>
<pre><code><span class="sig">void montauk::termsize(int* cols, int* rows);</span></code></pre>
<p>
Returns the current terminal dimensions (character grid) via the
two output pointers. Columns are packed in the low 32&nbsp;bits and
rows in the high 32&nbsp;bits of the raw return value; the typed
wrapper unpacks them for you. Either pointer may be
<code>nullptr</code> if you only need one dimension.
</p>
<!-- ================================================================= -->
<h2 id="data-structures">Data Structures</h2>
<h3 id="struct-sysinfo"><code>Montauk::SysInfo</code></h3>
<p class="file-path">programs/include/Api/Syscall.hpp</p>
<pre><code>struct SysInfo {
char osName[32]; <span style="color:#8b949e">// e.g. "MontaukOS"</span>
char osVersion[32]; <span style="color:#8b949e">// e.g. "0.1.0"</span>
uint32_t apiVersion; <span style="color:#8b949e">// Current: 2</span>
uint32_t maxProcesses; <span style="color:#8b949e">// Current: 16</span>
};</code></pre>
<h3 id="struct-fbinfo"><code>Montauk::FbInfo</code></h3>
<pre><code>struct FbInfo {
uint64_t width; <span style="color:#8b949e">// Framebuffer width in pixels</span>
uint64_t height; <span style="color:#8b949e">// Framebuffer height in pixels</span>
uint64_t pitch; <span style="color:#8b949e">// Bytes per scanline</span>
uint64_t bpp; <span style="color:#8b949e">// Bits per pixel (always 32)</span>
uint64_t userAddr; <span style="color:#8b949e">// Reserved (0 until mapped via SYS_FBMAP)</span>
};</code></pre>
<h3 id="struct-keyevent"><code>Montauk::KeyEvent</code></h3>
<pre><code>struct KeyEvent {
uint8_t scancode; <span style="color:#8b949e">// Raw PS/2 scancode</span>
char ascii; <span style="color:#8b949e">// Translated ASCII character (0 if non-printable)</span>
bool pressed; <span style="color:#8b949e">// true = key down, false = key up</span>
bool shift; <span style="color:#8b949e">// Shift modifier active</span>
bool ctrl; <span style="color:#8b949e">// Ctrl modifier active</span>
bool alt; <span style="color:#8b949e">// Alt modifier active</span>
};</code></pre>
<h3 id="struct-syscallframe"><code>Montauk::SyscallFrame</code> (kernel only)</h3>
<pre><code>struct SyscallFrame {
uint64_t r15, r14, r13, r12, rbp, rbx; <span style="color:#8b949e">// callee-saved</span>
uint64_t arg6, arg5, arg4, arg3, arg2, arg1;
uint64_t syscall_nr;
uint64_t user_rflags, user_rip, user_rsp;
};</code></pre>
<p>
This is the stack frame pushed by <code>SyscallEntry.asm</code> and
passed to <code>SyscallDispatch</code>. Userspace code never sees this
directly.
</p>
<!-- ================================================================= -->
<h2 id="shell-walkthrough">Shell Application Walkthrough</h2>
<p class="file-path">programs/src/shell/main.cpp</p>
<p>
The built-in shell is the best example of a real MontaukOS application.
It demonstrates most of the available syscalls.
</p>
<h3>Initialization</h3>
<pre><code>extern "C" void _start() {
montauk::print("\n MontaukOS Shell v0.1\n");
montauk::print(" Type 'help' for available commands.\n\n");
char line[256];
int pos = 0;
prompt();
while (true) {
char c = montauk::getchar(); <span style="color:#8b949e">// blocking read</span>
<span style="color:#8b949e">// ... handle input, echo, backspace ...</span>
}
}</code></pre>
<p>
The shell uses <code>montauk::getchar()</code> in a loop for blocking
character-by-character input, manually handling echo and backspace.
</p>
<h3>Shell Commands &amp; Syscalls Used</h3>
<table>
<tr><th>Command</th><th>Description</th><th>Syscalls Used</th></tr>
<tr>
<td><code>help</code></td>
<td>Print available commands</td>
<td><code>SYS_PRINT</code></td>
</tr>
<tr>
<td><code>info</code></td>
<td>Show OS name, version, API version</td>
<td><code>SYS_GETINFO</code>, <code>SYS_PRINT</code>, <code>SYS_PUTCHAR</code></td>
</tr>
<tr>
<td><code>man &lt;topic&gt;</code></td>
<td>Fullscreen manual page viewer</td>
<td><code>SYS_OPEN</code>, <code>SYS_GETSIZE</code>, <code>SYS_READ</code>, <code>SYS_CLOSE</code>, <code>SYS_ALLOC</code>, <code>SYS_TERMSIZE</code>, <code>SYS_GETKEY</code>, <code>SYS_PRINT</code>, <code>SYS_PUTCHAR</code></td>
</tr>
<tr>
<td><code>ls</code></td>
<td>List ramdisk files</td>
<td><code>SYS_READDIR</code>, <code>SYS_PRINT</code></td>
</tr>
<tr>
<td><code>cat &lt;file&gt;</code></td>
<td>Display file contents in 512-byte chunks</td>
<td><code>SYS_OPEN</code>, <code>SYS_GETSIZE</code>, <code>SYS_READ</code>, <code>SYS_CLOSE</code>, <code>SYS_PRINT</code></td>
</tr>
<tr>
<td><code>run &lt;file&gt;</code></td>
<td>Spawn a new process and wait for it to exit</td>
<td><code>SYS_SPAWN</code>, <code>SYS_WAITPID</code>, <code>SYS_PRINT</code></td>
</tr>
<tr>
<td><code>ping &lt;ip&gt;</code></td>
<td>Send 4 ICMP echo requests</td>
<td><code>SYS_PING</code>, <code>SYS_SLEEP_MS</code>, <code>SYS_PRINT</code>, <code>SYS_PUTCHAR</code></td>
</tr>
<tr>
<td><code>uptime</code></td>
<td>Show uptime in minutes, seconds, ms</td>
<td><code>SYS_GETMILLISECONDS</code>, <code>SYS_PRINT</code>, <code>SYS_PUTCHAR</code></td>
</tr>
<tr>
<td><code>clear</code></td>
<td>Scroll past visible content</td>
<td><code>SYS_PUTCHAR</code></td>
</tr>
<tr>
<td><code>exit</code></td>
<td>Terminate the shell</td>
<td><code>SYS_PRINT</code>, <code>SYS_EXIT</code></td>
</tr>
</table>
<h3>Pattern: Reading a File</h3>
<p>The shell's <code>cat</code> implementation shows the standard file-reading pattern:</p>
<pre><code><span style="color:#8b949e">// 1. Build VFS path</span>
char path[128];
<span style="color:#8b949e">// ... copy "0:/" + filename into path ...</span>
<span style="color:#8b949e">// 2. Open</span>
int handle = montauk::open(path);
if (handle &lt; 0) { <span style="color:#8b949e">/* error */</span> return; }
<span style="color:#8b949e">// 3. Get size</span>
uint64_t size = montauk::getsize(handle);
<span style="color:#8b949e">// 4. Read in chunks</span>
uint8_t buf[512];
uint64_t offset = 0;
while (offset &lt; size) {
uint64_t chunk = size - offset;
if (chunk &gt; sizeof(buf) - 1) chunk = sizeof(buf) - 1;
int bytesRead = montauk::read(handle, buf, offset, chunk);
if (bytesRead &lt;= 0) break;
buf[bytesRead] = '\0';
montauk::print((const char*)buf);
offset += bytesRead;
}
<span style="color:#8b949e">// 5. Close</span>
montauk::close(handle);</code></pre>
<h3>Pattern: IP Networking</h3>
<pre><code><span style="color:#8b949e">// Parse "10.0.2.2" into uint32_t in little-endian order</span>
uint32_t ip;
parse_ip("10.0.2.2", &amp;ip); <span style="color:#8b949e">// ip = 0x0202000A</span>
<span style="color:#8b949e">// Send 4 pings with 1-second intervals</span>
for (int i = 0; i &lt; 4; i++) {
int32_t rtt = montauk::ping(ip, 3000);
if (rtt &lt; 0) { <span style="color:#8b949e">/* timeout */</span> }
else { <span style="color:#8b949e">/* reply in rtt ms */</span> }
if (i &lt; 3) montauk::sleep_ms(1000);
}</code></pre>
<!-- ================================================================= -->
<h2 id="adding-syscalls">Adding New Syscalls</h2>
<p>
Adding a syscall requires changes in <strong>3 files</strong>
(kernel-side) and <strong>2 files</strong> (userspace-side). Follow
these steps:
</p>
<h3>Step 1: Assign a Syscall Number</h3>
<p>
Add a new constant in <strong>both</strong> copies of the syscall
number definitions. The numbers must match exactly.
</p>
<p class="file-path">kernel/src/Api/Syscall.hpp</p>
<p class="file-path">programs/include/Api/Syscall.hpp</p>
<pre><code>static constexpr uint64_t SYS_MYFUNC = 26; <span style="color:#8b949e">// next available number</span></code></pre>
<div class="warn">
<strong>Keep both files in sync.</strong> The kernel and userspace
headers define syscall numbers independently. If they disagree, the
wrong handler runs.
</div>
<h3>Step 2: Implement the Kernel Handler</h3>
<p class="file-path">kernel/src/Api/Syscall.cpp</p>
<p>Add a static function implementing your syscall's logic:</p>
<pre><code>static int64_t Sys_MyFunc(uint64_t arg1, const char* arg2) {
<span style="color:#8b949e">// Your kernel-side implementation here.</span>
<span style="color:#8b949e">// You have full access to kernel subsystems.</span>
return 0;
}</code></pre>
<h3>Step 3: Add the Dispatch Case</h3>
<p class="file-path">kernel/src/Api/Syscall.cpp &mdash; <code>SyscallDispatch()</code></p>
<pre><code>case SYS_MYFUNC:
return (int64_t)Sys_MyFunc(frame-&gt;arg1, (const char*)frame-&gt;arg2);</code></pre>
<p>
Arguments are accessed through <code>frame-&gt;arg1</code> through
<code>frame-&gt;arg6</code>, corresponding to RDI, RSI, RDX, R10, R8,
R9 respectively.
</p>
<h3>Step 4: Add a Typed Userspace Wrapper</h3>
<p class="file-path">programs/include/montauk/syscall.h</p>
<pre><code>inline int64_t my_func(uint64_t arg1, const char* arg2) {
return syscall2(Montauk::SYS_MYFUNC, arg1, (uint64_t)arg2);
}</code></pre>
<p>
Choose the appropriate <code>syscallN</code> variant based on the
number of arguments. Cast pointer types to <code>uint64_t</code>.
</p>
<h3>Step 5: Update the Log Message (optional)</h3>
<p class="file-path">kernel/src/Api/Syscall.cpp &mdash; <code>InitializeSyscalls()</code></p>
<p>
Update the boot log to reflect the new syscall count:
</p>
<pre><code><span style="color:#8b949e">// Change "26 syscalls" to "27 syscalls"</span>
Kt::KernelLogStream(Kt::OK, "Syscall") &lt;&lt; "SYSCALL/SYSRET initialized (LSTAR="
&lt;&lt; kcp::hex &lt;&lt; (uint64_t)SyscallEntry &lt;&lt; kcp::dec &lt;&lt; ", 27 syscalls)";</code></pre>
<h3>Complete Checklist</h3>
<table>
<tr><th>#</th><th>File</th><th>Change</th></tr>
<tr><td>1</td><td><code>kernel/src/Api/Syscall.hpp</code></td><td>Add <code>SYS_MYFUNC</code> constant (+ any new structs)</td></tr>
<tr><td>2</td><td><code>kernel/src/Api/Syscall.cpp</code></td><td>Add <code>Sys_MyFunc()</code> implementation + dispatch case</td></tr>
<tr><td>3</td><td><code>programs/include/Api/Syscall.hpp</code></td><td>Add matching <code>SYS_MYFUNC</code> constant (+ any new structs)</td></tr>
<tr><td>4</td><td><code>programs/include/montauk/syscall.h</code></td><td>Add typed wrapper in <code>montauk::</code> namespace</td></tr>
<tr><td>5</td><td><em>(optional)</em></td><td>Update syscall count in boot log</td></tr>
</table>
<!-- ================================================================= -->
<h2 id="memory-model">Process Memory Model</h2>
<p>
Each process gets its own PML4 page table. The kernel half
(upper&nbsp;256 entries) is shared; the lower half is per-process.
</p>
<table>
<tr><th>Region</th><th>Virtual Address</th><th>Size</th><th>Purpose</th></tr>
<tr><td>Exit stub</td><td><code>0x3FF000</code></td><td>4 KiB</td><td>Auto-exit trampoline (calls <code>SYS_EXIT(0)</code> if <code>_start</code> returns)</td></tr>
<tr><td>Program code</td><td><code>0x400000</code>+</td><td>Varies</td><td>ELF <code>.text</code>, <code>.rodata</code>, <code>.data</code>, <code>.bss</code></td></tr>
<tr><td>User heap</td><td><code>0x40000000</code>+</td><td>Grows up</td><td>Page pool (<code>SYS_ALLOC</code>); managed by userspace free-list heap</td></tr>
<tr><td>Framebuffer</td><td><code>0x50000000</code>+</td><td>height &times; pitch</td><td>Mapped by <code>SYS_FBMAP</code>; direct pixel access (32-bit ARGB)</td></tr>
<tr><td>User stack</td><td><code>0x7FFFFEF000</code>&ndash;<code>0x7FFFFFF000</code></td><td>16 KiB (4 pages)</td><td>Grows down from <code>0x7FFFFFF000</code></td></tr>
</table>
<p>
The ELF loader maps PT_LOAD segments with user-accessible page flags.
BSS is zero-initialized automatically (pages are allocated zeroed).
</p>
<!-- ================================================================= -->
<h2 id="limitations">Current Limitations</h2>
<ul>
<li><strong>No standard library (C++)</strong> &mdash; there is no libc++ for C++ programs.
String operations, integer formatting, etc. must be written manually (see the shell for
examples). A minimal C standard library exists for C programs (used by the DOOM port) in
<code>programs/include/libc/</code> with headers like <code>stdio.h</code>,
<code>stdlib.h</code>, and <code>string.h</code>.</li>
<li><strong>No dynamic linking</strong> &mdash; all programs are statically linked.</li>
<li><strong>No argc/argv</strong> &mdash; <code>_start</code> receives no
<code>argc</code>/<code>argv</code>. Use <code>montauk::getargs()</code>
to retrieve a single argument string (max 255 chars) passed via
<code>montauk::spawn()</code>.</li>
<li><strong>No page-level <code>free()</code></strong> &mdash;
<code>SYS_FREE</code> is a no-op; pages mapped via <code>SYS_ALLOC</code>
are only reclaimed when the process exits. However, the userspace heap
(<code>montauk::malloc</code>/<code>montauk::mfree</code>) does reuse
memory via a free list.</li>
<li><strong>16 process slots</strong> &mdash; the scheduler supports a
maximum of 16 concurrent processes.</li>
<li><strong>No writable file system</strong> &mdash; the VFS is read-only
(backed by the boot ramdisk).</li>
<li><strong>Shared console</strong> &mdash; all processes write to the
same terminal; there is no per-process stdout.</li>
<li><strong>Shared keyboard</strong> &mdash; keyboard input is global;
there is no input focus or multiplexing.</li>
<li><strong>10&nbsp;ms scheduling granularity</strong> &mdash; preemptive
round-robin with a fixed 10&nbsp;ms time slice.</li>
</ul>
<hr>
<p style="color:var(--fg-muted); font-size:.85rem; margin-top:2rem;">
MontaukOS Documentation &mdash; Copyright &copy; 2025-2026 Daniel Hammer
</p>
</main>
</body>
</html>