From 605fbcbe42e5cdaa7e40d7b480e9da22ce440122 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Tue, 17 Feb 2026 19:17:01 +0100 Subject: [PATCH] feat: scheduling, usermode, shell --- GNUmakefile | 14 +- kernel/src/Api/Syscall.cpp | 280 +++++++++++++++++++++ kernel/src/Api/Syscall.hpp | 62 +++++ kernel/src/Drivers/PS2/PS2Controller.cpp | 2 +- kernel/src/Fs/Ramdisk.cpp | 242 ++++++++++++++++++ kernel/src/Fs/Ramdisk.hpp | 33 +++ kernel/src/Fs/Vfs.cpp | 143 +++++++++++ kernel/src/Fs/Vfs.hpp | 33 +++ kernel/src/Hal/GDT.asm | 8 +- kernel/src/Hal/GDT.cpp | 61 +++-- kernel/src/Hal/GDT.hpp | 46 +++- kernel/src/Hal/MSR.hpp | 30 +++ kernel/src/Hal/SyscallEntry.asm | 89 +++++++ kernel/src/Main.cpp | 70 ++++-- kernel/src/Memory/Heap.cpp | 16 +- kernel/src/Memory/Paging.cpp | 106 +++++++- kernel/src/Memory/Paging.hpp | 19 +- kernel/src/Net/Arp.cpp | 4 +- kernel/src/Net/Icmp.cpp | 34 +++ kernel/src/Net/Icmp.hpp | 9 + kernel/src/Net/Ipv4.cpp | 88 +++++-- kernel/src/Net/Ipv4.hpp | 7 +- kernel/src/Platform/Limine.hpp | 9 + kernel/src/Sched/Context.asm | 45 ++++ kernel/src/Sched/ElfLoader.cpp | 154 ++++++++++++ kernel/src/Sched/ElfLoader.hpp | 49 ++++ kernel/src/Sched/Scheduler.cpp | 308 +++++++++++++++++++++++ kernel/src/Sched/Scheduler.hpp | 59 +++++ kernel/src/Terminal/Terminal.cpp | 8 +- kernel/src/Timekeeping/ApicTimer.cpp | 15 +- kernel/src/Timekeeping/ApicTimer.hpp | 5 +- limine.conf | 4 + programs/GNUmakefile | 74 ++++++ programs/bin/hello.elf | Bin 0 -> 12360 bytes programs/bin/shell.elf | Bin 0 -> 38744 bytes programs/include/Api/Syscall.hpp | 51 ++++ programs/include/zenith/syscall.h | 157 ++++++++++++ programs/link.ld | 43 ++++ programs/obj/hello/main.o | Bin 0 -> 8416 bytes programs/obj/shell/main.o | Bin 0 -> 74968 bytes programs/src/hello/main.cpp | 18 ++ programs/src/shell/main.cpp | 298 ++++++++++++++++++++++ ramdisk.tar | Bin 0 -> 61440 bytes ramdisk/test.txt | 0 scripts/mkramdisk.sh | 20 ++ scripts/net-setup.sh | 7 + 46 files changed, 2622 insertions(+), 98 deletions(-) create mode 100644 kernel/src/Api/Syscall.cpp create mode 100644 kernel/src/Api/Syscall.hpp create mode 100644 kernel/src/Fs/Ramdisk.cpp create mode 100644 kernel/src/Fs/Ramdisk.hpp create mode 100644 kernel/src/Fs/Vfs.cpp create mode 100644 kernel/src/Fs/Vfs.hpp create mode 100644 kernel/src/Hal/MSR.hpp create mode 100644 kernel/src/Hal/SyscallEntry.asm create mode 100644 kernel/src/Sched/Context.asm create mode 100644 kernel/src/Sched/ElfLoader.cpp create mode 100644 kernel/src/Sched/ElfLoader.hpp create mode 100644 kernel/src/Sched/Scheduler.cpp create mode 100644 kernel/src/Sched/Scheduler.hpp create mode 100644 programs/GNUmakefile create mode 100755 programs/bin/hello.elf create mode 100755 programs/bin/shell.elf create mode 100644 programs/include/Api/Syscall.hpp create mode 100644 programs/include/zenith/syscall.h create mode 100644 programs/link.ld create mode 100644 programs/obj/hello/main.o create mode 100644 programs/obj/shell/main.o create mode 100644 programs/src/hello/main.cpp create mode 100644 programs/src/shell/main.cpp create mode 100644 ramdisk.tar create mode 100644 ramdisk/test.txt create mode 100755 scripts/mkramdisk.sh diff --git a/GNUmakefile b/GNUmakefile index 9f71b6c..b5d715b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -173,10 +173,16 @@ kernel-deps: kernel: kernel-deps $(MAKE) -C kernel -$(IMAGE_NAME).iso: limine/limine kernel + +.PHONY: ramdisk +ramdisk: programs + ./scripts/mkramdisk.sh programs/bin ramdisk.tar + +$(IMAGE_NAME).iso: limine/limine kernel ramdisk rm -rf iso_root mkdir -p iso_root/boot cp -v kernel/bin-$(ARCH)/kernel iso_root/boot/ + cp -v ramdisk.tar iso_root/boot/ mkdir -p iso_root/boot/limine cp -v limine.conf iso_root/boot/limine/ mkdir -p iso_root/EFI/BOOT @@ -220,7 +226,7 @@ ifeq ($(ARCH),loongarch64) endif rm -rf iso_root -$(IMAGE_NAME).hdd: limine/limine kernel +$(IMAGE_NAME).hdd: limine/limine kernel ramdisk rm -f $(IMAGE_NAME).hdd dd if=/dev/zero bs=1M count=0 seek=64 of=$(IMAGE_NAME).hdd PATH=$$PATH:/usr/sbin:/sbin sgdisk $(IMAGE_NAME).hdd -n 1:2048 -t 1:ef00 @@ -230,6 +236,7 @@ endif mformat -i $(IMAGE_NAME).hdd@@1M mmd -i $(IMAGE_NAME).hdd@@1M ::/EFI ::/EFI/BOOT ::/boot ::/boot/limine mcopy -i $(IMAGE_NAME).hdd@@1M kernel/bin-$(ARCH)/kernel ::/boot + mcopy -i $(IMAGE_NAME).hdd@@1M ramdisk.tar ::/boot mcopy -i $(IMAGE_NAME).hdd@@1M limine.conf ::/boot/limine ifeq ($(ARCH),x86_64) mcopy -i $(IMAGE_NAME).hdd@@1M limine/limine-bios.sys ::/boot/limine @@ -249,7 +256,8 @@ endif .PHONY: clean clean: $(MAKE) -C kernel clean - rm -rf iso_root $(IMAGE_NAME).iso $(IMAGE_NAME).hdd +# $(MAKE) -C programs clean + rm -rf iso_root $(IMAGE_NAME).iso $(IMAGE_NAME).hdd ramdisk.tar .PHONY: distclean distclean: diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp new file mode 100644 index 0000000..a6522b6 --- /dev/null +++ b/kernel/src/Api/Syscall.cpp @@ -0,0 +1,280 @@ +/* + * Syscall.cpp + * SYSCALL/SYSRET setup and number-based dispatch + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "Syscall.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Assembly entry point +extern "C" void SyscallEntry(); + +namespace Zenith { + + // ---- Syscall implementations ---- + + static void Sys_Exit(int exitCode) { + (void)exitCode; + Sched::ExitProcess(); + } + + static void Sys_Yield() { + Sched::Schedule(); + } + + static void Sys_SleepMs(uint64_t ms) { + Timekeeping::Sleep(ms); + } + + static int Sys_GetPid() { + return Sched::GetCurrentPid(); + } + + static void Sys_Print(const char* text) { + Kt::Print(text); + } + + static void Sys_Putchar(char c) { + Kt::Putchar(c); + } + + static int Sys_Open(const char* path) { + return Fs::Vfs::VfsOpen(path); + } + + static int Sys_Read(int handle, uint8_t* buffer, uint64_t offset, uint64_t size) { + return Fs::Vfs::VfsRead(handle, buffer, offset, size); + } + + static uint64_t Sys_GetSize(int handle) { + return Fs::Vfs::VfsGetSize(handle); + } + + static void Sys_Close(int handle) { + Fs::Vfs::VfsClose(handle); + } + + static int Sys_ReadDir(const char* path, const char** outNames, int maxEntries) { + // Get entries from VFS into a kernel-local array + const char* kernelNames[64]; + int max = maxEntries; + if (max > 64) max = 64; + int count = Fs::Vfs::VfsReadDir(path, kernelNames, max); + if (count <= 0) return count; + + // Allocate a user-accessible page for string data via process heap + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc == nullptr) return -1; + + void* page = Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) return -1; + uint64_t physAddr = Memory::SubHHDM((uint64_t)page); + uint64_t userVa = proc->heapNext; + proc->heapNext += 0x1000; + Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, userVa); + + // Copy strings into the user page and write pointers to outNames + uint64_t offset = 0; + uint8_t* pageBuf = (uint8_t*)Memory::HHDM(physAddr); + int copied = 0; + for (int i = 0; i < count; i++) { + int len = Lib::strlen(kernelNames[i]) + 1; + if (offset + len > 0x1000) break; + memcpy(pageBuf + offset, kernelNames[i], len); + outNames[i] = (const char*)(userVa + offset); + offset += len; + copied++; + } + + return copied; + } + + static uint64_t Sys_Alloc(uint64_t size) { + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc == nullptr) return 0; + + // Round up to page boundary + size = (size + 0xFFF) & ~0xFFFULL; + if (size == 0) size = 0x1000; + + uint64_t userVa = proc->heapNext; + uint64_t numPages = size / 0x1000; + + for (uint64_t i = 0; i < numPages; i++) { + void* page = Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) return 0; + uint64_t physAddr = Memory::SubHHDM((uint64_t)page); + Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, userVa + i * 0x1000); + } + + proc->heapNext += size; + return userVa; + } + + static void Sys_Free(uint64_t) { + // No-op for now (pages leak). Proper freeing can come later. + } + + static uint64_t Sys_GetTicks() { + return Timekeeping::GetTicks(); + } + + static uint64_t Sys_GetMilliseconds() { + return Timekeeping::GetMilliseconds(); + } + + static void Sys_GetInfo(SysInfo* outInfo) { + if (outInfo == nullptr) return; + + // Copy strings into fixed-size arrays (user-accessible) + const char* name = "ZenithOS"; + const char* ver = "0.1.0"; + for (int i = 0; name[i]; i++) outInfo->osName[i] = name[i]; + outInfo->osName[8] = '\0'; + for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i]; + outInfo->osVersion[5] = '\0'; + + outInfo->apiVersion = 2; + outInfo->maxProcesses = Sched::MaxProcesses; + } + + static bool Sys_IsKeyAvailable() { + return Drivers::PS2::Keyboard::IsKeyAvailable(); + } + + static void Sys_GetKey(KeyEvent* outEvent) { + if (outEvent == nullptr) return; + auto k = Drivers::PS2::Keyboard::GetKey(); + outEvent->scancode = k.Scancode; + outEvent->ascii = k.Ascii; + outEvent->pressed = k.Pressed; + outEvent->shift = k.Shift; + outEvent->ctrl = k.Ctrl; + outEvent->alt = k.Alt; + } + + static char Sys_GetChar() { + return Drivers::PS2::Keyboard::GetChar(); + } + + static uint16_t g_pingSeq = 0; + static constexpr uint16_t PING_ID = 0x2E01; // "ZE" + + static int32_t Sys_Ping(uint32_t ipAddr, uint32_t timeoutMs) { + uint16_t seq = g_pingSeq++; + + Net::Icmp::ResetReply(); + Net::Icmp::SendEchoRequest(ipAddr, PING_ID, seq); + + uint64_t start = Timekeeping::GetMilliseconds(); + while (!Net::Icmp::HasReply(PING_ID, seq)) { + if (Timekeeping::GetMilliseconds() - start >= timeoutMs) { + return -1; + } + Sched::Schedule(); + } + + return (int32_t)(Timekeeping::GetMilliseconds() - start); + } + + // ---- Dispatch ---- + + extern "C" int64_t SyscallDispatch(SyscallFrame* frame) { + switch (frame->syscall_nr) { + case SYS_EXIT: + Sys_Exit((int)frame->arg1); + return 0; + case SYS_YIELD: + Sys_Yield(); + return 0; + case SYS_SLEEP_MS: + Sys_SleepMs(frame->arg1); + return 0; + case SYS_GETPID: + return (int64_t)Sys_GetPid(); + case SYS_PRINT: + Sys_Print((const char*)frame->arg1); + return 0; + case SYS_PUTCHAR: + Sys_Putchar((char)frame->arg1); + return 0; + case SYS_OPEN: + return (int64_t)Sys_Open((const char*)frame->arg1); + case SYS_READ: + return (int64_t)Sys_Read((int)frame->arg1, (uint8_t*)frame->arg2, + frame->arg3, frame->arg4); + case SYS_GETSIZE: + return (int64_t)Sys_GetSize((int)frame->arg1); + case SYS_CLOSE: + Sys_Close((int)frame->arg1); + return 0; + case SYS_READDIR: + return (int64_t)Sys_ReadDir((const char*)frame->arg1, + (const char**)frame->arg2, + (int)frame->arg3); + case SYS_ALLOC: + return (int64_t)Sys_Alloc(frame->arg1); + case SYS_FREE: + Sys_Free(frame->arg1); + return 0; + case SYS_GETTICKS: + return (int64_t)Sys_GetTicks(); + case SYS_GETMILLISECONDS: + return (int64_t)Sys_GetMilliseconds(); + case SYS_GETINFO: + Sys_GetInfo((SysInfo*)frame->arg1); + return 0; + case SYS_ISKEYAVAILABLE: + return (int64_t)Sys_IsKeyAvailable(); + case SYS_GETKEY: + Sys_GetKey((KeyEvent*)frame->arg1); + return 0; + case SYS_GETCHAR: + return (int64_t)Sys_GetChar(); + case SYS_PING: + return (int64_t)Sys_Ping((uint32_t)frame->arg1, (uint32_t)frame->arg2); + default: + return -1; + } + } + + // ---- SYSCALL MSR initialization ---- + + void InitializeSyscalls() { + // Enable SYSCALL/SYSRET in EFER + uint64_t efer = Hal::ReadMSR(Hal::IA32_EFER); + efer |= 1; // SCE bit (Syscall Enable) + Hal::WriteMSR(Hal::IA32_EFER, efer); + + // STAR: kernel CS in [47:32], sysret base in [63:48] + // SYSCALL: CS=0x08, SS=0x10 + // SYSRET: CS=0x10+16=0x20|RPL3=0x23, SS=0x10+8=0x18|RPL3=0x1B + uint64_t star = (0x0010ULL << 48) | (0x0008ULL << 32); + Hal::WriteMSR(Hal::IA32_STAR, star); + + // LSTAR: SYSCALL entry point + Hal::WriteMSR(Hal::IA32_LSTAR, (uint64_t)SyscallEntry); + + // FMASK: mask IF on SYSCALL entry (bit 9 = IF) + Hal::WriteMSR(Hal::IA32_FMASK, 0x200); + + Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR=" + << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 20 syscalls)"; + } + +} diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp new file mode 100644 index 0000000..59c4c96 --- /dev/null +++ b/kernel/src/Api/Syscall.hpp @@ -0,0 +1,62 @@ +/* + * Syscall.hpp + * ZenithOS syscall definitions -- shared between kernel and programs + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Zenith { + + // Syscall numbers + static constexpr uint64_t SYS_EXIT = 0; + static constexpr uint64_t SYS_YIELD = 1; + static constexpr uint64_t SYS_SLEEP_MS = 2; + static constexpr uint64_t SYS_GETPID = 3; + static constexpr uint64_t SYS_PRINT = 4; + static constexpr uint64_t SYS_PUTCHAR = 5; + static constexpr uint64_t SYS_OPEN = 6; + static constexpr uint64_t SYS_READ = 7; + static constexpr uint64_t SYS_GETSIZE = 8; + static constexpr uint64_t SYS_CLOSE = 9; + static constexpr uint64_t SYS_READDIR = 10; + static constexpr uint64_t SYS_ALLOC = 11; + static constexpr uint64_t SYS_FREE = 12; + static constexpr uint64_t SYS_GETTICKS = 13; + static constexpr uint64_t SYS_GETMILLISECONDS = 14; + static constexpr uint64_t SYS_GETINFO = 15; + static constexpr uint64_t SYS_ISKEYAVAILABLE = 16; + static constexpr uint64_t SYS_GETKEY = 17; + static constexpr uint64_t SYS_GETCHAR = 18; + static constexpr uint64_t SYS_PING = 19; + + struct SysInfo { + char osName[32]; + char osVersion[32]; + uint32_t apiVersion; + uint32_t maxProcesses; + }; + + struct KeyEvent { + uint8_t scancode; + char ascii; + bool pressed; + bool shift; + bool ctrl; + bool alt; + }; + + // Stack frame pushed by SyscallEntry.asm + 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; + }; + + // Kernel-only: set up SYSCALL MSRs and initialize dispatch + void InitializeSyscalls(); + +} diff --git a/kernel/src/Drivers/PS2/PS2Controller.cpp b/kernel/src/Drivers/PS2/PS2Controller.cpp index 4ab014a..34ef415 100644 --- a/kernel/src/Drivers/PS2/PS2Controller.cpp +++ b/kernel/src/Drivers/PS2/PS2Controller.cpp @@ -153,7 +153,7 @@ namespace Drivers::PS2 { SendCommand(CmdReadConfig); config = ReadData(); - config |= ConfigPort1Interrupt; + config |= ConfigPort1Interrupt | ConfigPort1Translation; if (g_DualChannel) { config |= ConfigPort2Interrupt; } diff --git a/kernel/src/Fs/Ramdisk.cpp b/kernel/src/Fs/Ramdisk.cpp new file mode 100644 index 0000000..bcff151 --- /dev/null +++ b/kernel/src/Fs/Ramdisk.cpp @@ -0,0 +1,242 @@ +/* + * Ramdisk.cpp + * USTAR tar-based ramdisk filesystem backed by Limine modules + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "Ramdisk.hpp" +#include +#include +#include + +namespace Fs::Ramdisk { + + static FileEntry fileTable[MaxFiles]; + static int fileCount = 0; + + static uint64_t OctalToUint(const char* str, int len) { + uint64_t result = 0; + for (int i = 0; i < len && str[i] != '\0' && str[i] != ' '; i++) { + result = result * 8 + (str[i] - '0'); + } + return result; + } + + static bool StrEqual(const char* a, const char* b) { + while (*a && *b) { + if (*a != *b) return false; + a++; + b++; + } + return *a == *b; + } + + static int StrLen(const char* s) { + int n = 0; + while (s[n]) n++; + return n; + } + + static bool StartsWith(const char* str, const char* prefix) { + while (*prefix) { + if (*str != *prefix) return false; + str++; + prefix++; + } + return true; + } + + void Initialize(void* moduleData, uint64_t moduleSize) { + Kt::KernelLogStream(Kt::OK, "Ramdisk") << "Parsing USTAR archive (" << moduleSize << " bytes)"; + + uint8_t* ptr = (uint8_t*)moduleData; + uint8_t* end = ptr + moduleSize; + fileCount = 0; + + while (ptr + 512 <= end && fileCount < MaxFiles) { + // Check for end-of-archive (two consecutive zero blocks) + bool allZero = true; + for (int i = 0; i < 512; i++) { + if (ptr[i] != 0) { + allZero = false; + break; + } + } + if (allZero) break; + + // Verify USTAR magic at offset 257 + const char* magic = (const char*)(ptr + 257); + if (magic[0] != 'u' || magic[1] != 's' || magic[2] != 't' || + magic[3] != 'a' || magic[4] != 'r') { + Kt::KernelLogStream(Kt::WARNING, "Ramdisk") << "Invalid USTAR magic, stopping parse"; + break; + } + + // File name at offset 0 (100 bytes) + const char* name = (const char*)ptr; + // File size at offset 124 (12 bytes, octal ASCII) + uint64_t size = OctalToUint((const char*)(ptr + 124), 12); + // Type flag at offset 156 + char typeFlag = (char)ptr[156]; + + FileEntry& entry = fileTable[fileCount]; + + // Copy name + int nameLen = 0; + while (nameLen < MaxNameLen - 1 && name[nameLen] != '\0') { + entry.name[nameLen] = name[nameLen]; + nameLen++; + } + entry.name[nameLen] = '\0'; + + // Strip leading "./" if present + if (entry.name[0] == '.' && entry.name[1] == '/') { + char temp[MaxNameLen]; + int srcIdx = 2; + int dstIdx = 0; + while (entry.name[srcIdx] && dstIdx < MaxNameLen - 1) { + temp[dstIdx++] = entry.name[srcIdx++]; + } + temp[dstIdx] = '\0'; + for (int i = 0; i <= dstIdx; i++) { + entry.name[i] = temp[i]; + } + } + + entry.isDirectory = (typeFlag == '5'); + entry.size = size; + + // Data starts at next 512-byte block + entry.data = ptr + 512; + + // Skip entries that are just the root "." or empty name + if (entry.name[0] == '\0' || (entry.name[0] == '.' && entry.name[1] == '\0')) { + // Advance past header + data blocks + uint64_t dataBlocks = (size + 511) / 512; + ptr += 512 + dataBlocks * 512; + continue; + } + + Kt::KernelLogStream(Kt::INFO, "Ramdisk") << " " << entry.name + << " (" << entry.size << " bytes" + << (entry.isDirectory ? ", dir" : "") << ")"; + + fileCount++; + + // Advance past header + data (rounded up to 512-byte blocks) + uint64_t dataBlocks = (size + 511) / 512; + ptr += 512 + dataBlocks * 512; + } + + Kt::KernelLogStream(Kt::OK, "Ramdisk") << "Loaded " << fileCount << " entries"; + } + + int Open(const char* path) { + // Normalize: skip leading '/' + if (path[0] == '/') path++; + + for (int i = 0; i < fileCount; i++) { + if (StrEqual(fileTable[i].name, path)) { + return i; + } + // Also try matching with trailing slash stripped from table entry + int entryLen = StrLen(fileTable[i].name); + if (entryLen > 0 && fileTable[i].name[entryLen - 1] == '/') { + // Compare without trailing slash + bool match = true; + int pathLen = StrLen(path); + if (pathLen == entryLen - 1) { + for (int j = 0; j < pathLen; j++) { + if (path[j] != fileTable[i].name[j]) { + match = false; + break; + } + } + if (match) return i; + } + } + } + return -1; + } + + int Read(int handle, uint8_t* buffer, uint64_t offset, uint64_t size) { + if (handle < 0 || handle >= fileCount) return -1; + + const FileEntry& entry = fileTable[handle]; + if (offset >= entry.size) return 0; + + uint64_t bytesToRead = size; + if (offset + bytesToRead > entry.size) { + bytesToRead = entry.size - offset; + } + + memcpy(buffer, entry.data + offset, bytesToRead); + return (int)bytesToRead; + } + + uint64_t GetSize(int handle) { + if (handle < 0 || handle >= fileCount) return 0; + return fileTable[handle].size; + } + + void Close(int handle) { + // No-op for ramdisk: files are memory-mapped and read-only + (void)handle; + } + + int ReadDir(const char* path, const char** outNames, int maxEntries) { + // Normalize path: skip leading '/' + if (path[0] == '/') path++; + + int pathLen = StrLen(path); + int count = 0; + + for (int i = 0; i < fileCount && count < maxEntries; i++) { + const char* entryName = fileTable[i].name; + + if (pathLen == 0) { + // Root directory: find entries without '/' in them (or only trailing '/') + bool hasSlash = false; + int entryLen = StrLen(entryName); + for (int j = 0; j < entryLen; j++) { + if (entryName[j] == '/' && j < entryLen - 1) { + hasSlash = true; + break; + } + } + if (!hasSlash) { + outNames[count++] = entryName; + } + } else { + // Subdirectory: match entries starting with "path/" + // and that are direct children (no additional '/' beyond the prefix) + if (!StartsWith(entryName, path)) continue; + + // Check that path prefix is followed by '/' + char separator = entryName[pathLen]; + if (separator != '/') continue; + + // Check it's a direct child (no more '/' except trailing) + const char* rest = entryName + pathLen + 1; + int restLen = StrLen(rest); + bool hasDeepSlash = false; + for (int j = 0; j < restLen; j++) { + if (rest[j] == '/' && j < restLen - 1) { + hasDeepSlash = true; + break; + } + } + if (!hasDeepSlash && restLen > 0) { + outNames[count++] = entryName; + } + } + } + + return count; + } + + int GetFileCount() { + return fileCount; + } + +} diff --git a/kernel/src/Fs/Ramdisk.hpp b/kernel/src/Fs/Ramdisk.hpp new file mode 100644 index 0000000..d68de54 --- /dev/null +++ b/kernel/src/Fs/Ramdisk.hpp @@ -0,0 +1,33 @@ +/* + * Ramdisk.hpp + * USTAR tar-based ramdisk filesystem backed by Limine modules + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Fs::Ramdisk { + + static constexpr int MaxFiles = 128; + static constexpr int MaxNameLen = 100; + + struct FileEntry { + char name[MaxNameLen]; + uint8_t* data; + uint64_t size; + bool isDirectory; + }; + + void Initialize(void* moduleData, uint64_t moduleSize); + + int Open(const char* path); + int Read(int handle, uint8_t* buffer, uint64_t offset, uint64_t size); + uint64_t GetSize(int handle); + void Close(int handle); + + int ReadDir(const char* path, const char** outNames, int maxEntries); + int GetFileCount(); + +} diff --git a/kernel/src/Fs/Vfs.cpp b/kernel/src/Fs/Vfs.cpp new file mode 100644 index 0000000..7fd0f07 --- /dev/null +++ b/kernel/src/Fs/Vfs.cpp @@ -0,0 +1,143 @@ +/* + * Vfs.cpp + * Virtual File System with numerical logical drive identifiers + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "Vfs.hpp" +#include + +namespace Fs::Vfs { + + struct HandleEntry { + bool inUse; + int driveNumber; + int localHandle; + }; + + static FsDriver* driveTable[MaxDrives]; + static HandleEntry handleTable[MaxHandles]; + + // Parse "N:/path" into drive number and local path. + // Returns true on success, sets outDrive and outPath. + static bool ParsePath(const char* path, int& outDrive, const char*& outPath) { + if (path == nullptr) return false; + + // Parse decimal drive number before ':' + int drive = 0; + int i = 0; + bool hasDigit = false; + + while (path[i] >= '0' && path[i] <= '9') { + drive = drive * 10 + (path[i] - '0'); + hasDigit = true; + i++; + } + + if (!hasDigit) return false; + if (path[i] != ':') return false; + + // Everything after "N:" is the local path + outDrive = drive; + outPath = &path[i + 1]; + return true; + } + + static int AllocHandle() { + for (int i = 0; i < MaxHandles; i++) { + if (!handleTable[i].inUse) return i; + } + return -1; + } + + void Initialize() { + for (int i = 0; i < MaxDrives; i++) { + driveTable[i] = nullptr; + } + for (int i = 0; i < MaxHandles; i++) { + handleTable[i].inUse = false; + } + + Kt::KernelLogStream(Kt::OK, "VFS") << "Initialized (" << MaxDrives << " drives, " << MaxHandles << " handles)"; + } + + int RegisterDrive(int driveNumber, FsDriver* driver) { + if (driveNumber < 0 || driveNumber >= MaxDrives) return -1; + if (driver == nullptr) return -1; + + driveTable[driveNumber] = driver; + Kt::KernelLogStream(Kt::OK, "VFS") << "Registered drive " << driveNumber; + return 0; + } + + int VfsOpen(const char* path) { + int drive; + const char* localPath; + + if (!ParsePath(path, drive, localPath)) { + Kt::KernelLogStream(Kt::ERROR, "VFS") << "Invalid path format"; + return -1; + } + + if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) { + Kt::KernelLogStream(Kt::ERROR, "VFS") << "Drive " << drive << " not registered"; + return -1; + } + + int localHandle = driveTable[drive]->Open(localPath); + if (localHandle < 0) return -1; + + int globalHandle = AllocHandle(); + if (globalHandle < 0) { + driveTable[drive]->Close(localHandle); + Kt::KernelLogStream(Kt::ERROR, "VFS") << "No free handles"; + return -1; + } + + handleTable[globalHandle].inUse = true; + handleTable[globalHandle].driveNumber = drive; + handleTable[globalHandle].localHandle = localHandle; + + return globalHandle; + } + + int VfsRead(int handle, uint8_t* buffer, uint64_t offset, uint64_t size) { + if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return -1; + + HandleEntry& entry = handleTable[handle]; + return driveTable[entry.driveNumber]->Read(entry.localHandle, buffer, offset, size); + } + + uint64_t VfsGetSize(int handle) { + if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return 0; + + HandleEntry& entry = handleTable[handle]; + return driveTable[entry.driveNumber]->GetSize(entry.localHandle); + } + + void VfsClose(int handle) { + if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return; + + HandleEntry& entry = handleTable[handle]; + driveTable[entry.driveNumber]->Close(entry.localHandle); + entry.inUse = false; + } + + int VfsReadDir(const char* path, const char** outNames, int maxEntries) { + int drive; + const char* localPath; + + if (!ParsePath(path, drive, localPath)) { + Kt::KernelLogStream(Kt::ERROR, "VFS") << "Invalid path format for ReadDir"; + return -1; + } + + if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) { + Kt::KernelLogStream(Kt::ERROR, "VFS") << "Drive " << drive << " not registered"; + return -1; + } + + return driveTable[drive]->ReadDir(localPath, outNames, maxEntries); + } + +} diff --git a/kernel/src/Fs/Vfs.hpp b/kernel/src/Fs/Vfs.hpp new file mode 100644 index 0000000..2c6b88b --- /dev/null +++ b/kernel/src/Fs/Vfs.hpp @@ -0,0 +1,33 @@ +/* + * Vfs.hpp + * Virtual File System with numerical logical drive identifiers + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Fs::Vfs { + + static constexpr int MaxDrives = 16; + static constexpr int MaxHandles = 64; + + struct FsDriver { + int (*Open)(const char* path); + int (*Read)(int handle, uint8_t* buffer, uint64_t offset, uint64_t size); + uint64_t (*GetSize)(int handle); + void (*Close)(int handle); + int (*ReadDir)(const char* path, const char** outNames, int maxEntries); + }; + + void Initialize(); + int RegisterDrive(int driveNumber, FsDriver* driver); + + int VfsOpen(const char* path); + int VfsRead(int handle, uint8_t* buffer, uint64_t offset, uint64_t size); + uint64_t VfsGetSize(int handle); + void VfsClose(int handle); + int VfsReadDir(const char* path, const char** outNames, int maxEntries); + +} diff --git a/kernel/src/Hal/GDT.asm b/kernel/src/Hal/GDT.asm index 777ba62..f773b88 100644 --- a/kernel/src/Hal/GDT.asm +++ b/kernel/src/Hal/GDT.asm @@ -8,6 +8,7 @@ section .text ; Text/code section global ReloadSegments global LoadGDT +global LoadTR LoadGDT: lgdt [rdi] ; Run LGDT on the contents of 1st C parameter @@ -28,4 +29,9 @@ ReloadSegments: mov gs, ax mov ss, ax - ret \ No newline at end of file + ret + +LoadTR: + mov ax, 0x28 ; TSS selector + ltr ax + ret diff --git a/kernel/src/Hal/GDT.cpp b/kernel/src/Hal/GDT.cpp index 907bfbb..326ed51 100644 --- a/kernel/src/Hal/GDT.cpp +++ b/kernel/src/Hal/GDT.cpp @@ -1,43 +1,76 @@ /* - * gdt.hpp + * gdt.cpp * Intel Global Descriptor Table * Copyright (c) 2025 Daniel Hammer */ #include "GDT.hpp" #include "../Terminal/Terminal.hpp" +#include namespace Hal { using namespace Kt; GDTPointer gdtPointer{}; BasicGDT kernelGDT{}; - - void PrepareGDT() { - kernelGDT = { - {0xFFFF, 0, 0, 0x00, 0x00, 0}, - {0xFFFF, 0, 0, 0x9A, 0xA0, 0}, - {0xFFFF, 0, 0, 0x92, 0xA0, 0}, - {0xFFFF, 0, 0, 0x9A, 0xA0, 0}, - {0xFFFF, 0, 0, 0x92, 0xA0, 0}, + TSS64 g_tss{}; - {0, 0, 0, 0xFA, 0x00, 0x0}, + void PrepareGDT() { + // Zero the TSS + memset(&g_tss, 0, sizeof(g_tss)); + g_tss.iopbOffset = sizeof(TSS64); + + kernelGDT = { + {0xFFFF, 0, 0, 0x00, 0x00, 0}, // Null + {0xFFFF, 0, 0, 0x9A, 0xA0, 0}, // KernelCode (DPL=0, code, 64-bit) + {0xFFFF, 0, 0, 0x92, 0xA0, 0}, // KernelData (DPL=0, data) + {0xFFFF, 0, 0, 0xF2, 0xA0, 0}, // UserData (DPL=3, data) + {0xFFFF, 0, 0, 0xFA, 0xA0, 0}, // UserCode (DPL=3, code, 64-bit) + {0, 0, 0, 0, 0, 0}, // TSS low (filled below) + {0, 0, 0, 0, 0, 0}, // TSS high (filled below) }; - + + // Encode 16-byte TSS descriptor + uint64_t base = (uint64_t)&g_tss; + uint32_t limit = sizeof(TSS64) - 1; + + // Low 8 bytes (normal GDT entry format) + kernelGDT.TSS.LimitLow = limit & 0xFFFF; + kernelGDT.TSS.BaseLow = base & 0xFFFF; + kernelGDT.TSS.BaseMiddle = (base >> 16) & 0xFF; + kernelGDT.TSS.AccessByte = 0x89; // Present, 64-bit TSS Available + kernelGDT.TSS.GranularityByte = (limit >> 16) & 0x0F; + kernelGDT.TSS.BaseHigh = (base >> 24) & 0xFF; + + // High 8 bytes (base[63:32] + reserved) + uint32_t baseUpper = (uint32_t)(base >> 32); + kernelGDT.TSSHigh.LimitLow = baseUpper & 0xFFFF; + kernelGDT.TSSHigh.BaseLow = (baseUpper >> 16) & 0xFFFF; + kernelGDT.TSSHigh.BaseMiddle = 0; + kernelGDT.TSSHigh.AccessByte = 0; + kernelGDT.TSSHigh.GranularityByte = 0; + kernelGDT.TSSHigh.BaseHigh = 0; + gdtPointer = GDTPointer{ .Size = sizeof(kernelGDT) - 1, .GDTAddress = (uint64_t)&kernelGDT }; } - + // Helpers implemented in gdt.asm extern "C" void LoadGDT(GDTPointer *gdtPointer); extern "C" void ReloadSegments(); - + extern "C" void LoadTR(); + void BridgeLoadGDT() { LoadGDT(&gdtPointer); ReloadSegments(); KernelLogStream(DEBUG, "Hal") << "Set new GDT (0x" << base::hex << (uint64_t)&kernelGDT << ")"; } -}; \ No newline at end of file + + void LoadTSS() { + LoadTR(); + KernelLogStream(OK, "Hal") << "Loaded TSS (selector 0x28)"; + } +}; diff --git a/kernel/src/Hal/GDT.hpp b/kernel/src/Hal/GDT.hpp index cdbd300..daf1236 100644 --- a/kernel/src/Hal/GDT.hpp +++ b/kernel/src/Hal/GDT.hpp @@ -17,25 +17,45 @@ namespace Hal { uint8_t BaseMiddle; uint8_t AccessByte; uint8_t GranularityByte; - uint8_t BaseHigh; + uint8_t BaseHigh; }__attribute__((packed)); - + struct BasicGDT { - GDTEntry Null; - GDTEntry KernelCode; - GDTEntry KernelData; - GDTEntry UserCode; - GDTEntry UserData; - - // Task State Segment - GDTEntry TSS; + GDTEntry Null; // 0x00 + GDTEntry KernelCode; // 0x08 + GDTEntry KernelData; // 0x10 + GDTEntry UserData; // 0x18 (before UserCode for SYSRET) + GDTEntry UserCode; // 0x20 + GDTEntry TSS; // 0x28 (low 8 bytes of 16-byte TSS descriptor) + GDTEntry TSSHigh; // 0x30 (high 8 bytes of 16-byte TSS descriptor) }__attribute__((packed)); - + struct GDTPointer { uint16_t Size; uint64_t GDTAddress; }__attribute__((packed)); - + + struct TSS64 { + uint32_t reserved0; + uint64_t rsp0; + uint64_t rsp1; + uint64_t rsp2; + uint64_t reserved1; + uint64_t ist1; + uint64_t ist2; + uint64_t ist3; + uint64_t ist4; + uint64_t ist5; + uint64_t ist6; + uint64_t ist7; + uint64_t reserved2; + uint16_t reserved3; + uint16_t iopbOffset; + }__attribute__((packed)); + + extern TSS64 g_tss; + void BridgeLoadGDT(); void PrepareGDT(); -}; \ No newline at end of file + void LoadTSS(); +}; diff --git a/kernel/src/Hal/MSR.hpp b/kernel/src/Hal/MSR.hpp new file mode 100644 index 0000000..6d0ddf6 --- /dev/null +++ b/kernel/src/Hal/MSR.hpp @@ -0,0 +1,30 @@ +/* + * MSR.hpp + * Model-Specific Register read/write helpers + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include + +namespace Hal { + + inline uint64_t ReadMSR(uint32_t msr) { + uint32_t lo, hi; + asm volatile("rdmsr" : "=a"(lo), "=d"(hi) : "c"(msr)); + return ((uint64_t)hi << 32) | lo; + } + + inline void WriteMSR(uint32_t msr, uint64_t value) { + uint32_t lo = (uint32_t)(value & 0xFFFFFFFF); + uint32_t hi = (uint32_t)(value >> 32); + asm volatile("wrmsr" : : "a"(lo), "d"(hi), "c"(msr)); + } + + // Well-known MSR addresses + static constexpr uint32_t IA32_EFER = 0xC0000080; + static constexpr uint32_t IA32_STAR = 0xC0000081; + static constexpr uint32_t IA32_LSTAR = 0xC0000082; + static constexpr uint32_t IA32_FMASK = 0xC0000084; + +} diff --git a/kernel/src/Hal/SyscallEntry.asm b/kernel/src/Hal/SyscallEntry.asm new file mode 100644 index 0000000..d18eaa1 --- /dev/null +++ b/kernel/src/Hal/SyscallEntry.asm @@ -0,0 +1,89 @@ +; +; SyscallEntry.asm +; SYSCALL/SYSRET entry point and user-mode transition +; Copyright (c) 2025 Daniel Hammer +; + +[bits 64] +section .text + +extern SyscallDispatch +extern g_kernelRsp + +; ============================================================ +; SyscallEntry — called by the SYSCALL instruction +; RCX = user RIP, R11 = user RFLAGS, RAX = syscall number +; Args: RDI, RSI, RDX, R10, R8, R9 +; Interrupts are masked (FMASK clears IF) +; ============================================================ +global SyscallEntry +SyscallEntry: + mov [rel g_userRsp], rsp ; stash user RSP + mov rsp, [rel g_kernelRsp] ; switch to kernel stack + + ; Build SyscallFrame on kernel stack (push order matches struct) + push qword [rel g_userRsp] ; user_rsp + push rcx ; user_rip + push r11 ; user_rflags + push rax ; syscall_nr + push rdi ; arg1 + push rsi ; arg2 + push rdx ; arg3 + push r10 ; arg4 + push r8 ; arg5 + push r9 ; arg6 + + ; Callee-saved registers (preserve for user) + push rbx + push rbp + push r12 + push r13 + push r14 + push r15 + + sti ; safe to take interrupts now + + mov rdi, rsp ; arg1 = pointer to SyscallFrame + call SyscallDispatch ; returns int64_t in rax + + cli ; disable interrupts for sysret + + pop r15 + pop r14 + pop r13 + pop r12 + pop rbp + pop rbx + + add rsp, 56 ; skip arg6..arg1 (6*8) + syscall_nr (1*8) = 56 + + pop r11 ; user RFLAGS + pop rcx ; user RIP + pop rsp ; user RSP + + o64 sysret + +; ============================================================ +; JumpToUserMode — initial transition to ring 3 via IRETQ +; RDI = user RIP (entry point) +; RSI = user RSP (top of user stack) +; ============================================================ +global JumpToUserMode +JumpToUserMode: + mov ax, 0x1B ; UserData | RPL3 + mov ds, ax + mov es, ax + + push 0x1B ; SS = UserData | RPL3 + push rsi ; RSP = user stack top + push 0x202 ; RFLAGS (IF=1) + push 0x23 ; CS = UserCode | RPL3 + push rdi ; RIP = entry point + iretq + +; ============================================================ +; BSS: scratch space for user RSP save +; ============================================================ +section .bss +global g_userRsp +g_userRsp: resq 1 diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index 0257a0d..e56ac4f 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -37,7 +37,10 @@ #include #include #include - +#include +#include +#include +#include using namespace Kt; namespace Memory { @@ -95,22 +98,20 @@ extern "C" void kmain() { uint64_t hhdm_offset = hhdm_request.response->offset; Memory::HHDMBase = hhdm_offset; - if (memmap_request.response != nullptr) { - Kt::KernelLogStream(OK, "Mem") << "Creating PageFrameAllocator"; - - Memory::PageFrameAllocator pmm(Memory::Scan(memmap_request.response)); - Memory::g_pfa = &pmm; - - Kt::KernelLogStream(OK, "Mem") << "Creating HeapAllocator"; - Memory::HeapAllocator heap{}; - Memory::g_heap = &heap; - - heap.Walk(); - - } else { + if (memmap_request.response == nullptr) { Panic("System memory map missing!", nullptr); } + Kt::KernelLogStream(OK, "Mem") << "Creating PageFrameAllocator"; + Memory::PageFrameAllocator pmm(Memory::Scan(memmap_request.response)); + Memory::g_pfa = &pmm; + + Kt::KernelLogStream(OK, "Mem") << "Creating HeapAllocator"; + Memory::HeapAllocator heap{}; + Memory::g_heap = &heap; + + heap.Walk(); + #if defined (__x86_64__) Hal::IDTInitialize(); @@ -142,8 +143,49 @@ extern "C" void kmain() { Efi::SystemTable* ST = (Efi::SystemTable*)Memory::HHDM(system_table_request.response->address); Efi::Init(ST); + // Initialize ramdisk from Limine modules + if (module_request.response != nullptr && module_request.response->module_count > 0) { + Kt::KernelLogStream(OK, "Modules") << "Found " << (uint64_t)module_request.response->module_count << " module(s)"; + for (uint64_t i = 0; i < module_request.response->module_count; i++) { + limine_file* mod = module_request.response->modules[i]; + const char* modString = mod->string; + + // Find "ramdisk" module by its string + if (modString != nullptr && + modString[0] == 'r' && modString[1] == 'a' && modString[2] == 'm' && + modString[3] == 'd' && modString[4] == 'i' && modString[5] == 's' && + modString[6] == 'k' && modString[7] == '\0') { + Kt::KernelLogStream(OK, "Modules") << "Ramdisk module at " << kcp::hex << (uint64_t)mod->address << kcp::dec << ", size=" << mod->size; + Fs::Ramdisk::Initialize(mod->address, mod->size); + } + } + } else { + Kt::KernelLogStream(WARNING, "Modules") << "No modules loaded (ramdisk unavailable)"; + } + + // Initialize VFS and register ramdisk as drive 0 + Fs::Vfs::Initialize(); + + static Fs::Vfs::FsDriver ramdiskDriver = { + Fs::Ramdisk::Open, + Fs::Ramdisk::Read, + Fs::Ramdisk::GetSize, + Fs::Ramdisk::Close, + Fs::Ramdisk::ReadDir + }; + Fs::Vfs::RegisterDrive(0, &ramdiskDriver); + Graphics::Cursor::Initialize(framebuffer); + Hal::LoadTSS(); + Zenith::InitializeSyscalls(); + + Sched::Initialize(); + Sched::Spawn("0:/shell.elf"); + + // Enable preemptive scheduling via the APIC timer + Timekeeping::EnableSchedulerTick(); + // Main loop: update cursor position and halt until next interrupt for (;;) { Graphics::Cursor::Update(); diff --git a/kernel/src/Memory/Heap.cpp b/kernel/src/Memory/Heap.cpp index 64eb03a..3c8345a 100644 --- a/kernel/src/Memory/Heap.cpp +++ b/kernel/src/Memory/Heap.cpp @@ -80,19 +80,19 @@ namespace Memory InsertToFreelist(rest, newBlockSize); } - + Lock.Release(); return block; } prev = current; current = current->next; - - Lock.Release(); } - // First pass allocation failed - size_t pagesNeeded = size / 0x1000; + Lock.Release(); + + // First pass allocation failed -- grow the heap + size_t pagesNeeded = (sizeNeeded + 0xFFF) / 0x1000; InsertPagesToFreelist(pagesNeeded); return Request(size); @@ -102,7 +102,9 @@ namespace Memory auto new_block = Request(size); if (ptr != nullptr && new_block != nullptr) { - memcpy(new_block, ptr, size); + size_t oldSize = GetAllocatedBlockSize(ptr); + size_t copySize = (oldSize < size) ? oldSize : size; + memcpy(new_block, ptr, copySize); Free(ptr); } @@ -123,7 +125,7 @@ namespace Memory auto actualSize = size + sizeof(Header); void* actualBlock = (void*)header; - InsertToFreelist(actualBlock, size); + InsertToFreelist(actualBlock, actualSize); Lock.Release(); } diff --git a/kernel/src/Memory/Paging.cpp b/kernel/src/Memory/Paging.cpp index 5de9d77..635b0ea 100644 --- a/kernel/src/Memory/Paging.cpp +++ b/kernel/src/Memory/Paging.cpp @@ -28,10 +28,10 @@ namespace Memory::VMM { for (size_t i = 0; i < memMap->entry_count; i++) { auto entry = memMap->entries[i]; - + for (size_t pageAddr = entry->base; pageAddr < (entry->base + entry->length); pageAddr += 0x1000) { Map(pageAddr, HHDM(pageAddr)); - } + } } LoadCR3(PML4); @@ -40,21 +40,41 @@ namespace Memory::VMM { PageTable* Paging::HandleLevel(VirtualAddress virtualAddress, PageTable* table, const size_t level) { PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[virtualAddress.GetIndex(level)]); - + if (!entry->Present) { entry->Present = true; entry->Writable = true; - + uint64_t downLevelAddr = Memory::SubHHDM((uint64_t)Memory::g_pfa->AllocateZeroed()); - + entry->Address = downLevelAddr >> 12; - + return (PageTable*)downLevelAddr; } else { return (PageTable*)(entry->Address << 12); } } + PageTable* Paging::HandleLevelUser(VirtualAddress virtualAddress, PageTable* table, const size_t level) { + PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[virtualAddress.GetIndex(level)]); + + if (!entry->Present) { + entry->Present = true; + entry->Writable = true; + entry->Supervisor = 1; // User-accessible + + uint64_t downLevelAddr = Memory::SubHHDM((uint64_t)Memory::g_pfa->AllocateZeroed()); + + entry->Address = downLevelAddr >> 12; + + return (PageTable*)downLevelAddr; + } else { + // Ensure User bit is set on existing entries in the user path + entry->Supervisor = 1; + return (PageTable*)(entry->Address << 12); + } + } + void Paging::Map(std::uint64_t physicalAddress, std::uint64_t virtualAddress) { if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) { Panic("Value that isn't page-aligned passed as address to Paging::Map!", nullptr); @@ -95,9 +115,79 @@ namespace Memory::VMM { pageEntry->Address = physicalAddress >> 12; } + void Paging::MapUser(std::uint64_t physicalAddress, std::uint64_t virtualAddress) { + if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) { + Panic("Value that isn't page-aligned passed as address to Paging::MapUser!", nullptr); + } + + VirtualAddress virtualAddressObj(virtualAddress); + + auto PML3 = HandleLevelUser(virtualAddressObj, PML4, 4); + auto PML2 = HandleLevelUser(virtualAddressObj, PML3, 3); + auto PML1 = HandleLevelUser(virtualAddressObj, PML2, 2); + + PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&PML1->entries[virtualAddressObj.GetPageIndex()]); + + pageEntry->Present = true; + pageEntry->Writable = true; + pageEntry->Supervisor = 1; // User-accessible + + pageEntry->Address = physicalAddress >> 12; + } + + std::uint64_t Paging::CreateUserPML4() { + // Allocate a new PML4 + void* newPage = Memory::g_pfa->AllocateZeroed(); + uint64_t newPml4Phys = Memory::SubHHDM((uint64_t)newPage); + PageTable* newPml4 = (PageTable*)newPage; // HHDM virtual address + + // Copy kernel-half entries (256-511) from the global PML4 + PageTable* kernelPml4 = (PageTable*)Memory::HHDM((uint64_t)g_paging->PML4); + for (int i = 256; i < 512; i++) { + newPml4->entries[i] = kernelPml4->entries[i]; + } + + return newPml4Phys; + } + + void Paging::MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) { + if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) { + Panic("Non-aligned address in Paging::MapUserIn!", nullptr); + } + + VirtualAddress va(virtualAddress); + + // Walk/create page tables from the given PML4, setting User bit at each level + auto walkLevel = [](PageTable* table, uint64_t index) -> PageTable* { + PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[index]); + if (!entry->Present) { + entry->Present = true; + entry->Writable = true; + entry->Supervisor = 1; // User-accessible + uint64_t newPhys = Memory::SubHHDM((uint64_t)Memory::g_pfa->AllocateZeroed()); + entry->Address = newPhys >> 12; + return (PageTable*)newPhys; + } else { + entry->Supervisor = 1; + return (PageTable*)(entry->Address << 12); + } + }; + + PageTable* pml4 = (PageTable*)pml4Phys; + auto pml3 = walkLevel(pml4, va.GetL4Index()); + auto pml2 = walkLevel(pml3, va.GetL3Index()); + auto pml1 = walkLevel(pml2, va.GetL2Index()); + + PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]); + pageEntry->Present = true; + pageEntry->Writable = true; + pageEntry->Supervisor = 1; + pageEntry->Address = physicalAddress >> 12; + } + std::uint64_t Paging::GetPhysAddr(std::uint64_t pml4, std::uint64_t virtualAddress, bool use40BitL1) { VirtualAddress virtualAddressObj(virtualAddress); - + PageTable* pml4Virt = (PageTable*)HHDM(pml4); PageTableEntry* pml4_entry = &pml4Virt->entries[virtualAddressObj.GetL4Index()]; @@ -122,4 +212,4 @@ namespace Memory::VMM { std::uint64_t Paging::GetPhysAddr(std::uint64_t virtualAddress) { return GetPhysAddr((std::uint64_t)PML4, virtualAddress, false); } -}; \ No newline at end of file +}; diff --git a/kernel/src/Memory/Paging.hpp b/kernel/src/Memory/Paging.hpp index 7b8a3d7..52b68f3 100644 --- a/kernel/src/Memory/Paging.hpp +++ b/kernel/src/Memory/Paging.hpp @@ -35,7 +35,7 @@ namespace Memory::VMM { std::uint8_t NX : 1; }; - + struct PageTable { PageTableEntry entries[512]; } __attribute__((packed)) __attribute__((aligned(0x1000))); @@ -79,24 +79,35 @@ namespace Memory::VMM { else if (level == 1) return GetPageIndex(); + + return 0; } }; class Paging { + PageTable* HandleLevel(VirtualAddress virtualAddress, PageTable* table, size_t level); + PageTable* HandleLevelUser(VirtualAddress virtualAddress, PageTable* table, size_t level); +public: PageTable* PML4{}; - PageTable* HandleLevel(VirtualAddress virtualAddress, PageTable* table, size_t level); -public: Paging(); void Init(std::uint64_t kernelBaseVirt, std::uint64_t kernelSize, limine_memmap_response* memMap); void Map(std::uint64_t physicalAddress, std::uint64_t virtualAddress); void MapMMIO(std::uint64_t physicalAddress, std::uint64_t virtualAddress); + void MapUser(std::uint64_t physicalAddress, std::uint64_t virtualAddress); static std::uint64_t GetPhysAddr(std::uint64_t PML4, std::uint64_t virtualAddress, bool use40BitL1 = false); std::uint64_t GetPhysAddr(std::uint64_t virtualAddress); + + // Create a new PML4 with kernel-half (entries 256-511) copied from g_paging. + // Returns the physical address of the new PML4. + static std::uint64_t CreateUserPML4(); + + // Map a page into an arbitrary PML4 (specified by physical address) with User bit set. + static void MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress); }; extern Paging* g_paging; extern "C" uint64_t GetCR3(); extern "C" void LoadCR3(PageTable* PML4); -}; \ No newline at end of file +}; diff --git a/kernel/src/Net/Arp.cpp b/kernel/src/Net/Arp.cpp index 415ce8d..f98e0da 100644 --- a/kernel/src/Net/Arp.cpp +++ b/kernel/src/Net/Arp.cpp @@ -7,6 +7,7 @@ #include "Arp.hpp" #include #include +#include #include #include #include @@ -91,8 +92,9 @@ namespace Net::Arp { uint32_t senderIp = pkt->SenderIp; // Already in network byte order in struct uint32_t targetIp = pkt->TargetIp; - // Cache the sender's IP->MAC mapping + // Cache the sender's IP->MAC mapping, then flush any packets waiting on it CacheInsert(senderIp, pkt->SenderMac); + Ipv4::FlushPending(); uint16_t op = Ntohs(pkt->Operation); diff --git a/kernel/src/Net/Icmp.cpp b/kernel/src/Net/Icmp.cpp index d5c7a12..737a6dc 100644 --- a/kernel/src/Net/Icmp.cpp +++ b/kernel/src/Net/Icmp.cpp @@ -15,10 +15,40 @@ using namespace Kt; namespace Net::Icmp { + // Reply tracking for outgoing pings + static volatile bool g_replyReceived = false; + static volatile uint16_t g_replyId = 0; + static volatile uint16_t g_replySeq = 0; + void Initialize() { KernelLogStream(OK, "Net") << "ICMP initialized"; } + void ResetReply() { + g_replyReceived = false; + } + + bool HasReply(uint16_t identifier, uint16_t sequence) { + return g_replyReceived + && g_replyId == identifier + && g_replySeq == sequence; + } + + void SendEchoRequest(uint32_t destIp, uint16_t identifier, uint16_t sequence) { + uint8_t packet[sizeof(Header)]; + Header* hdr = (Header*)packet; + + hdr->Type = TYPE_ECHO_REQUEST; + hdr->Code = 0; + hdr->Checksum = 0; + hdr->Identifier = Htons(identifier); + hdr->Sequence = Htons(sequence); + + hdr->Checksum = Ipv4::Checksum(packet, sizeof(Header)); + + Ipv4::Send(destIp, Ipv4::PROTO_ICMP, packet, sizeof(Header)); + } + void OnPacketReceived(uint32_t srcIp, const uint8_t* data, uint16_t length) { if (length < sizeof(Header)) { return; @@ -54,6 +84,10 @@ namespace Net::Icmp { replyHdr->Checksum = Ipv4::Checksum(reply, length); Ipv4::Send(srcIp, Ipv4::PROTO_ICMP, reply, length); + } else if (hdr->Type == TYPE_ECHO_REPLY && hdr->Code == 0) { + g_replyId = Ntohs(hdr->Identifier); + g_replySeq = Ntohs(hdr->Sequence); + g_replyReceived = true; } } diff --git a/kernel/src/Net/Icmp.hpp b/kernel/src/Net/Icmp.hpp index 12d9841..616f2ce 100644 --- a/kernel/src/Net/Icmp.hpp +++ b/kernel/src/Net/Icmp.hpp @@ -26,4 +26,13 @@ namespace Net::Icmp { // Handle an incoming ICMP packet (called by IPv4 layer) void OnPacketReceived(uint32_t srcIp, const uint8_t* data, uint16_t length); + // Send an ICMP echo request to the given IP address + void SendEchoRequest(uint32_t destIp, uint16_t identifier, uint16_t sequence); + + // Check if a reply was received for the given identifier/sequence + bool HasReply(uint16_t identifier, uint16_t sequence); + + // Reset the reply tracker (call before sending a new ping) + void ResetReply(); + } diff --git a/kernel/src/Net/Ipv4.cpp b/kernel/src/Net/Ipv4.cpp index 07981af..d010a2c 100644 --- a/kernel/src/Net/Ipv4.cpp +++ b/kernel/src/Net/Ipv4.cpp @@ -15,7 +15,6 @@ #include #include #include -#include using namespace Kt; @@ -23,6 +22,18 @@ namespace Net::Ipv4 { static uint16_t g_identification = 0; + // Deferred packet queue for packets awaiting ARP resolution + struct PendingPacket { + uint32_t DestIp; + uint8_t Protocol; + uint8_t Data[Ethernet::MAX_PAYLOAD_SIZE - HEADER_SIZE]; + uint16_t Length; + bool Active; + }; + + static constexpr uint32_t PENDING_QUEUE_SIZE = 8; + static PendingPacket g_pendingQueue[PENDING_QUEUE_SIZE] = {}; + void Initialize() { g_identification = 0; KernelLogStream(OK, "Net") << "IPv4 initialized, IP: " @@ -138,30 +149,9 @@ namespace Net::Ipv4 { } } - bool Send(uint32_t destIp, uint8_t protocol, const uint8_t* payload, uint16_t payloadLen) { - if (payloadLen > (Ethernet::MAX_PAYLOAD_SIZE - HEADER_SIZE)) { - return false; - } - - // Determine next-hop IP and resolve MAC - uint32_t nextHop = GetNextHop(destIp); - uint8_t destMac[6]; - - if (!Arp::Resolve(nextHop, destMac)) { - // ARP request sent, wait briefly and retry - for (int attempt = 0; attempt < 3; attempt++) { - Timekeeping::Sleep(50); - if (Arp::Resolve(nextHop, destMac)) { - break; - } - } - // Final check - if (!Arp::Resolve(nextHop, destMac)) { - return false; - } - } - - // Build IP packet + // Build and send an IP packet over Ethernet (MAC already resolved) + static bool SendDirect(uint32_t destIp, uint8_t protocol, const uint8_t* destMac, + const uint8_t* payload, uint16_t payloadLen) { uint8_t packet[Ethernet::MAX_PAYLOAD_SIZE]; Header* hdr = (Header*)packet; @@ -176,13 +166,57 @@ namespace Net::Ipv4 { hdr->SrcIp = GetIpAddress(); hdr->DstIp = destIp; - // Calculate header checksum hdr->Checksum = Checksum(hdr, HEADER_SIZE); - // Copy payload memcpy(packet + HEADER_SIZE, payload, payloadLen); return Ethernet::Send(destMac, Ethernet::ETHERTYPE_IPV4, packet, HEADER_SIZE + payloadLen); } + bool Send(uint32_t destIp, uint8_t protocol, const uint8_t* payload, uint16_t payloadLen) { + if (payloadLen > (Ethernet::MAX_PAYLOAD_SIZE - HEADER_SIZE)) { + return false; + } + + // Determine next-hop IP and resolve MAC + uint32_t nextHop = GetNextHop(destIp); + uint8_t destMac[6]; + + if (Arp::Resolve(nextHop, destMac)) { + return SendDirect(destIp, protocol, destMac, payload, payloadLen); + } + + // ARP request already sent by Resolve(), queue the packet for later + for (uint32_t i = 0; i < PENDING_QUEUE_SIZE; i++) { + if (!g_pendingQueue[i].Active) { + g_pendingQueue[i].DestIp = destIp; + g_pendingQueue[i].Protocol = protocol; + g_pendingQueue[i].Length = payloadLen; + memcpy(g_pendingQueue[i].Data, payload, payloadLen); + g_pendingQueue[i].Active = true; + return true; + } + } + + // Queue full, drop the packet + return false; + } + + void FlushPending() { + for (uint32_t i = 0; i < PENDING_QUEUE_SIZE; i++) { + if (!g_pendingQueue[i].Active) { + continue; + } + + uint32_t nextHop = GetNextHop(g_pendingQueue[i].DestIp); + uint8_t destMac[6]; + + if (Arp::Resolve(nextHop, destMac)) { + SendDirect(g_pendingQueue[i].DestIp, g_pendingQueue[i].Protocol, + destMac, g_pendingQueue[i].Data, g_pendingQueue[i].Length); + g_pendingQueue[i].Active = false; + } + } + } + } diff --git a/kernel/src/Net/Ipv4.hpp b/kernel/src/Net/Ipv4.hpp index f77fe41..87620ef 100644 --- a/kernel/src/Net/Ipv4.hpp +++ b/kernel/src/Net/Ipv4.hpp @@ -36,9 +36,14 @@ namespace Net::Ipv4 { // Handle an incoming IP packet (called by Ethernet layer) void OnPacketReceived(const uint8_t* data, uint16_t length); - // Send an IP packet with the given protocol and payload + // Send an IP packet with the given protocol and payload. + // If ARP resolution is pending, the packet is queued and sent when the reply arrives. bool Send(uint32_t destIp, uint8_t protocol, const uint8_t* payload, uint16_t payloadLen); + // Flush any packets that were waiting for ARP resolution. + // Called by the ARP layer when a new cache entry is inserted. + void FlushPending(); + // Compute the Internet checksum over a buffer uint16_t Checksum(const void* data, uint16_t length); diff --git a/kernel/src/Platform/Limine.hpp b/kernel/src/Platform/Limine.hpp index cd0dad4..1497c38 100644 --- a/kernel/src/Platform/Limine.hpp +++ b/kernel/src/Platform/Limine.hpp @@ -59,6 +59,15 @@ namespace { .response = nullptr }; + __attribute__((used, section(".limine_requests"))) + volatile limine_module_request module_request = { + .id = LIMINE_MODULE_REQUEST, + .revision = 1, + .response = nullptr, + .internal_module_count = 0, + .internal_modules = nullptr + }; + } // Finally, define the start and end markers for the Limine requests. diff --git a/kernel/src/Sched/Context.asm b/kernel/src/Sched/Context.asm new file mode 100644 index 0000000..56bdfe7 --- /dev/null +++ b/kernel/src/Sched/Context.asm @@ -0,0 +1,45 @@ +; +; Context.asm +; Context switch: save/restore callee-saved registers, stack pointer, and CR3 +; Copyright (c) 2025 Daniel Hammer +; + +[bits 64] +section .text + +; void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t newCR3) +; rdi = pointer to save old RSP +; rsi = new RSP to restore +; rdx = new PML4 physical address (for CR3) +global SchedContextSwitch +SchedContextSwitch: + ; Save callee-saved registers on the current stack + push rbp + push rbx + push r12 + push r13 + push r14 + push r15 + + ; Save current RSP into *oldRsp + mov [rdi], rsp + + ; Load new RSP + mov rsp, rsi + + ; Switch address space if CR3 differs (avoid unnecessary TLB flush) + mov rax, cr3 + cmp rax, rdx + je .skip_cr3 + mov cr3, rdx +.skip_cr3: + + ; Restore callee-saved registers from the new stack + pop r15 + pop r14 + pop r13 + pop r12 + pop rbx + pop rbp + + ret diff --git a/kernel/src/Sched/ElfLoader.cpp b/kernel/src/Sched/ElfLoader.cpp new file mode 100644 index 0000000..97521fb --- /dev/null +++ b/kernel/src/Sched/ElfLoader.cpp @@ -0,0 +1,154 @@ +/* + * ElfLoader.cpp + * ELF64 binary loader for user-mode processes + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "ElfLoader.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Sched { + + static bool ValidateElfHeader(const Elf64Header* hdr) { + // Check ELF magic: 0x7f 'E' 'L' 'F' + if (hdr->e_ident[0] != 0x7f || + hdr->e_ident[1] != 'E' || + hdr->e_ident[2] != 'L' || + hdr->e_ident[3] != 'F') { + Kt::KernelLogStream(Kt::ERROR, "ELF") << "Invalid ELF magic"; + return false; + } + + // Class must be ELFCLASS64 (2) + if (hdr->e_ident[4] != 2) { + Kt::KernelLogStream(Kt::ERROR, "ELF") << "Not a 64-bit ELF"; + return false; + } + + // Data encoding must be ELFDATA2LSB (1) - little endian + if (hdr->e_ident[5] != 1) { + Kt::KernelLogStream(Kt::ERROR, "ELF") << "Not little-endian"; + return false; + } + + if (hdr->e_type != ET_EXEC) { + Kt::KernelLogStream(Kt::ERROR, "ELF") << "Not an executable (type=" << (uint64_t)hdr->e_type << ")"; + return false; + } + + if (hdr->e_machine != EM_X86_64) { + Kt::KernelLogStream(Kt::ERROR, "ELF") << "Not x86_64 (machine=" << (uint64_t)hdr->e_machine << ")"; + return false; + } + + return true; + } + + 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; + return 0; + } + + uint64_t fileSize = Fs::Vfs::VfsGetSize(handle); + if (fileSize < sizeof(Elf64Header)) { + Kt::KernelLogStream(Kt::ERROR, "ELF") << "File too small (" << fileSize << " bytes)"; + Fs::Vfs::VfsClose(handle); + return 0; + } + + // Read entire file into a heap buffer + uint8_t* fileData = (uint8_t*)Memory::g_heap->Request(fileSize); + if (fileData == nullptr) { + Kt::KernelLogStream(Kt::ERROR, "ELF") << "Failed to allocate " << fileSize << " bytes for file"; + Fs::Vfs::VfsClose(handle); + return 0; + } + + Fs::Vfs::VfsRead(handle, fileData, 0, fileSize); + Fs::Vfs::VfsClose(handle); + + // Validate ELF header + Elf64Header* hdr = (Elf64Header*)fileData; + if (!ValidateElfHeader(hdr)) { + Memory::g_heap->Free(fileData); + 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); + + if (phdr->p_type != PT_LOAD) { + continue; + } + + if (phdr->p_memsz == 0) { + 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; + uint64_t numPages = (segEnd - segBase) / 0x1000; + + for (uint64_t p = 0; p < numPages; p++) { + void* page = Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) { + Kt::KernelLogStream(Kt::ERROR, "ELF") << "Out of physical pages"; + Memory::g_heap->Free(fileData); + return 0; + } + + uint64_t physAddr = Memory::SubHHDM((uint64_t)page); + uint64_t virtAddr = segBase + p * 0x1000; + + // Map into the process's PML4 with User bit set + Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, virtAddr); + + // Copy file data that overlaps this page (via HHDM) + uint64_t pageStart = virtAddr; + uint64_t pageEnd = virtAddr + 0x1000; + + uint64_t segFileStart = phdr->p_vaddr; + uint64_t segFileEnd = phdr->p_vaddr + phdr->p_filesz; + + uint64_t copyStart = (pageStart > segFileStart) ? pageStart : segFileStart; + uint64_t copyEnd = (pageEnd < segFileEnd) ? pageEnd : segFileEnd; + + if (copyStart < copyEnd) { + uint64_t dstOffset = copyStart - pageStart; + uint64_t srcOffset = copyStart - phdr->p_vaddr + phdr->p_offset; + uint64_t copySize = copyEnd - copyStart; + + uint8_t* dst = (uint8_t*)Memory::HHDM(physAddr) + dstOffset; + uint8_t* src = fileData + srcOffset; + memcpy(dst, src, copySize); + } + } + } + + 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/ElfLoader.hpp b/kernel/src/Sched/ElfLoader.hpp new file mode 100644 index 0000000..9d4ff96 --- /dev/null +++ b/kernel/src/Sched/ElfLoader.hpp @@ -0,0 +1,49 @@ +/* + * ElfLoader.hpp + * ELF64 binary loader for user-mode processes + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include + +namespace Sched { + + struct Elf64Header { + uint8_t e_ident[16]; + uint16_t e_type; + uint16_t e_machine; + uint32_t e_version; + uint64_t e_entry; + uint64_t e_phoff; + uint64_t e_shoff; + uint32_t e_flags; + uint16_t e_ehsize; + uint16_t e_phentsize; + uint16_t e_phnum; + uint16_t e_shentsize; + uint16_t e_shnum; + uint16_t e_shstrndx; + }; + + struct Elf64ProgramHeader { + uint32_t p_type; + uint32_t p_flags; + uint64_t p_offset; + uint64_t p_vaddr; + uint64_t p_paddr; + uint64_t p_filesz; + uint64_t p_memsz; + uint64_t p_align; + }; + + static constexpr uint32_t PT_LOAD = 1; + static constexpr uint16_t ET_EXEC = 2; + static constexpr uint16_t EM_X86_64 = 62; + + // Load an ELF64 binary into a per-process address space. + // pml4Phys = physical address of the process's PML4. + // Returns the entry point address, or 0 on failure. + uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys); + +} diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp new file mode 100644 index 0000000..7aa0e93 --- /dev/null +++ b/kernel/src/Sched/Scheduler.cpp @@ -0,0 +1,308 @@ +/* + * Scheduler.cpp + * Preemptive process scheduler with user-mode support + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "Scheduler.hpp" +#include "ElfLoader.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +// Assembly: context switch with CR3 parameter +extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t newCR3); + +// Assembly: jump to user mode via IRETQ +extern "C" void JumpToUserMode(uint64_t rip, uint64_t rsp); + +// Global kernel RSP for SYSCALL entry (written by scheduler, read by SyscallEntry.asm) +extern "C" uint64_t g_kernelRsp; +uint64_t g_kernelRsp = 0; + +namespace Sched { + + static Process processTable[MaxProcesses]; + static int currentPid = -1; // -1 = idle (kernel main loop) + static int nextPid = 0; + static uint64_t idleSavedRsp = 0; + + // The idle loop runs in the kernel PML4 + static uint64_t GetKernelCR3() { + return (uint64_t)Memory::VMM::g_paging->PML4; + } + + // Startup function for newly spawned processes. + // SchedContextSwitch "returns" here on first schedule. + static void ProcessStartup() { + // Send EOI for the timer IRQ that triggered the context switch + Hal::LocalApic::SendEOI(); + + if (currentPid >= 0) { + Process& proc = processTable[currentPid]; + + // Set up kernel RSP for SYSCALL entry + g_kernelRsp = proc.kernelStackTop; + + // Set up TSS RSP0 for hardware interrupts from ring 3 + Hal::g_tss.rsp0 = proc.kernelStackTop; + + // Jump to user mode (never returns) + JumpToUserMode(proc.entryPoint, proc.userStackTop); + } + + ExitProcess(); + for (;;) { + asm volatile("hlt"); + } + } + + void Initialize() { + for (int i = 0; i < MaxProcesses; i++) { + processTable[i].pid = i; + processTable[i].state = ProcessState::Free; + processTable[i].name = nullptr; + processTable[i].savedRsp = 0; + processTable[i].stackBase = 0; + processTable[i].entryPoint = 0; + processTable[i].sliceRemaining = 0; + processTable[i].pml4Phys = 0; + processTable[i].kernelStackTop = 0; + processTable[i].userStackTop = 0; + processTable[i].heapNext = 0; + } + + currentPid = -1; + nextPid = 0; + idleSavedRsp = 0; + + Kt::KernelLogStream(Kt::OK, "Sched") << "Initialized (" << MaxProcesses + << " process slots, " << (uint64_t)TimeSliceMs << " ms time slice)"; + } + + void Spawn(const char* vfsPath) { + int slot = -1; + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].state == ProcessState::Free) { + slot = i; + break; + } + } + + if (slot < 0) { + Kt::KernelLogStream(Kt::ERROR, "Sched") << "No free process slots"; + return; + } + + // Create per-process PML4 with kernel-half copied + uint64_t pml4Phys = Memory::VMM::Paging::CreateUserPML4(); + + // Load ELF into the process's address space + uint64_t entry = ElfLoad(vfsPath, pml4Phys); + if (entry == 0) { + Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to load ELF: " << vfsPath; + return; + } + + // 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; + } + 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; + } + + uint8_t* kernelStackBase = (uint8_t*)stackMem; + uint64_t kernelStackTop = (uint64_t)kernelStackBase + StackSize; + + // Allocate user stack pages and map them in the process PML4 + uint64_t userStackBase = UserStackTop - UserStackSize; + uint64_t topStackPagePhys = 0; + for (uint64_t i = 0; i < UserStackPages; i++) { + void* page = Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) { + Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for user stack"; + return; + } + uint64_t physAddr = Memory::SubHHDM((uint64_t)page); + Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, userStackBase + i * 0x1000); + if (i == UserStackPages - 1) topStackPagePhys = physAddr; + } + + // Allocate and map a user-space exit stub page. + // When _start() returns, it jumps here and calls SYS_EXIT(0). + { + void* stubPage = Memory::g_pfa->AllocateZeroed(); + if (stubPage == nullptr) { + Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for exit stub"; + return; + } + uint64_t stubPhys = Memory::SubHHDM((uint64_t)stubPage); + Memory::VMM::Paging::MapUserIn(pml4Phys, stubPhys, ExitStubAddr); + + // Write: xor edi, edi; xor eax, eax; syscall + uint8_t* stub = (uint8_t*)stubPage; + stub[0] = 0x31; stub[1] = 0xFF; // xor edi, edi (exit code 0) + stub[2] = 0x31; stub[3] = 0xC0; // xor eax, eax (SYS_EXIT = 0) + stub[4] = 0x0F; stub[5] = 0x05; // syscall + } + + // Push exit stub address as the return address on the user stack. + // UserStackTop - 8 falls at offset 0xFF8 within the top stack page. + { + uint8_t* topPage = (uint8_t*)Memory::HHDM(topStackPagePhys); + *(uint64_t*)(topPage + 0xFF8) = ExitStubAddr; + } + + // Set up the initial kernel stack frame so that SchedContextSwitch + // "returns" into ProcessStartup + uint64_t* sp = (uint64_t*)kernelStackTop; + + *(--sp) = (uint64_t)ProcessStartup; // return addr + *(--sp) = 0; // rbp + *(--sp) = 0; // rbx + *(--sp) = 0; // r12 + *(--sp) = 0; // r13 + *(--sp) = 0; // r14 + *(--sp) = 0; // r15 + + Process& proc = processTable[slot]; + proc.pid = nextPid++; + proc.state = ProcessState::Ready; + proc.name = vfsPath; + proc.savedRsp = (uint64_t)sp; + proc.stackBase = (uint64_t)kernelStackBase; + proc.entryPoint = entry; + proc.sliceRemaining = TimeSliceMs; + proc.pml4Phys = pml4Phys; + proc.kernelStackTop = kernelStackTop; + 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; + } + + void Schedule() { + int next = -1; + int start = (currentPid >= 0) ? currentPid + 1 : 0; + + for (int i = 0; i < MaxProcesses; i++) { + int idx = (start + i) % MaxProcesses; + if (processTable[idx].state == ProcessState::Ready) { + next = idx; + break; + } + } + + if (next < 0) { + return; + } + + if (next == currentPid) { + return; + } + + uint64_t* oldRspPtr; + uint64_t oldCR3; + + if (currentPid >= 0) { + processTable[currentPid].state = ProcessState::Ready; + oldRspPtr = &processTable[currentPid].savedRsp; + } else { + oldRspPtr = &idleSavedRsp; + } + + currentPid = next; + processTable[next].state = ProcessState::Running; + processTable[next].sliceRemaining = TimeSliceMs; + + uint64_t newCR3 = processTable[next].pml4Phys; + + // Update kernel RSP for SYSCALL entry + g_kernelRsp = processTable[next].kernelStackTop; + + // Update TSS RSP0 for hardware interrupts from ring 3 + Hal::g_tss.rsp0 = processTable[next].kernelStackTop; + + SchedContextSwitch(oldRspPtr, processTable[next].savedRsp, newCR3); + } + + void Tick() { + if (currentPid < 0) { + // Idle — check if any process became ready + Schedule(); + return; + } + + if (processTable[currentPid].sliceRemaining > 0) { + processTable[currentPid].sliceRemaining--; + } + + if (processTable[currentPid].sliceRemaining == 0) { + Schedule(); + } + } + + int GetCurrentPid() { + return (currentPid >= 0) ? processTable[currentPid].pid : -1; + } + + Process* GetCurrentProcessPtr() { + if (currentPid < 0) return nullptr; + return &processTable[currentPid]; + } + + void ExitProcess() { + if (currentPid < 0) { + return; + } + + Kt::KernelLogStream(Kt::OK, "Sched") << "Process " << (uint64_t)processTable[currentPid].pid << " terminated"; + + processTable[currentPid].state = ProcessState::Terminated; + + int next = -1; + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].state == ProcessState::Ready) { + next = i; + break; + } + } + + if (next >= 0) { + int old = currentPid; + currentPid = next; + processTable[next].state = ProcessState::Running; + processTable[next].sliceRemaining = TimeSliceMs; + + uint64_t newCR3 = processTable[next].pml4Phys; + g_kernelRsp = processTable[next].kernelStackTop; + Hal::g_tss.rsp0 = processTable[next].kernelStackTop; + + SchedContextSwitch(&processTable[old].savedRsp, processTable[next].savedRsp, newCR3); + } else { + int old = currentPid; + currentPid = -1; + SchedContextSwitch(&processTable[old].savedRsp, idleSavedRsp, GetKernelCR3()); + } + + for (;;) { + asm volatile("hlt"); + } + } + +} diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp new file mode 100644 index 0000000..05cabbd --- /dev/null +++ b/kernel/src/Sched/Scheduler.hpp @@ -0,0 +1,59 @@ +/* + * Scheduler.hpp + * Preemptive process scheduler with user-mode support + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include + +namespace Sched { + + static constexpr int MaxProcesses = 16; + static constexpr uint64_t StackPages = 4; // 16 KiB kernel stack per process + static constexpr uint64_t StackSize = StackPages * 0x1000; + static constexpr uint64_t UserStackPages = 4; // 16 KiB user stack + static constexpr uint64_t UserStackSize = UserStackPages * 0x1000; + static constexpr uint64_t UserStackTop = 0x7FFFFFF000ULL; // User stack top VA + static constexpr uint64_t UserHeapBase = 0x40000000ULL; // User heap start VA + static constexpr uint64_t ExitStubAddr = 0x3FF000ULL; // User-space exit stub page + static constexpr uint64_t TimeSliceMs = 10; // 10 ms time slice + + enum class ProcessState { + Free, + Ready, + Running, + Terminated + }; + + struct Process { + int pid; + ProcessState state; + const char* name; + uint64_t savedRsp; + uint64_t stackBase; // Bottom of allocated kernel stack (lowest address) + uint64_t entryPoint; + uint64_t sliceRemaining; // Ticks left in current time slice + uint64_t pml4Phys; // Physical address of per-process PML4 + 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 + }; + + void Initialize(); + void Spawn(const char* vfsPath); + void Schedule(); + + // Called from the APIC timer handler on every tick. + void Tick(); + + // Get the PID of the currently running process (-1 if idle) + int GetCurrentPid(); + + // Get a pointer to the currently running process (nullptr if idle) + Process* GetCurrentProcessPtr(); + + // Called by terminated processes to mark themselves done + void ExitProcess(); + +} diff --git a/kernel/src/Terminal/Terminal.cpp b/kernel/src/Terminal/Terminal.cpp index 68f1eb7..959ce25 100644 --- a/kernel/src/Terminal/Terminal.cpp +++ b/kernel/src/Terminal/Terminal.cpp @@ -60,11 +60,17 @@ namespace Kt { } void Putchar(char c) { + if (c == '\n') { + flanterm_write(ctx, "\r\n", 2); + return; + } flanterm_write(ctx, &c, 1); } void Print(const char *text) { - flanterm_write(ctx, text, Lib::strlen(text)); + for (size_t i = 0; text[i] != '\0'; i++) { + Putchar(text[i]); + } } }; \ No newline at end of file diff --git a/kernel/src/Timekeeping/ApicTimer.cpp b/kernel/src/Timekeeping/ApicTimer.cpp index 9d39161..8883680 100644 --- a/kernel/src/Timekeeping/ApicTimer.cpp +++ b/kernel/src/Timekeeping/ApicTimer.cpp @@ -10,6 +10,7 @@ #include #include #include +#include using namespace Kt; @@ -34,9 +35,15 @@ namespace Timekeeping { static volatile uint64_t g_tickCount = 0; static uint32_t g_ticksPerMs = 0; - // Timer IRQ handler: increment tick count + static bool g_schedEnabled = false; + + // Timer IRQ handler: increment tick count and drive scheduler static void TimerHandler(uint8_t) { g_tickCount = g_tickCount + 1; + + if (g_schedEnabled) { + Sched::Tick(); + } } // Use PIT channel 2 to create a precise delay for calibration. @@ -128,10 +135,14 @@ namespace Timekeeping { return g_tickCount; // 1 tick = 1 ms at 1000 Hz } + void EnableSchedulerTick() { + g_schedEnabled = true; + } + void Sleep(uint64_t ms) { uint64_t target = g_tickCount + ms; while (g_tickCount < target) { asm volatile("hlt"); } } -}; \ No newline at end of file +}; diff --git a/kernel/src/Timekeeping/ApicTimer.hpp b/kernel/src/Timekeeping/ApicTimer.hpp index d73a2b6..94242fd 100644 --- a/kernel/src/Timekeeping/ApicTimer.hpp +++ b/kernel/src/Timekeeping/ApicTimer.hpp @@ -17,6 +17,9 @@ namespace Timekeeping { // Get elapsed milliseconds since timer initialization uint64_t GetMilliseconds(); + // Enable scheduler tick (called after scheduler is initialized) + void EnableSchedulerTick(); + // Busy-wait sleep for the given number of milliseconds void Sleep(uint64_t ms); -}; \ No newline at end of file +}; diff --git a/limine.conf b/limine.conf index b57f6f9..2086f9c 100644 --- a/limine.conf +++ b/limine.conf @@ -8,3 +8,7 @@ timeout: 0 # Path to the kernel to boot. boot():/ represents the partition on which limine.conf is located. path: boot():/boot/kernel + + # Ramdisk module (USTAR tar archive) + module_path: boot():/boot/ramdisk.tar + module_string: ramdisk diff --git a/programs/GNUmakefile b/programs/GNUmakefile new file mode 100644 index 0000000..f9edb1c --- /dev/null +++ b/programs/GNUmakefile @@ -0,0 +1,74 @@ +# Nuke built-in rules and variables. +MAKEFLAGS += -rR +.SUFFIXES: + +# Target architecture. +ARCH := x86_64 + +# Auto-detect cross compiler from toolchain/local/. +TOOLCHAIN_PREFIX := $(shell cd .. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +# Compiler flags: freestanding, no stdlib, kernel-mode compatible. +override CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -mno-80387 \ + -mno-mmx \ + -mno-sse \ + -mno-sse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I include \ + -isystem ../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../kernel/freestnd-cxx-hdrs/x86_64/include + +# Linker flags: freestanding static ELF. +override LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T link.ld + +# Output directory. +BINDIR := bin + +# Discover all programs (each subdirectory under src/ is a program). +PROGRAMS := $(notdir $(wildcard src/*)) + +# Build targets: one ELF per program. +TARGETS := $(addprefix $(BINDIR)/,$(addsuffix .elf,$(PROGRAMS))) + +.PHONY: all clean + +all: $(TARGETS) + +# Build each program from its source files. +# For now each program is a single .cpp file compiled and linked directly. +$(BINDIR)/%.elf: src/%/main.cpp link.ld GNUmakefile + mkdir -p $(BINDIR) obj/$* + $(CXX) $(CXXFLAGS) -c src/$*/main.cpp -o obj/$*/main.o + $(CXX) $(CXXFLAGS) $(LDFLAGS) obj/$*/main.o -o $@ + +clean: + rm -rf $(BINDIR) obj diff --git a/programs/bin/hello.elf b/programs/bin/hello.elf new file mode 100755 index 0000000000000000000000000000000000000000..844af02ec53ec942b17284bcbdaae6c184f86c96 GIT binary patch literal 12360 zcmeHNTWlOx89rw&yK9fV7q71yr(V-ErMSBrU*ft>oY-+T&XTQdW$mxP>Dn>FFYVXhze8?El7Zbl&V3zAVD9H-~k~n5~@G9U{z^i~)fe*U^ z>u)QV;{46uFRbNmv+z9N??URr_XhxPlc`inhDKfy%Lt8-`Ip(7?}gO&|N3FC%**dp zz^i~)0j~mH1-uG)74RzHRluu&R{^gAUIn}g{J$!2-l$gV@sd@q#kU)V)z~WN#%L!i zY_NA_##F`wG*gJgD2XV#L115#x$m;Xm@KKs@##QVQ7dMX`4hlngCB>V98guV^T5-> z&Hq^%6#q*Y{Ex%!iJ@z73zUSf{~G8>JgmIQ!|%cPCfJTo1H8q%2?tn*4(*#f^c`s7 zFr{adFsxj_(DMfvmG5!+lVpuHnnmWD;@WSZ_4?qV8a9@{ui(jf-=8B`3WlA0E`k;`Rj1#CFtWhz&?V0{%bh&6AbV$*a3naybtUk z!A|})z#)Pf{}CKw1iSc;!5$*m%};|JCK%+?U=I@v@xMVnL@>xgGVpV(TiT6SU}mBFh-8Cjh#l^|i{UhMA0-t6 zCmu+G?LGoO+F?wiT)Pj8xNb`EMf9L)v7;0eER3^B0*5T4iBpSEI@~(yw~<8VVag>- z;qM}etc1ToL%{z6kztk2N)G#D7#N>oa&UH3J)u20I+BT=92(ZDLzsAqE#Zcik1$O; zrOBwhqO$R*G;r(80E%>&k4G=^x6f$n1J7uewWl@gLS-35`1?gZ8TmBQ14jkj(Jg4@ zgi7=cuFW8%^GS}mHJV!E@YOWFj_h4im;)}jqU=}Yp{ zL(bx~2=DRF5AF_;SO0xmCw5@JyBRzwTnFzZskk`h5VlE$iD66j=7o9)=Rk{b%EoTs zY*akas7dswofojbQ>LF{slAS8%^ojJ-K+2SL%e%M2Rdc?{k9d~;+^=GzTb~N@WPdY zX0OLxjC=PA(>_rJcGr$Cz~+SszZfgrr*!qh^dC|EEw7jo`v_BqTY9h3JIwrUmRa^E zXTIk!a?bxA#K&i7qzPigk9xB?^dt+r!>M#E@<2^(2PF|k`WiPvlOqEVe| z)C$#VJh2&1tiT6yh{16drdSZEenI?G+oB|e37Bbm| zGubq28aqwFFJ@PAX_n99C1ZSICcbSpDx0QJWZNL7r}9nKDA%oKoZdkw%~E>psZ2JT z$)y)pme1w{cQL(imhBp5rCCOzT45(&s3J+eT4ebv%hR^y_?Cqn>D3GRy*|~}+#=V? zYI>RJ<$^Uifw>8cPXjnpFP>kxDDb(9>9hcsGwJMEwo$KFg}RzqUSi|pduwQgu4c}% zY8{mqg0^$TUNNPn9h}YBsdL$MdNu!4PE04s#d6&NquXJrONf1kt!CbI zrHaP2lwG8iTnbm|rd6ml_JUc()T`S?Bjp@@GMTz+Sf)`;*;l$*Oz4Sn(Q2e{Yvre> zQjUKsZegp!9u%~*^PsTQMy1*KFZ|uJN|qV!LzF$Ssq3~uKLAv~!p&*?6(m#Qz1!M$@v#vWvvM znY8M36SHK|*x260?OCe3xUjKd8P{B`TGy+U29?J4ELa6|(`c!dxp|MdS}_gR6%)FU zXe`ZY-_ZZ4A3_|}$k&RWw~_Pxa;q+m-Jx6Fqd*i-@1deOe}7rrR%7ULdCQ6a^9RJg?8LXP^UIIgfq?_+@6LJifcUR}%#J_m zI@)S$tu9R?-Z<`SKO3{D^NytpBl)@SXoC*lfw66g!ZJv&b!hl!?4IV%>yl)DQ2b*~ M{1;qDXKauEH;R#Wf&c&j literal 0 HcmV?d00001 diff --git a/programs/bin/shell.elf b/programs/bin/shell.elf new file mode 100755 index 0000000000000000000000000000000000000000..6a5f9821ed3f95e4e940ddb0e7a547367de04c14 GIT binary patch literal 38744 zcmeHwd3;sXwf{c%oO5&U&2Tdl#@sMZNeBc87~lp7Bp4wIC|DIkNFbWY+#3W3f)FZ^ zhXt#(Rx$e2YG1XsK1F0uaJIFrTB|r!oGOU(Q=m@yeb+jB?|Ty9)!*m$`TfU%b?#bw zuW7Hn_Hc&W+g4gJ%VQWq9Ud`N5EZt=T;!aE*#C1TWmyq;!Xil|GOtag$*Mjq5$E$r zsza*J7@uT2^>Hc!ry_7F0;eKyDgvh>a4G_)B5*1Kry_7F0;eKyDgvh>@c%sm3(Gn_ zC^@I(?2?5g3+9(~d|P(a$7?-81h>Z(hgy?rw*(G2(y9COuIWoV|ZXoFiG$@o7cJm*pL=lioG` z%c}S9uu$`Kdw)^{-Jb>Ra^oLoO!}c^=MKNJwu5e->x^@W)BJmx5MV6QBCL<+#`l6_iWj|kIb^o z#n+V?U1cMVt=~Ilqcx6-J6)rGF zd3NUJ*+%W@vhUsG zeFuF07d9yOP^? zu%e?YXm7gm$&$x5q2Sf0Ys+jd~)=4zv?`VEfR#_Rjr4RK5txPWk+dlzcQMwsDi1tM%ly$I`JIM-*SQwutY}XUr^*6iqXS?)2 zsPymH38fswMHsUjioK8CgR-408?V+IviEpvD7d{uhU9D+OCHhh6g?hnUk9BS6BNt) zpx|&8wX5jG*6iSRF!Rhyjf=(wxBC$H4<;uE+s{`r`*u7?G9hKJ=#Rk-x4?S}mX>{v zWRo$2yi?>0ZnzTFyT;;FvWI3T~^JPwQdV^yLg#o#quCNbiC-D8Ym6 z7hv=u$v5=nQ7H31*gjhpTlhTmF(r7%>m0try=%A3M3xOiLdrBlH&V~ZroKC~NCexD zA?MVzHW_`PrhL6jzL3g=JZ2941d9UlaP1mH1UD>(NgmE6yWd8bV&iwMYy6h%zNZ_t zzZ)f>JSI218?~q#wXqxZdlz-+Xfm zZXsqA)zU-TA>qhlFGSms9YJP8N2%FLPE239nhD-x;MLymp(&0jY-LJzA5|2zY}%6r z7o9o$H%Hxb_@My#u2*CyuB)jRG1rhMG$WPlrtM+Lp0MK;vbmz;Bi^4~y9Gt-52Beo zt8Q+8>Hxd6;RUEbKOxUbc4u<0Qr2z4<^j_;P@WOhd%+#a^_fF0(1Z1M z^X1mTqhBGjmV$P?I}NHI8w-aH{(~yY$>8S8?Ssk3K%EB4N#aEAXUx@yhM~+6_ylb~ zltnB_@H=QfMk`RkODg>algLr5U{92F+(0c*J@iO1*b&--mv(KWupa%QtmB#Tj=vu~ z?+7z-ZlZ^G3*{;H}O?ya@vsC4|Dh_Q$vE%6&MgHLL2P&~R=| zV_kH`y!oN|kf{%?9FtcN5SMY`)Q~7ySyfkGwY0t_RNd6jP}R6BG8tx9iutP})m30G zsVonztZ9kVH8sM?;WZ7-(bXf!F)|urt0A$F&7wXM60@6{mMvXflZP-gUfkGpX`@n% zYC1xp1*@BDLc^#5!$P%9EupI8T!64XLLuS|>0|zirb|Q76?GA(ya9nl2a!h<4b6tu zf-kZ<60K?AnwEyDC>o(PheGv{7pS$iNTkRfiep8B9f2)Py3{Ej2Z|VWAN9yJicO62)xP{Q(j9QQ==*AcQRp z+Bt~{2A1fYk%Zz{M%xV6k>HDK2AN=Z&p=Ir$G!?5eQ8-8o;3#*6D_;0F)I4z10R)j z9LbCNabWeH$GS-8Jc`MaUjzpIU zZ=n(Y9jMddro`{Zr^sCjMmv50&i?Dco^%tH`pKUlp@zQ~JWnRn<9`zjDTJE-a-35M zwfz4=#L@`0{qu27C)Ddt25$zTasEG{iJ64@{8c#jBGm66482)|2K-}y_9ismzX`m3 z2u<+YK>HG!=uZXOkI*Fl40zg~(4hZSlnx*?+5c;x*@UL}w}K%=XsUlP&>TY3ieH91 zW%i(if%{-Kp!Ed{uOGBWFtU?x%JuAdCg4`kz)AgfF%o% z0LbqTMQ4M`s}`9?5@|md#j}XzmRuojv%^3kFN#o=Y2=)Zu8rG`Y*sb4I0rSJUyL=L zLg|Cx{T;wZh4C+-a7v<5OwFBW{FnZ=>G;gpO^T2mW&bY(ueG%%*JA zqni4RZCnB5GJua1d6bRE5z#nrm<<}@ZdCO(`ZZ@l=6TfI2L5XR*2qyYHd6}>VKKVD zu!_8z1g3AmeFPx-)q{Er`aVtiPpZ5eWG|2@0Am9Py1gS`!P*L7t5*Zr0FYL#S4U#4k+uwt zT`N_5mePs2*`jVxOs2-tB-6Ome<{y8p?MpCJyR0)aT3b2hr#*~fIWK#$Ws9285td~ zG86qZ$s!U02@_&wosUoP)3h}^=p9Brx=|!z-{z$ZB$`O#iZ1SmCE_`0h|s6@)ST?6xKe>AD>H; z9*wP)qx6fE&x1k?`V*M2@4MXkH8ygJ+tyf$sSDro(W_+z}@pn0cBKSDyq{{hXQ4z^C5I5fQ>3XTPRFPD5DpHwGO~WF9)(3 z;5OPXpo}UEvFBq(>J7^14G`K2;J_+ATMVbb%C+%#sJ<7#hV}y44e&9CJFuey%ACTK z$1_s{u*_|I4Wh3CxQ&X>779}m%Jdgt{S3gS4a~J&Alz-76i`MLrc{)swke~%A=nGR zMirke6s9DU(E_mM1K4N@kZAyEG?Dg3=SZW5C_v=sICB7|#K4FclXrP$-!4 zI-w5L%K@x>HIPdIKIV|Y9I6E0!zdAn?Koei4bqnNe}X$xAw%e(Yvo9azX`}DsJa?p zJ&h5gWGsyh;=ac-meAjm;lN+PbO*R^2N-XF*aPGdB0dE13XqopX0Lo9u9Eg+SU;)D zv!TBc0>_77{eWagfjA1}Ya%9q2nNsyfKdWs0FZuw_zDpDKym?i%y8#OA?}ez7D?5! z(8&0^P%#}m#ehVb&h|KgzY`@4bM z1@K)?9PUK^i!@;hg<(P9V}9n}l)1+sx(C2HrTFaQB$VmDf|XZL+F!m6;w^x*YEn+U zrj05LeZ+^cz}b6$28mAqY*8`UT2Qg8MC$_U_W*8e5G_aqsCM$oFg`x)7!V2zIu?a7 zHVik~jB1VCD7|8tqYd2CpOG zE894s@6-`_5>;DL^KogyCe8u-LI9gkT&8&0Wughy4FERL2INYB@BN-89?>SgLe+Jt z*C`V>g8c>nn^0V)plemtr}v}!J^-6|49FgUFP%8#nBi8uq)iM))%B@gD-*AR{qF!a zp}0&@=rZvcsy_j+i7p`D1AH@GCWfl!)0CL593`R;awf%C=)sfyVM;rdqs-d6cQv+v zeD(IWbk8Fcrc>R>yc;^4PEACo0yy0jqjj8!3Qay(a{*2V1DOJlQAvFeWBOBR)zp~I zJtM=$O_|~Jau-`utoHG&m!u4*8%)D#=8)Rc{ZjM9!5E-WL?MV+B7LB%yx1xCoAf-G zkMuFJ`fFU}FHmB(7&hNk4plA@k@^;*+3a&Pm>N^z>u;p_d!Y?9H7hP#V~WejcXpcR zAV3QN++@WbcO1J4a1&S?08V-Xxe}n#j&IrR^E-|iVgb@6#|SmR8D5HS1nUg|&O60s zGMkzq??&}q09OAnkcR+1QlxC8(#-r^%e7rvThEC1M{ZqHOO6 z>ni}ZeGte$0n+SPvig>`sxWnVJ;(@g3;zX)qX4$3m~1WRlTIp&es~=RV2izh^a7}c zW>NpCo3$SC2JZO98>u^S_mv062(S(Va4;2{CB6Wg>Z=k|PXn;}Gl9$l_~LMDOWELo zxZGBmq%g4Ykzk~b=`HQmp>R2Xo2D3Tp)e()Y+r?fHUQh+3gl-1X_l?FYO4wZ10RV- z>aUg6-=Oer09#dzHg1)KnN^7>tNT#!7=W$52;{E-x7GF9s=`#CBpIpwFa$ZahamMK zfUPPR2SIUY6^%TKw2G3V|A0XYHJKR!bh4B&*M~a#2v?>#_0&?90p*Y z6ubX8cI8tESf>G;z6UZ7pt{|8UO@My`FTN?=<^|Nwj6p51c-fa345+3-Hm+_3V~tRJ@cj4UCVWY!@K$@?0T) zEj4(=<=}d0&}V(|Y^EI*#?Ns2JJ|jPFc;^?SS>B2?%H{Nm18Ug$A@7305G@#gs}>z zqoBS9#IFb8$7U{$H*|c-`j|9r2!(->kC*!_!%m#@C=2|V0PZuzXC5b^`fLnX^8nmu zVIWfhvd^r}Jp9?bHmWdX^sWA{kwFzh4vc%{$W{<5Y20ikOEY*g`?T2@J@mfi{0{{*nn2Z1~QkVbj$k}5}x zA?OL%{yu+DhMouG9|1h=DK1mo3@)YoeN?{-VC7!{`4Zs!EpfQFN5iyZ!|kmPwq0+Dp-Lxh zijc|l;75%kkN5yZk7d@7GxV_MEGV7>aBlE6n_-}+5(-n@xi9l|<$o0HEdcHd#fNQz zlvNTcT>k;qYXIDqTY&ruAS2c9nn4%>F!*%pGCbfId=tpQci`ls!a?@~Gq>(&i{A-k z2gDu##6Jz<4Ip$~7XK2l;HvR^5M$l~u~)iLN$tQ9{|^)n2S*4{au7s{fzm@jYEg0l zfDRsX$$GpiOeNMcnXkb=?!HZs=m2o@6`v&(rX*DJ?*QxV0B-(&1Nj5Mo&STSQBx>P z&HvA4&J9VUe}d3+05+=l?BgVq(bvJcAHYUG0&)|vizQ0h-avW*q){GpS81a;=z>3|4$YCR8v({)03H#F%NB*; zQY|V$^)vu0KNHA2fNwT&s6j{V`nfb=@N?C_rgxADY71Q)E(fp)#bq*=@_#j|F9ooP z4j@+peDr))nHZ)7Y0%OByA7E$;#sU;4<7J@fg_*z3?;WvYLCRK^k6ovO5338MgZST z5@wj$4Dk{^x2NY1lwR%v$0GptQgNBgrM#re(LVy%%Qu0%4)Bp4<)uY|NY*A4rm|{B z`kl($ClEaha8g-Hm|9jzDAPZH^*aEYPC}w50HjqmI#e4~m{PGby*I+aJ&_He{s10DU!G0tOpkd}GqJpflFt)Zw7B49vESiV-v5Ebgp@QHzd41s%pM z)o2;-pcduO4r(#tIS(-7JgU7gK-IWWe_00o^+Qz(v0&G>zD-hX0=-S-q0+9z~G{E;WD2fkYQd|aVDF8Uv<8RU$0r5c^(4G%l;kU?*xd%CGg{z%n?KM zL(z%>&k)-RR6GKfhXL%0;xhRiFVC7OyuW)E)lUQ1mHj|o0r*BkPdrV`AA+@tImyNvITi0Ia_Uu+hhX>;<@u?$t&Wh7r!kMVa0a(&+0D+7Do( ziq8}ZQxeMPmtg%Iz(zf|6+y3txsAT8!>cg0UR<1cxiZ=ZLRkPds`zZ7FeRakjsWX0 z02`eIWFo+A^i6G4VTrP(^_ib5qh%191z@9!&lU<(63Xa>V7&mqMw@{&0;ExsviEZx zUWKV#G+Ht_bvHudrvSF7m~c%b1r@tW-5bGr1Ar~w4dgC>wCG&0)3nU%OqtH1bS2Y4_N)C!N4OBv9AEaCs9a$yz+~bhfzR| z<~#uAt&>sqCh^@4c~RI5^4g4KR8yPNwgG8RejVr>KoaIrn{3+qz-&{I$eDrD`Vn4S zAyO@ur5!=px{QwyP||W*tl~weZ%bZ`3Sz8Q6?ON4sWABu6r%Qt)VuJGErk#~% zeP5@Px;&&y*X3|&e;TYw)akUCI|6OJsme=3$kR5GM7&M4gcn_l@?_eZdD>JG4izyoyQ#Ov3IT{M32FfRTS&%ghi)k1Z(=d!RIAX)47^N3IXk@W| zF|mFu=J58Hyp(5eDc%g`br>=oh6KyL0y&;wBzMqbQi7FSM<2epi_y@8WZR1% zCM3rVMcB*U8;LSc7$^NMGj{dHCn(;FDg8mfpratnt-IUeO(fkBlza{-NCeUB36r20 z&u84h9)s;u5-NJ*o_HF7Uj^Ob33I(i+>cLVr1Lm>I;z9OLNQY;mUs*-YOit@sw)A! z$}9tNA;8BR@;Y6Z)PNnHFr?^ESkNIc`zLZ++99|O;Gwp7s2%*!T(NtO7YsRGUJs$` z0K6_LcI!BH`YP{LplA3rtTOv3KktuJOgDbJXl7RoWnks$QCJV=H^ zNil~~%7rIuO%|S>YE9fc=(3%Lq~#gYdXr}!YD8laxkh@^%Zw|HwlqRB9M=ui%1py3 z=ZaoA2tq!>l%=(!-7($0saV;Qf$NiJkTp`4lNib|$Uxsrj*Y1d^dmV(la%To6!AGe z3h{t=jG=69G%G>eW!ysD+?0hm?&jsBG$uK%8{jx`aT$QA zBoB#+9K&BHr5Hn!(&EkRzS#?Vr5MRbX56db6veK)2v4Kp;W+$-dyPqCdJ^|17jUC+ zHHIcpmV$*CPV)&TN3%c_r=hv=qyPzshwrk-nAsi2socL(D_uuS)9DqjvN;@YkO!Y!t~Po*sWZEKIgJ<0-+8hV zsMC=QdvUVyr&4radglWxf$*w_aF)pMjOny8h8y#(NoO19S!Y)R`@sHRnvnE%fvT`9R-?6{4fq!Km zu|XZPH+w;?_ub*c#~r?Ve8!o$e)k#x_4hWGuszc|t+(y>`SY0lJ$+)1HeCEps|3eJ-+88|44L9ZNO14VlH^Riop^A2nPB7A>+b1Z7cl8dSLo=pLZjg5sA+I{8u}PTzL%w^+1z_BvXZ*q zCkNdys&$i<$axIuN?C6S9|nu%OQs=X8gbog#&Zn~zMeH&2A(`Bb%i+iE|P5dK9sfT z0h7MrgbvadEB6$3r+e_JKeC~D&o+FTBr!Kl!_4rRe#b;&z@)EEp@Z3UxfIj1n-B8p zPgv3dCVi`lme=J{Uej(q$g96bNeYC}cJo1A|B*+)q;H4O^158gYue2RdHweq0h7K@M$7ARDX(caALR9) zUIa|~1{y7|%cZ=g-F%SOe?<{6={sw*ye^mWns)O+{zjQU=s)_F8!fNPrM#xQ@zLP^ zNNTV|BG!z{JFUcC#(5&n@TrPko@t#T?&PfSLjM^8B$4Bql*ToUYH%mXsTw}hSLA4) zb-5JNw40BYCiQ0lmWZ>q9)!A@VQJ5Gb2N|7K;Z#}hd(@+Vwn$1CxkBGeL9rYe8v4ct-t{6O&A-HqsnqdVK*^qsd2uuNrVQM zcp#x!+%wWwe9y?f_h4ZT3`mH$=>a+2>!GWX8meAXkMm6BgiwxB$0FX!J+zy?h>4C) z{0V5RoZ9WC-F%3fdVku7`Ap|5;-t%E+%)axgS>j>8uIitPL6%Z>vActX*VC_)vMW% zr!OYd^158gYue3+yi_k^qy6-aP>wv}qRUC%O}qIJUiGdq>07yXfLMIY&4KVk+`RMMU)Lw2F#- zjM3U&995)tia(%1o0ahJmhWbON~5D}_PY5urLpJM&+RMm_k|ptJ)feOE6XET_w*8BM$S z(CO;!J+&W<(aww>qiKi6hxdRd%!q|ARlyyCGciw`d4YwO!gy~YWD599Umm-t3;6z& z;nOrTzz6kOnwHQ7QbN;8LcOh~C3Jz5&@@YM=CR9oAgg@mneHCUZr1JQ113HmG2fw& z!{&GRaI#gNy%%aJ(`;_*w9FJ_ZOSkt%7qu2m!ROnD}Y~dmBIHYe3wghx<^S9BK~9k z{9Y?bhII0E25wvKHeSMZ+WOS!0u^J;GL)ChdMW1HzZq{4=Uc}6hOq=HeZwWsa^nJF zU_5wL0rqa*HnE1rf+!!(Sn8m)>M#ObMrOK;&c&A_Wv0oAd!aU*0AmR`PEsn(xz3eT zj69!GFh*X`jr{SZk>^^8XCmjXrkyLS78WKdKC;eoDhFsn#1Oky^>6u zB8X|)saDw{8=~IW3OFuk(ld!&(6m#%R_az0PKAr zw3`ou{ShgT)s(*Ro(^_PmrHp~_u$iSEFm?K37VAioTi=3)vp?1Bdo1KOqWYBO}qKv z?O_!iCs>-4vD7qmgzNAMPnbzd&>hKeHCC|dT1*kO?U*902aIP76hC9UY&g@VGeLR7 zT1wy+rWk2Pv4)MDXQp_Q^u=O@;mUO5~AMJ~XmAJMo?6gLYk@z^?7`IEtEV=~6 z@)V=duXK6mRV8>&A6LlN8|pbhau!;H=S>et4LplsA=I?#!?XsKx_~BKH_Z(7f){d} zo#!}e=2b7<3aS2OaL{qU6UI6Aws=^sBkIqb)DE7$MCC-sjcAm5W@Ia4D6@Kv7WVsk z?oF`Ldkg!prfn-R6_2B3Khy1~m8F3PnerwiT+mq5yI|9_qTFT^iR#=wR}ldpMkV?be^DJf2wle!#Lq`7Cq7Ii}AYIsM!*7W>*8J;XDOTXO z`~&bUGxDm+5?(7F;rw@Wf6Z@OWS|?3?ooYDtwtcy`dR~r{oj@TN?Y(+-Vba%D*VB= zez4z25O%k!MAUp_TOZq>QZ>F9sC#w1Q~iUz9yQNsowk*bVP)m?D8d^n8NxoOxnLP> zi#^~vWZ@avZ{n;&ai7KuJN9hrJnMZtI~4W{Ue#Y?tlQ#li$mJ!=PqUZJVx02bxB`q zP?4~U^%IM9E4`o9Z?Le{*Thip;!=biyEh3sDC{%z4L&FMvD)Nu0{(HJs-$qKXQd6zoh|G^G+NuR@^s=!39euxxKY&E}cl5J8`2QWfL*J(MTKEI+ zI5n?)XT}LRIA1p64oLKZI5j-KFymCt;;*Uck925ix=>)=HH>eQ@TEwkDi@xg89)OeA9xD9UpM+6`%2)BO zj-6(wn_>P83Xg>cTSL9(RKrdn!kq4_wYj*M$T@Z|e0aQhPA#5mXAwQeVe!tf?bE%6 z*PLc}*V*YT?_Xs1W)aV@R6B<;!_MQ+Vu!^bKJP1b3Vl}C`BXRAPUF(4M8FEy?WCeD zti;4~{8W1s3eY~uTkLQtdL4=4?LEtLuWfr5+lgMo^4L?%tYX`5-qsei3_H<$RxCng zJ1UaROv4^wAQZQ@8MZ$y?E>&4C|>g^VRV2Y!%Q{oQHDJe2GYIeuiBs|EzP?KOiAYJ zVi8#U=C9j|z0;N8p9NX$?=^$0$-E?L`@N>$mztJF1~VY_veCDm&d!SWP4^&Ffn zk2yO^+2ls{zKQx|uTgBLdCk>m zj2R#|Qw)0u@!sU{-l_bBHmWr*HS9rDf4fuv5!V~$J8aB6+qaInvN;;sqW4xWM~oU_ zCT(GsVE0$-ZrC@i4jZRaam93|X{-JTpI9Q;Sp>Wd6dam2{q>Lv~U=wPZ0HD(&tu zhmz9!=g^k~bGDO5=8Ti@-2lEc@FmfR*~%(QPQrSlVlB3fd^@QI-&Bf{Vdx4q|CxJG zz!T5wYZ#F;&oHlfCu*Fj=gl>~;uFk0i~jey=PtQFKVgX|(Z zajTsIY9NH7m~Z+5??u{9PfLTg*>=j5MI;<@GU&g1_~zqjgPYI3KhUFIraoGytKA70 zYuc}!reQWi=g#xZS&mdO>3gX%>n zf{Ip}&-*z>@9W`{o+AGX%AEPtZ@#dGG$fk8hX$2<_d?L^t)6o3gCMOa+^QU}S#E5B zFrrjM7SqkyAZU`@3EmDb@CkDOYxkNvh^dpKMKc}G?Zgc|&Xh(2 z)Z;Z{KG=yjF#DZ7b;^kgZRFvX0WT<+&u~*S%twq`R5`uDU*`rpv*1gNH>*lQqYOvkYxDeWDFlkhL3wN=*b3trKKY|ZU%#F zz@IPyx*PBpvSxlln#`ZB$)ni~rhh>6{7#G4B2bvg)6Fg@L1&(e^qxgIg#8ZhC1?Uf z3ax`ka%G2?LcHEapV__VaHEf1$;)SNb0fl-=xyaVz6@pV6zx8vJDHwguJcd_&NKhM zrg#g&cSjrIN;=GMWy{~~p~kyK#uq$zV#3o(qW3;?E?Sk2HvEnQ;mmS!-h-n&8{(~f z%ixpRbw&U8neRpD51O~GS!XAZjyZP1PCHnP*$orb=_pIK0~MIKuvx>HF2jD~r;Z^G z-W;;#n*-joZoH}9bURqa>~kIVBJ{^x7;q-(cd<_;$uBA7H?Jw~CVWyu2%gsrVBUn9 zWG^kn>(Ng^FYP@Nuy+g=KZ~*->Gl7v4DCk{uvzP&PBo0Yr;V$tO~_L6Z|fT0x)U`= zN;tb>5|rIyekx|E@G_*{2hbJLJb^hryY?w>EYU$5t= z9zl!U(~eqy-;o3P>mK!T{YAR7_*Xruoz1B;VXt$htn1tG-Y@xJ9$>qjnD?HD#Tk3x zZVo_$o@kRDBTx5W(e)`#J+4f;snGSQPW_W7t50+4pEy~4x>Nu7$?7wl`h6#>&vfed zo~*u?Q$OuQ?i*&7aWCcNdx%;JUE83if4GvCpmfZ?wb*3Q#S!fr-J?QJv?;t;faA3u z98Ppljb|yS-ru7d9gK+`7sU6%5=GGlaJPFT|`s@zUa=+3Nl z!-|v0eddzeBwO&cORn}ra)$YZ!0s1eg>xL%e<63=;~{oQ^ADbM2hha}k_a*U ztrNq`=-T8j*!1C%=MwxS=o0)Y)$+zA_4Q)OxpOBtKM_?(za~^#Cz@O6SA3fBJ5iBV&42x zQQw50#o|+IV+8i_TUVqSzjYL0uEpi06*EP3!?Gp(^H%r?C;B0(FMO91bN|yoB8*9qMmKyvLS6xeK<>}Rx^9z>L3CKk1E~^odXjMxzvgA_yP8M+d zR1-ULR%yvhmw1u#q_p7#r3Ix84Wgm05pjSQ1rrb)m*wVG$GK{*9XA%WvdG1y3=~2Y z4)&pRQTYNF%XsbvY5@7}1gN^H70oOth;?98)YmnlD^^F!=?B87OJ5et`qeYx{0y0h ztY~V9hPw6qr4%tlWNA}VJ;$dAgw!^ptmG^q`wd!aYU@^s6;+X$b<6SFW-%{{BK0*j z%}W|0=o|bVS4$o8ap~%4P2?J>HB7f)$ya=0qXXE+GHO|Z_vYF1U(;OE$y8Y7@Lhc|}F}{L&fo=FXfimQa@#j4jgrC#st3RBS1! z=!fWZ-%_`_`lyh)v9zvo1yY-M>1W+AH?%}*mf#QFoxD&PGwZC7|PW{S04(Cn6qA)hk*XFBZ|7RXTI#m!Dh80_F3;UvhRyc}2i)TUy&M;rIm9hIO#r7 zj=@qvzsrX&Y&rvj581$@*FC00Q*{*HxV6baIhJOF6O@nRS%M=#`S6HEOv7wdSQ@DaTH6-^B_ z`OB&r>uTzAS5!4L)U@O;t67=vgkawMd{|wMU)GC=NK18ognouDzoDwGF|WEA{jQ4e zV}{lBt;=fi<(!n4mw$0hOJhxazB4!)m*rOHu2|L*$;W71GNCYE@@u)0<~ngAp;fC+ zB$mIl4&nas{A%jRqZIVzK4$?^sa)O^)6e~N*DN`=Vxk<>wBUf8#LEv&0+f*zMUrg3h$*$IFtd|bA}_MK zAzHN*pV1cQbA`fqu;#Ti(R?KGmenk6UG6Ms60cgiw54XHs;O_P#_u6gmpPnOEme)n zF#|}lrEz(;YF+?k9SkZqDn`z?PyqfP4}32xqK7AfT2I))lAVvQ<8=ii;qIi!0ZsB_ zRe5sQ!9vc*5jC# z1C>3?i2r4N7gdNSDN$WVPu}^c>sdzZzm%QR?2;w4C-2e|lo21^(&6Lgf8#%y>8Xl}f5q+9|%$rY79_#<*Y{5YEucYk~?<z0#|lbaSD(V$xr#Y!*P>hl%GB2 F{|{q!r4;}G literal 0 HcmV?d00001 diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp new file mode 100644 index 0000000..a66e7a9 --- /dev/null +++ b/programs/include/Api/Syscall.hpp @@ -0,0 +1,51 @@ +/* + * Syscall.hpp + * ZenithOS syscall definitions for userspace programs + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Zenith { + + // Syscall numbers + static constexpr uint64_t SYS_EXIT = 0; + static constexpr uint64_t SYS_YIELD = 1; + static constexpr uint64_t SYS_SLEEP_MS = 2; + static constexpr uint64_t SYS_GETPID = 3; + static constexpr uint64_t SYS_PRINT = 4; + static constexpr uint64_t SYS_PUTCHAR = 5; + static constexpr uint64_t SYS_OPEN = 6; + static constexpr uint64_t SYS_READ = 7; + static constexpr uint64_t SYS_GETSIZE = 8; + static constexpr uint64_t SYS_CLOSE = 9; + static constexpr uint64_t SYS_READDIR = 10; + static constexpr uint64_t SYS_ALLOC = 11; + static constexpr uint64_t SYS_FREE = 12; + static constexpr uint64_t SYS_GETTICKS = 13; + static constexpr uint64_t SYS_GETMILLISECONDS = 14; + static constexpr uint64_t SYS_GETINFO = 15; + static constexpr uint64_t SYS_ISKEYAVAILABLE = 16; + static constexpr uint64_t SYS_GETKEY = 17; + static constexpr uint64_t SYS_GETCHAR = 18; + static constexpr uint64_t SYS_PING = 19; + + struct SysInfo { + char osName[32]; + char osVersion[32]; + uint32_t apiVersion; + uint32_t maxProcesses; + }; + + struct KeyEvent { + uint8_t scancode; + char ascii; + bool pressed; + bool shift; + bool ctrl; + bool alt; + }; + +} diff --git a/programs/include/zenith/syscall.h b/programs/include/zenith/syscall.h new file mode 100644 index 0000000..1ba0705 --- /dev/null +++ b/programs/include/zenith/syscall.h @@ -0,0 +1,157 @@ +/* + * syscall.h + * ZenithOS program-side syscall wrappers using SYSCALL instruction + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include + +namespace zenith { + + // ---- Raw SYSCALL wrappers ---- + + // The SYSCALL handler does not restore RDI, RSI, RDX, R10, R8, R9 + // (they are skipped on the return path). We move arguments into the + // correct registers inside the asm block and list ALL argument + // registers in the clobber list. This guarantees the compiler + // reloads every argument on each call — GCC cannot optimise away + // clobbers, unlike "+r" outputs whose dead values it may discard. + + inline int64_t syscall0(uint64_t nr) { + int64_t ret; + asm volatile("syscall" : "=a"(ret) : "a"(nr) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; + } + + inline int64_t syscall1(uint64_t nr, uint64_t a1) { + int64_t 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; + } + + inline int64_t syscall2(uint64_t nr, uint64_t a1, uint64_t a2) { + int64_t ret; + asm volatile( + "mov %[a1], %%rdi\n\t" + "mov %[a2], %%rsi\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1), [a2] "r"(a2) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; + } + + inline int64_t syscall3(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3) { + int64_t ret; + asm volatile( + "mov %[a1], %%rdi\n\t" + "mov %[a2], %%rsi\n\t" + "mov %[a3], %%rdx\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; + } + + inline int64_t syscall4(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4) { + int64_t 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; + } + + inline int64_t syscall5(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { + int64_t ret; + asm volatile( + "mov %[a1], %%rdi\n\t" + "mov %[a2], %%rsi\n\t" + "mov %[a3], %%rdx\n\t" + "mov %[a4], %%r10\n\t" + "mov %[a5], %%r8\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3), [a4] "r"(a4), [a5] "r"(a5) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; + } + + inline int64_t syscall6(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5, uint64_t a6) { + int64_t ret; + asm volatile( + "mov %[a1], %%rdi\n\t" + "mov %[a2], %%rsi\n\t" + "mov %[a3], %%rdx\n\t" + "mov %[a4], %%r10\n\t" + "mov %[a5], %%r8\n\t" + "mov %[a6], %%r9\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3), [a4] "r"(a4), [a5] "r"(a5), [a6] "r"(a6) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; + } + + // ---- Typed wrappers ---- + + // Process + [[noreturn]] inline void exit(int code = 0) { + syscall1(Zenith::SYS_EXIT, (uint64_t)code); + __builtin_unreachable(); + } + + 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); } + + // Console + inline void print(const char* text) { syscall1(Zenith::SYS_PRINT, (uint64_t)text); } + inline void putchar(char c) { syscall1(Zenith::SYS_PUTCHAR, (uint64_t)c); } + + // File I/O + inline int open(const char* path) { return (int)syscall1(Zenith::SYS_OPEN, (uint64_t)path); } + inline int read(int handle, uint8_t* buf, uint64_t off, uint64_t size) { + return (int)syscall4(Zenith::SYS_READ, (uint64_t)handle, (uint64_t)buf, off, size); + } + inline uint64_t getsize(int handle) { return (uint64_t)syscall1(Zenith::SYS_GETSIZE, (uint64_t)handle); } + inline void close(int handle) { syscall1(Zenith::SYS_CLOSE, (uint64_t)handle); } + inline int readdir(const char* path, const char** names, int max) { + return (int)syscall3(Zenith::SYS_READDIR, (uint64_t)path, (uint64_t)names, (uint64_t)max); + } + + // Memory + inline void* alloc(uint64_t size) { return (void*)syscall1(Zenith::SYS_ALLOC, size); } + inline void free(void* ptr) { syscall1(Zenith::SYS_FREE, (uint64_t)ptr); } + + // Timekeeping + inline uint64_t get_ticks() { return (uint64_t)syscall0(Zenith::SYS_GETTICKS); } + inline uint64_t get_milliseconds() { return (uint64_t)syscall0(Zenith::SYS_GETMILLISECONDS); } + + // System + inline void get_info(Zenith::SysInfo* info) { syscall1(Zenith::SYS_GETINFO, (uint64_t)info); } + + // Keyboard + inline bool is_key_available() { return (bool)syscall0(Zenith::SYS_ISKEYAVAILABLE); } + inline void getkey(Zenith::KeyEvent* out) { syscall1(Zenith::SYS_GETKEY, (uint64_t)out); } + inline char getchar() { return (char)syscall0(Zenith::SYS_GETCHAR); } + + // Networking + inline int32_t ping(uint32_t ip, uint32_t timeoutMs = 3000) { + return (int32_t)syscall2(Zenith::SYS_PING, (uint64_t)ip, (uint64_t)timeoutMs); + } + +} diff --git a/programs/link.ld b/programs/link.ld new file mode 100644 index 0000000..8b4af41 --- /dev/null +++ b/programs/link.ld @@ -0,0 +1,43 @@ +/* + * link.ld + * Linker script for ZenithOS userspace programs + * Copyright (c) 2025 Daniel Hammer + * + * Programs are loaded at a standard user-space address. +*/ + +OUTPUT_FORMAT(elf64-x86-64) + +ENTRY(_start) + +SECTIONS +{ + . = 0x400000; + + .text : { + *(.text .text.*) + } + + . = ALIGN(4096); + + .rodata : { + *(.rodata .rodata.*) + } + + . = ALIGN(4096); + + .data : { + *(.data .data.*) + } + + .bss : { + *(.bss .bss.*) + *(COMMON) + } + + /DISCARD/ : { + *(.eh_frame*) + *(.note .note.*) + *(.comment*) + } +} diff --git a/programs/obj/hello/main.o b/programs/obj/hello/main.o new file mode 100644 index 0000000000000000000000000000000000000000..781ee69cd2d4de0e7526ec2dcc11328a0de192e0 GIT binary patch literal 8416 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 literal 0 HcmV?d00001 diff --git a/programs/src/hello/main.cpp b/programs/src/hello/main.cpp new file mode 100644 index 0000000..3cb35d2 --- /dev/null +++ b/programs/src/hello/main.cpp @@ -0,0 +1,18 @@ +/* + * main.cpp + * Hello world program for ZenithOS + * Copyright (c) 2025 Daniel Hammer +*/ + +#include + +extern "C" void _start() { + + + zenith::print("Hello from userspace!\n"); + + // while(true) { + // zenith::print("ab"); + // } + +} diff --git a/programs/src/shell/main.cpp b/programs/src/shell/main.cpp new file mode 100644 index 0000000..bf5079e --- /dev/null +++ b/programs/src/shell/main.cpp @@ -0,0 +1,298 @@ +/* + * main.cpp + * Interactive shell for ZenithOS + * Copyright (c) 2025 Daniel Hammer +*/ + +#include + +static bool streq(const char* a, const char* b) { + while (*a && *b) { + if (*a != *b) return false; + a++; b++; + } + return *a == *b; +} + +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 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]); + } +} + +static void prompt() { + zenith::print("zenith> "); +} + +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(" ls List ramdisk files\n"); + zenith::print(" cat Display file contents\n"); + zenith::print(" ping Send ICMP echo requests\n"); + zenith::print(" uptime Show uptime in milliseconds\n"); + zenith::print(" clear Clear the screen\n"); + zenith::print(" exit Exit the shell\n"); +} + +static void cmd_info() { + Zenith::SysInfo info; + zenith::get_info(&info); + zenith::print(info.osName); + zenith::print(" v"); + zenith::print(info.osVersion); + zenith::print("\n"); + zenith::print("Syscall API version: "); + print_int(info.apiVersion); + zenith::putchar('\n'); +} + +static void cmd_ls() { + const char* entries[64]; + int count = zenith::readdir("0:/", entries, 64); + if (count <= 0) { + zenith::print("(empty)\n"); + return; + } + for (int i = 0; i < count; i++) { + zenith::print(" "); + zenith::print(entries[i]); + zenith::putchar('\n'); + } +} + +static void cmd_cat(const char* arg) { + arg = skip_spaces(arg); + if (*arg == '\0') { + zenith::print("Usage: cat \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 handle = zenith::open(path); + if (handle < 0) { + zenith::print("Error: cannot open '"); + zenith::print(arg); + zenith::print("'\n"); + return; + } + + uint64_t size = zenith::getsize(handle); + if (size == 0) { + zenith::close(handle); + return; + } + + // 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; + } + + zenith::close(handle); + zenith::putchar('\n'); +} + +static void cmd_uptime() { + uint64_t ms = zenith::get_milliseconds(); + uint64_t secs = ms / 1000; + uint64_t mins = secs / 60; + secs %= 60; + ms %= 1000; + + zenith::print("Uptime: "); + print_int(mins); + zenith::print("m "); + print_int(secs); + zenith::print("s "); + print_int(ms); + zenith::print("ms\n"); +} + +static bool parse_ip(const char* s, uint32_t* out) { + // Parse "a.b.c.d" into a uint32_t in network byte order (little-endian stored) + uint32_t octets[4]; + int idx = 0; + uint32_t val = 0; + bool hasDigit = false; + + for (int i = 0; ; i++) { + char c = s[i]; + if (c >= '0' && c <= '9') { + val = val * 10 + (c - '0'); + if (val > 255) return false; + hasDigit = true; + } else if (c == '.' || c == '\0') { + if (!hasDigit || idx >= 4) return false; + octets[idx++] = val; + val = 0; + hasDigit = false; + if (c == '\0') break; + } else { + return false; + } + } + + if (idx != 4) return false; + *out = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); + return true; +} + +static void print_ip(uint32_t ip) { + print_int(ip & 0xFF); + zenith::putchar('.'); + print_int((ip >> 8) & 0xFF); + zenith::putchar('.'); + print_int((ip >> 16) & 0xFF); + zenith::putchar('.'); + print_int((ip >> 24) & 0xFF); +} + +static void cmd_ping(const char* arg) { + arg = skip_spaces(arg); + if (*arg == '\0') { + zenith::print("Usage: ping \n"); + return; + } + + uint32_t ip; + if (!parse_ip(arg, &ip)) { + zenith::print("Invalid IP address: "); + zenith::print(arg); + zenith::putchar('\n'); + return; + } + + zenith::print("PING "); + print_ip(ip); + zenith::putchar('\n'); + + for (int i = 0; i < 4; i++) { + int32_t rtt = zenith::ping(ip, 3000); + if (rtt < 0) { + zenith::print(" Request timed out\n"); + } else { + zenith::print(" Reply from "); + print_ip(ip); + zenith::print(": time="); + print_int((uint64_t)rtt); + zenith::print("ms\n"); + } + if (i < 3) { + zenith::sleep_ms(1000); + } + } +} + +static void cmd_clear() { + // Print enough newlines to scroll past visible content + for (int i = 0; i < 50; i++) { + zenith::putchar('\n'); + } +} + +static void process_command(const char* line) { + // Skip leading spaces + line = skip_spaces(line); + if (*line == '\0') return; + + if (streq(line, "help")) { + cmd_help(); + } else if (streq(line, "info")) { + cmd_info(); + } else if (streq(line, "ls")) { + cmd_ls(); + } else if (starts_with(line, "cat ")) { + cmd_cat(line + 4); + } else if (streq(line, "cat")) { + cmd_cat(""); + } else if (starts_with(line, "ping ")) { + cmd_ping(line + 5); + } else if (streq(line, "ping")) { + cmd_ping(""); + } else if (streq(line, "uptime")) { + cmd_uptime(); + } else if (streq(line, "clear")) { + cmd_clear(); + } else if (streq(line, "exit")) { + zenith::print("Goodbye.\n"); + zenith::exit(0); + } else { + zenith::print("Unknown command: "); + zenith::print(line); + zenith::print("\nType 'help' for available commands.\n"); + } +} + +extern "C" void _start() { + zenith::print("\n"); + zenith::print(" ZenithOS Shell v0.1\n"); + zenith::print(" Type 'help' for available commands.\n"); + zenith::print("\n"); + + char line[256]; + int pos = 0; + + prompt(); + + while (true) { + char c = zenith::getchar(); + + if (c == '\n') { + zenith::putchar('\n'); + line[pos] = '\0'; + process_command(line); + pos = 0; + prompt(); + } else if (c == '\b') { + if (pos > 0) { + pos--; + zenith::putchar('\b'); + zenith::putchar(' '); + zenith::putchar('\b'); + } + } else if (c >= ' ' && pos < 255) { + line[pos++] = c; + zenith::putchar(c); + } + } +} diff --git a/ramdisk.tar b/ramdisk.tar new file mode 100644 index 0000000000000000000000000000000000000000..c7658e0a87c25737d346006b9d842519d38d3492 GIT binary patch literal 61440 zcmeIb349b)x;K2PtE$u8SvosmO^1Ck8w7|Lv;#yEj1UbdIBE=?4vA(n-9c~xm3Sq? z#Q{ghaa_==j?TT#IOBCeaRGN{RmX8q$8lE}#`P*P<0#+%dFq_gl>k@o_kHuezxVfc zB~R7!obznwIZK_YI@MKDD*owP8MfK8X9^t3XU{BiA1RAAQ9iT0VrKdD*|W;PS3YC< z%o!py^Plb+4D`j~&Ao7`rMas;+ELur+}RoJJ&fUR%0G+C*Z+*N^75heKeMc&LWIixX&$*0|6gAJ>uc(d_ZWr_h({bN94gFd&R&RTj?ye5 zEGoeh78xR)d2PzdRQX|vIL8q(T^$pAia&WwMj#o1WCW5CNJbzTfn)@d5lBWL8G&R3 zk`YKoAQ^#wb_AAfw_u`n;N9w_jXOkU80hOkyZWJFKzC49Yimicu%F!s7E<=^`hoX@ z_P+i9Y|oPVlMzTpAQ^#V1d;Yj1aF zs4o`njrBA~qD2AGyiz=43SkSwN0x%LE23QvAWDd9GY#(wj}WHEo{6OcnU>w&6&ESR zz^4@~0-x!#ZOS)%r!rg0m2|LDF2Et>D6q{Qz8Y*kT7hEvQdT25E0k&d#K?RfnLk0Q z|8gMzZln|L6aEOO|1g3-2bGyg>IEsxv`)YwdoMDrdkpg;%FT(zTZFg5NPQJlZpz%$ z`*9S33^3ZMkAu^A1Zc*&lxqAAbdXTPC_-u`p&sLNq-GIn8XHMJp_UN^nnS21*|Zx? zyUDbhLAzsVS4X?0v}>Z>*|h7U-Nm%qM7tl*?q=HENxS>8L*<_ZKYWw0GY!wnNXok( zxZ%$dA`GiS%;7zft{;vzm47;RD$A1w)JV&I5E51t85y7{&F*z^S%paPoIk_50ErWU zj|W)&AZ7un0HjXGE^RZ*2%9XFN#mXx`wly+)^EdbU|tuhWs zB@U%s1Wu>22Tp;JVK2gO&-q!sNSOoLONpXj9;J@T{RD^OKzm6k?@;5u5~QY&2OpJi zL@Mbze;muDlZqb!GP-)jL=wtqE*CQh8QB|+qt>!W9lDNv7nGPrCh0OgnZJM%)5^Sq z4!)FgDKXQgx-v6UM&KZf3Snlj;=mjwEb6AKW*C*JQ)B+HB zCB|{XrE{rSMs`ZL;QZmlm48UlDRr2ey(^=NSqlayQgBhrK@^h$GXh0&3eV(7)B`yq z!zMPOZfuNbw0UT#4Ktb_FzHq-ZQC%`B+@+B|%UP~n2XN&(R)B>$3wMky!)vAGBkNlwNb{WK z4%OQmWV(a(Mq7%{ z@9v`XNN0CTv}10pv$>-qRJQsw>wyLO(fbAZ}0Akfo|>ViV$8LgBF?A z(j0Hrv}jz>*i_eeLd}ZmQ>yFgtLN3%hxMCS9L{O zL?04nRW!v#tgX8@9-=u2saaUF^u)UQ`ntxN`AZfrXymk$YpNHB^P^qu@ir*xY+lpU z+yNy`9WA2iw8gWWoS8kn&{5NHLS!&chqleSmNe8X7Lm5*-Wk(jn~?HZK&0vUwbdsx ze*DQbHH@sNtEpcgR(5xHFxgPIc%djSPxzok8|oH_j&4MrcT}9y9FIvgm0&@gqq?!a zrlz6k#6~t=nXp~A`1mCvwl)?)4VOErTDtpIc0`4WS%}F&JSvb4C)X`r zDua7SW_5l2lKJ91(gz2cXic?KkecOnOW6YIVQJm`6B-4FRV6jno#uoE*{E(;_$!s? zHJzOz)*ErwDVKIOw|A99dU`}@TX$!)bkM5o(w6AzQYT1D8cWeiSM@e`#uCD!y(`ku z*Agw2RbNt4dQP;rE80=&wDhi);z)5@OK+?cU8`wUMXBW1a@9TU;&4K1)*Mc(bY*)y z_CMfPbydw=P4yw^FlDUADONpClGVo}l(UD>w^-Lth@ z;ms>o_C{ALs-rv7(H^7FIGoMB&0VXan(XabH7L8Iy(_9%VNjuz4A**x7U=&aJrZ&h zBd?YZJD6O*tm7i{WwPcj0!H#Q59Ljse?`b4M>q$)Ctljraf%D)47nQ-a*$tRTIk-C zoJym2C5EO^-h1O5jvo%m972luN047P!IA%0=b-mP2=a5J ze9t$?Q#%-{lSKYq%769^^6RAhP(N>+bw0?7y@Ban>1|EdTqqgQ`U#bdT* zcmb$(;LF-e{(3R)F9z>RsS5RFv~CIBH{Yn;@VHSGihtI>GuR);jlb=5^X~z=Q(C+E z-c!Fic+mBZkP8hKE)Zg4nzwe~vEY3h>49|ZhMh+Lvwi!5_nEbkUvBvOMeCdUcgD}y z@R(6I@N#YBg}N!vy5zqRrT39GTKUz%`>d?mf#>^oRtK-%39aGajZfD?X?6dBzSnCb z&(?3gZ8^N$PWSeo>Q5(J@JmdW?F??Dhr{)eH@4FaRZgra-(NTIbp7U^?gn2iSo)tq zmU;)Oe&EaP2T}g|DgRY_^y9U`g}b-o88u1P4}4fZ@M+z^D?$6Ro$E%|Mt1Y7RZsPg zCPm=*C}`Iie?IZ(uX`WdPPc2>+IBoA7dz-F48M3)d&v_N{_j+uUVTROnbl2aJRU5( z7@2}~C0@|l_mWJneh81;MfJlY5#IWNH`oe}tZYY_lgPxrTe&_GmiK+1;>YzbYk$LE zL$#M34EEmyjkN=RE8kzc`HWxJZU4=$oy}bz`kk=afoJNzfVpRCcm2hz-CT8Lt#Pn+ z%7G0#%QjjwD0$%d+JR4NH#~(_bijY!bG4Djsvn^;*Kh8ye^qNdUN_|rF2!5wgO7}> zANZJKQ@dfGQGfKlzJ0YDo(Rd1obh<8Qx1+Bw?TSgZ7_%Su5}BSZ&~%jB^0Hl)en(a zW zWU-%v`%hoUsmB@A8KmC@uF7uRQakYH@@LC;mLJ?#NE8=e^CJD0b5!vU-B!A&*4-n8tw9VbG`uD3FG zz2Wmew`tjnsWs134?N8g*!5OHwf~O`1}=Jn!?0lBAoqbiCnBh9hLaswj4FmxsB(JbDqA zZ%_Te!JxhAq9>{!-GqcazeP{2A8=A@BVX?LJ|({V4^DLS+~9s4662d5KOZRvFOQ=9 z+9ckS#8664jo>l8Q28jb)ytFB4>e>ZC{6_rjyqmrE;Q)!SF0b*=JFm0+5?YO1>$q7 ze8K*&pzz|q7p~iL$(LUvcl{d+Hb;!w$g3Q)jaQA{4PR@6kGvwI`u4s@Rk^v-RX;={ zjqYEOS7~&D2*&%%SakCZbimM`BjpCS-(Y_g;<^CicMY8k?uN43fmap`?0n=JIQ~Ez z=zTtJ2ekuVSMKT?6})dAI{sru?dEj`NzkIl;C+kfEbvU=}?9_tGQ@2i#}IYq{jd-R)?j|KZLg--Md zisetCU|#{1tMd82!r*;i=8=~Q7f%V^=R@4zos${tKSRmv+I|nogp|F?Uj{F`9^O;1 zwCuAan~4$R%}QVJvWt+tV>))#57TJsv?(Rey$A0HtK)`!p1ysq;mhf;x|!Dxpm~?S zN)0^Nena+3#9%Lh>#2T?zFQG1`Fa}D>a%U%s$vFFQ!2c0t+PLS0PJ2RIz z_|#whNC;KJCB%rLO1k$xNI3GC3(<6BJFV#EldKx8n5cf}GgmRedjP!J`-f?WV+vE5 z>W98i6smjE!v%2B8N+|_g?kL&8zA5H8QF>JTB=2iHRK76NY4A_)enaquh5$72mV6y zsNfY_khEbBib>;k?dJX`-(Z(6dk!j4PiSY=59M>MQmfm9$peOOpgba~@eXwDCKkb5 z)`y8(-_v`0p$F&N%@ltJ9-bqMv1U^97_ZAQfEo2VbkJ1^a`~}tg#*@ewIDJS|h6mhXhx zAobehmW)6$0?7z0i#4x`&Iv`Dh6hl zg(itf0U=MZ3)#isH{055el6gJ+}|bi9_-B z&S*=hyDuK##Ga0|&g(~F4rk33ow0zxnv@FrM4{-s)dJBb!Go-i1w(j#n@wWDulirEIqJqdHiiT!GX~Bmjvhipq z=k#_q(^6`!ITY%MCAdSOdaSq%VL5C|d+eMLRWyQ3TPRWW#}fa7_E=9xGpYnEC|XxM z+7&0(P)OA`vxOR?U8v#tCpLtlk+$wouT!~FDeHAAi*(Y){>r5Gu25%t2NrooVW7p4 zfge<>4m0-inToeXL$L^!q3eQ$LR9aXEmT7kvr+X2MBrP6e{DIvyWqUrHp{>%x@am# z1MmLOG{d!}`BHj7rWxK7k(1`JFTr6XomMCFyKS?bx7|k4&uC98IGtF1_VYN5d;q7} zl7cQ4_MO1ODeahwj=Udu#qi51!%FERR=PPcdpNl;?F#HFotNKs;lkjTAbABIUVbCo zCyp9{+gNz{jcnlMH(2{Mu(V9;W9UAbUTLG3-)^MbIqKy%dim=Zy!^HsM^U^IjCSf6 z?EP1PJ%g9_`N^Lkp@x4LJkKQ5auc<3!4G~h1-I-Jl{|0eK`AT-Ty106|dx<4D}C_*#*^Wo`e zLWBN4B6SR*nf@OGEhIF{zZDE2LbLt!9%2!pIaM#homzWb+SpyNH9q4~AOU($EH?xy`{FhGR4^_jILJ>4<93g}-17FrgX1}Gg3 zMW=u&iHLk7gS4N91QRf@{J;T5iR8*v}ToK3wmkO^MmQelSsV> zygvi*o1Cf7A>|cNF9NU?^r0dDUTQQ1y@xi*81*1D8taf_!e6>gm9hn5ip>;MRL=#| zjBAlfFN02{V%-XYf;|V|V~)F!qHKI9ZFs~r5FT!f8iUZW4SF^2GyvOBY!wg~Dr+FS~Jf#16kUs>l{)d6w3lPH>AWNi#v2$7r9X}gcNm7l-dJ|04 zS&SwSHmdL-NwNO7f$h&*sr1((a~{<(`QHg70&w)7HAMe`kv~-Ww}I(8k|Oy2%Sl!!)*0SY1I@8qnj7B*+%|eWwjhqr2w|780|wul+`d;j|H&R1|W+8ZmXt0 z%r;D%#T+b;X`Hl-{3dw7Wo?4g834Ab81ee16iJsN%IbMw?E|pY%Yj@5kXEffPQ|%K z3K|-_LaO*EYa7O9i>g5}nHtNHOf$0oraapQ%{KtpGbLdkBB4CHAFTHR*t4gBJPA;q zkT>`aO+f>+0dAx7wNZsB6*IHn zRz?dUG#bE06`w5>rX-Zn**oNcPSHDUfZON=+Ni>milejdS4ImV zG#bE06`w5>rX-Zn*^{TLHD#cgYh-sqt%oL7ah%eV_nZ^FiF06y_3 zi4;*lrH!(qVFf~3_Xa9oC(b;1HM8^R* zB#IB0T|*>PNGiZO9l)j+0GS7H*L+4m8C95y>b&fUK-p*`giZpmQN?Eqg((ST^c=9Z z1K8*VK-L1>Mn?sdQH3G)yv@(PS{c0>LR$eGSjA_H$rM<5Zu}Xt?*y=+oj@J}_?W{T z*l7V}PGQR91=#^u<}$tv(LVyXjEc_|3R4ow^e15b2*9QdjI{?rxXXBSKp9n-Qc;t= zUKt$@!C?S4s`zZ7FeRakmV>nvz(%Wq90!m_(`jyWsx)edN`z@)?ptJtt`k;(@l*f@ zQ*q$}g@P%s6WWoz3c$+O0yz)hV-6Y2iAwNo^b(QYkNx@DAWd2SEBIhGWC$I1h3rYG z*8e zA?}bymP^&gqmZe$A!8nRssQOUob8lc9`PNN|K$8LF<5ZRS`Utk033V8Wr=gZr7HFs zWM2*7*xwH1Hh_HS%UI^Y zll@6bJEbGd+PZTsrht6)_PTV>BNV2ax{-f7bU2loj!Fe^b61SkAtEX?rC==vI28l`yTY^52aO8W4i4bc{Xm!Os2(MY)!G+hq9iVHJNTOO{S4UYR~mc&9ld&gF+F7 zAZCf&v99zgC*5z-^I+a`%VhTFxXNFu#B4FC(UlHWE)kLa8lu_bb2OM5Q{fwJ=^$I9wYka#IQ99 zVX96tjO%ifof zkjc*h_D&%j&q>f$M!bZ)w`h186Kog8I~j}E^C=&qr9Bch!%R`Lqs_Sf#vA1Jefg{N3H_31rWZ$3X_PqiubtdhP#f1?c#Yh@KbR72f#-+*9-B} zPvQmsxw=P@wgZrUL9q}&mKr?b0&u-B?xT@-Hq(y`<9pcs0c^hqm@7&Xtdw--mWU?$G(r~v%= z0IoB|XC5M<>Z}Z`B>=9oFpy&bvd*k+C3tOK8&#Mx`r7E1$><$mS`ML80Blt8nOas! zcvhG5CHmQ5Z3D2;^MR}ZD5Eq4eXNZt4D$%yUK+7@v@GerLg>2yHmdkcEvqC{NpA(~ zzX90jJwWaTNTWP=$(B9F5cC9W_lRF8L%#;&F9AI4DK1l72QH=j9b~@+VCA0y`4r&$ z58`l7k0xmoJCOCth+H&3uC+8=PXqvLLUEbmS1uFdkv*0!J7@w>0c1MB#~ji`uJVog zLn&0_9ByyDXZ=-{n5cBpqzEmU9{i|&CyZsIYSS7PKM$|0OvBO#|#5SmQa|g z&RzMhDF5SN?*(vOC_YRRq^y!q;rdsweiy)HxgN-m05VdezB>+G00y7RZovbN@z;VJ ze+za#N*s3=Fmvnn^{Ed6*$%P00jW=ccohg;m!-ZyEVyd?3Pjm!Aa+VO8mJuDQvZy^ z$>0b9s`r4%GLX6#NGnp#1klEvE>rh+g{g-1bpFfmkE?GJBnALne8p$shh)R7NJ*&T z-vZW~0bKn51o8`jyZw)sf~HWI8vmckUmTJ~e+8ju0c=$9*@s9dqpyHQ@}V0z&%27*`fkmszlYuJ`TXjPXe+8;9E!>s?$-qzAsG}{9N^SxdUW^ z%0d^1s{m|5ahc4e{9lXg^8jpO0LbM4A3dK{CMGFC>U1>!UXLoD@(j*j_q^c=14nz} zJ(OHeO?xWNO7|4fS?LDox(2{ElY|*&HbcCC<9)fMW2Kin!0`Zpy;NK#b15(Ba`cw~ z_VQ0aUIF+>kMh!@KxAqY3RA6Wd+x2u+y@Zd2XLCQl)#U9N=!*8(_e%29{@I;frg$2 zkXG5~L~T@IO2vb@!x0Xyi9!gC2Cz}ZX9|TW31zeltR(<88U}JKKpM?eA)=A4g1h!I zJ0^SwM~GZi2+CTbv-}6fi(F-jvO1isl$Z!lAh~(R$}&(UsyOu)V_EHlG%T}5rkFqv z$*)Pj5!06a5cI}`aScyHyjURyyp&S&E6~-&n1BQr&I1(0m{aW`t$SX z?T6+pV!^CwC70oQ;M)KwJX^GI!XqFa0)*Z`ncFz&MUcOyq`yI554&;@yiYAUs^dEaNa;Dgu;}7c(b9d?)hG&!+NKAiWF_`aL|lFntk{)_{xy(kGy; zmlBK|*vFmM@tkchn?GC^bjjL$$60-rX-ZnDPWxhV53I^nGJ9o{gXDTuyk3{j{J|6(OL){ z4`8E;&lU<(63XaVU_BGSMtgvC0i;orTJOg?yb4oo(df9uuif+Mq(M)GS=(`9#8xOd_KzfdHL&CI-lmh$jBbqZ(`TS3ijomapmfX# zT#$IXgfS*`1rmdNwip+nRJt=TUK%=E7*muXS|Ui-H*6?JOqdP1;kjg=|&d8eO@CD$SpZT^BUo%#i7E+FPC9o3aDgFXHo&en; z$oUlLwYgv8>~|atJOB~vo{ro9WyHYJIo`Pglk;>8sr<#JliexzNR_ZI||v>cPjcpmE4 zXRbg7F-BB|9m0rU6EE*X&OcyxU1llv#CM^xD~fvI^98hibPJ_XF+@=jcIV%Kv|AH= zS#W4IE!NvkX{1>kkBd>^=@~;q%Wo;w@s(~6hI45h<4MT)Qn4qzNnj%9eK60+y$Pit z=5Z7S?R5eZIX^@0q}+{?@AyOcM9$Z+I4ZYP+HD!cA{PD#3Osqa!(}$>Mg?4lnxdtr zUd-e@*N&yS&1{Ah&$Xq5i!5Gih$-S${m?B7<55Uo)^{>v&cuGN=vE$;BJNgX}R} z@fp+x=tfcIQ{?vf5@^lbAo-hfDnY_mx>@rNBGLSJG_r!AmycBec zC(QXCaTkupROfN@JYps;bH(mCR50Xtc@>1N1n_xLv0I0*E01pj>n#BG_+B9Y z2~g)nB)h&bf-jKJS3pely$zeb!l6DkVhv6$GUyv5^dS!0=Fg3w3f*7RNYU(x@0IwR zkB#^b|NWDI^Zk=lazcLeBrWkRlytYggz*g57fXVJzvq!T_-h|ob;47yt^iL@wI(he zblFZpa^%ND@XZr^5~M4Gz8*pa%&j#pGS=r1n&-G~sBebk8%7;x3@btqN>ed0)7nw) zgzn*#tnA6ajVPgSu@sTAqV!OaK?X(+`IgHll5;dksnPhFNs+G(ykk<)hYGpStOOr5 zk#P$Ra#JQ2xrt|(y9Hnhp7^hpOM1Z|( z|HMnl+kCJ4j7BLp_~5-=K7z2s1f#*4Fw;26nmH$U;d)n!@!@cb?+%}F60YCzp%}xf-nX%Y?U}dDdfk49j|c3p z=!j91{(QGd`@(4O_4~q(cffB*;NSzJdcVeRoaFnH5*q3#Udc%?dS-4DXmEyCiCV() zpNmy><9(GAw^^1SF~K*6)JruPo;mz2q67_je5)uyBOk6nBkvtodGA#(%~&GC!i!7W ztc+~LKYM~hOt{wq^;7#2d{51~!g~`P-cC7~g2U^+zoTlbFZ@3X;P6=B6FTfpeJd5d zvG0d!o}jAI*q~WOv1N8OaYUA)r7WfR^cS9g_$(lSZd8)2<%N8 za?2>to2AmR#>vczW~P!XF%0~}#(;m!SYs(=dwkDI{;8;#)iNx0zgDh3PRh+Ad_ zMnJ`gslaYXxgmv3!oojvNi1BE^@y?CtYM=h1|!_qT|DVM!W^FE*aE8+qy+le6WWjv z95tAO)6o(NQo@Wv&H;szRIgKEXlBhF85sWQh=b4=vQyGdM3`(vPmtw#^DngQL@q zgUiREj*;YhQF>a)wdW!msOo*P(@mmWH`#zTk1pLH^DW^+XR&;l)MZQ~WpK_+&Y{jX zG)K$8lSibk5C`8yk}co+GB-D1(l4CQM*0%z^x-czJ%mraWJBYgZTK`vVs4tcnc*}2 zj*0YuNk5%J8?)(jDW+*RALP|5EI9#_epN-w>vSovX*VC_)!Qf;0h4}*Ma%1SDX(ca zALRAxB&dA1yiS+$ns)O+UcVUvp1D|hm$4{qAc}=_dAg|xo z2$=MHGTOdQm-3o+^Fdy}dJ!<`7ihG+PM7kUcJo1AzeN!+={IY%yiS+$ns)O+UcDWG z`lDaD(egT7%4>QMA9e1($QEdcbeuCT*k+{pvd{!xgS>c6V z;U6&Rw|KM{I$e69X*VC_^iNx$Bs<#oE0*R-1tl zP&F7Z={JlV|DZ>wlYeg7&1VQ-la$Y=c=2R|b>N&UGpHULFn{s(2w&CcZB}3m<|d4d zDN<=g(=eNuw$eDmnIuAkOFWR)Bkq{$tGZ+A$UAUi4va~Qx#=-QgY%)QfeNaYsi%03 zmGXrWu zE#UQ$1u!INM<78X9}!Wja3{IPNkl-W%ZO;&%?Ep{-DW}zhF+f%un>J-oX!L-AHp=WC8E>QPn2qToi61y z?dF5LTDmxN(9r2pUekjH4GiSCVl!M-%$Ie1Nj2Gw;RHDODAX|Q^vFhl*v@eKsnew$ zO}qKHScdmH-H_VRklN$Ma_cVRJ;PW|`HmwAmdR)vN3~;&zMe+e4- z;8&f}4w&>?y|l5MPM2mh?dC(JtJQmIKA51L5j{cE42?JM8=f#D7JgI(cL+|xIC0XM z78Zr^+(gJG;4^(C?4nNK`%{Kb)64)L)Uq@!p%bKprj>+Rt)?Y(f|SrSOK{6$m+?SW z`OYKV9T?rLo6WmT93C*=q{BY*A2>LzRqnlKX(`j}S-;IPv(Rd@CZVC6b(VQ963)61 z_=T4ke2>C+fn=w9lr$jXALHj&T1h6PGp{so+wvjf1x%-{4~>JM608M=@{(E4!+869 z<2B-Z&3MN!nxN7*S@Nth&J^^mX|GDa+|9dQtfRgl>LxRmJ?>()8G(aFey)oy#*ZWA z=g5J3nKqmTV`)WBqg0xUohzvXc?LDX1bOBl^4pt6JJ(7)v$_3h+PT6?kY`Y@NRVd^ zBEP*!1W=V#o?6tZG8+?GluDCw)YCMwJ&3=nCGqV+If$wSVYU-0O?qZ=U(htN;eu)s z>;frOOTlo%3Gw&kIBY&yE15JYf|#bAY}Hz1LDY(^fa8KDJx8+(ns%};mb%q|lc1de zCqX-x9yd$=L0`B>ymY#Zm!{ocxJPGyK+5B6O22qd8@r{`rM#wx@aYvxXqspVnv~<5 zrk$3nml|OrtgS&zr%N$SyZPYlJ{27&Selfv)HGFuYx4?Em`O{}9m#M6XRt^shKSaF z3=!7d#?uCppEh1JoMF=$puAx%C2$K zacx_%%{r<~;xl+++%Fjm=n@pCrv#09rOP|7O2ImPTp?d&sOJQk1t^VS`tFt*cof5l zP}8Ol!x~iT1R8YRG&6JvK9OUO$6UOHd9N1T3Xem-$f-iSA!)T*JS@*6>NO`dgQp)+ zIox&)3Z-hD%^8JyY1K zi_tBOf)zvP0&D7AVV7wY0uMqCrBB!Sj>(uTuO&uXqo)Xa5Ix!|LehJh#cTO8tPx{{ zt)ha$qV|RTeHkT7Tx30LJZo6LHC{KYcMQBKX7AK_Io2>-UY~A-IdFJDW#x~wCQSXd zNdqpyK(4UWgx?a2tj4QgDUsl}{A2JdGxDm+5?(76;ru3gwC1-hGBAin^{Be1QX>#K zBdxKM{%5H_(iXgy_gx#03ct3kukBaUggq!L9XWrot-snIQZ{}VXmEC_ll`^50Xa`= zowk*hXB89;Ny3ViJYnzAT(FF?B_43?weXDWCn?t6ln+ydop`o&y7dm89SZw7ud1&y z>!y^OQqb)5bC+6v9wY4CI%T9au2R@l`iVuZl{?BBHD1{2XJROLDOti!+?xa)6!wYw z2A>oB#O=F(F{~X1UQV>tZ&FzJxwe9d-%Pja+em3vcD6Mfk@TeYB#S{C*<(RaD{ z186AUL_afx|C{LT`Zl%K!VBIhYFzn;nIb$+?YwBDydlvuQ&ji-#7t3b7H?D2OFA?( zoh2~t8pf9y>2xrN;spAEB|NzYECieaSP$3)*a7eug`S|V0+_+O_bBaS?98ud=Y>~!-Pu^gHG$jCJF4SR}#P~5oQu>CnXXM!I=@tRKxV*m_!X0~BZ zGwcO0kn1&nv>tkLa=gpIlwrOimV?D_{&;@Y1$U_gbWP8m@Se|QVpNhTZF&D;(yM)wv&FRJob^#ZGyssj&1?C&5KEHPZv}L7} zZ@K2bxt#Z!e?oqy*Ql~{yyjXI#te{~S%y7>DRMemJXju;if%-F&#nL}9KQ7lhOOTobm8=ay)BXctb=X#fWCBw7A zSjz^6lmr~ohu`rf51R5gYVDpd+B#bMXl~@=yjiyWuJ2An&UX^32Z2Nz_j^~_sop9( z!#l@Yg?8*c+a5{njY5K2>#OoTNY#+$gy?z1!W|=fn%8`d;$z;jev#uQmatLl946I} zF7yxMJ7|bzudl@R=W-EV9ioY&?G3^!!%Q_iG!k$j-tdG`1Qg;wk$^6XQjllnU)Hsv zDU36JV6;lQMA0ETqm)Xrf(_LS_LxIS&HMY%mjQFO(~ivfN8q~}d^zCDpdPc8RaPH? z^%})mWgDe-MijqPij+yH3N`+j4$f}B1fJ{Uh`JuI782$*7>RqGxjY1@5i3o zAn!kc{d)in*xf4o$u|V zGN(fT4Hk`|cM(U-@12W0q%1-wJKIj*YG;8O3!ym1n~}hWk+u_Za-eOYoi%qk35T2( z^q)g~^YO63#b@8|9+EGcK1!#v-3>5Nv>!V~!)S)eUE*7`3QfhZXH``}&ToFW4!7Ju znz+4SkE^n$RN0G4Xg^owS0Q^5vX>(XD*9+VALSVR=@6gv5c%&==8Ug?^SO1TA>I5r zG^nLKT+5Tr8-m#WBX))`wMj3`x-#aweC2pS|eLjguf-$~90aSPdV>iEY) ze8L#O+P&s>V%o;hqLGfrcH)K}XGo(C>hYQhAMEt2nf=zGI_1EHGIICJgBKLcr@5$k z<^x76vYc*41F*ZzIsFWR_re{t78ZPx1@p}vh>;S!=}Wo|FRqHOL1kldtaMe1heWGa)_ai^<44Kqe_Q6Xgqln*or*W0o`PPQd!%9R7_52*Y2VW8|6Uo& zk04;OHbk8&7wK3Ut~gS{nH7_utQPY_8H)!=`g`k)4TlN(A;LP6 zA9EAJ>~7sNG}Cz6@X?T*W&Qx|-Q6-}XM!TrYwn~rMLKo&_?GPK$EVbED~%i!^@#mZ zj+I{XlZ1`KbU}23EOP^Q52rK!9dB*IFL!6$h?+%TMjaoaD|@TFWr)WSd-FyW>9eSL zn48xZ4V?w5Qp-(b7jFz!8`h%@HEgB*YcJL&Kf4=P{bDHE?}Q z#kqFfOS(Lm2%!aR<1D}TJ)YLX)jzCziTnOhgWX8XIHwuF%{!^xLKjJhGqP$?zLRE=L98!^) z&g!mOf1`7I;;+J}ItL1!DeBpSwdtnxfyuDCBf%ctf4{b+~`^Y7?NtWPqmt5=N zgEbpRP@Qi%B0Ic{brq&?bD9 zYE@TLM~7%SZSk!0qh0OswhH>1P))n&>22@AN2Yqa@c}5&2YgmV6V?Ho)85k*>uHWe z@e!(y?pRbbu4rs(IJs`|QpT1v)GQV~&G9y-=GQN2tPvgE_$(Ih`nqDUhi_exYJBS` z##}4vYU&q=NM}nEf8Gk8aEdf{bd+(5{Jxb;o9^;+_Nr(+($?Htv)W~V^d^cjyElq2 zakcl>G@KA=Xe@7P7m$gypC1*mcyn(&)^r}q3ml(nVnb6;k0cSw4#_xj#pR z5GRUOL7zQihoqUh#m6skS_|BZvvK;shKQ`}?(X3DRDzJ& z=GRuAEM&bwYqYg}jc99*EofhbZ<{5&sEl<)qdiTXG1Lvd$JN`8_PBCwJQ_P0RW7_Rx`XvbTZ+%`?xOTaXLn1qV{WVy%{WxNDpb4#Joxg| z+*MtDY_Yht6<_c|BkXFSPsSCuLN}<$ImPI2@n|HDkE9j1Iw_I1Xylx^UEK~>L*0A_ z!LJFnJ6Lp0B#O_mb$7);xAt{K2rowOQfV#C@n%gs^|Nd#$63aFY2Ex28dU|HSXW

If#qKT@se0rs>KhfONu3}4#iatcA>z1n3RYw(6jWz8JZD`uWOP_Va*w7n` zHsM8gr(LKPbNtCQHO}|z(5f4HyD`4RP^Hqr6KYln`V3!;2O{oGk+#0Bb3{D4Mz@^C zy3=Y{pso@8)u&X~)mP7}uW>mOJr+fSlI5e$#om*_#`>C?hNcsph|rj@ zpzdT=TwPzkWWGyZrRtJ34U16{I@%M3vPpjHPc-q!=h9w5!$Qs3 zXA9M#5R=5f($V5#oyh|}f>+ws-5D)yY3^!|b`-ZYcXmd5OIxC=OPvrbX)J}+Rrs=A zOvHL4r7`*pU1?`?dsj)M2lcL!@G-+kM_)^{RE|j{C8g&?d%L0?rB3JQYAKEsx3%=f zO3@peW>u6*el1ts(=HAtv}VoW#7bATBi!GfUkx24bcR4TjTf(qL>!_e+6n|UP3NhS zHWVr{Z5lq@*V?`c=@GDlgKs_`Zik0TSTFF&!J&BpBzWinmVDNvp6k>5o1E?m153P) zKN&cszu^f3OZ+O_EZ~$LHp7(6zotva;{n-}E^mtI=l;5Cnog^qEju-xa6lf-ryuGh z)FSIECE0T}Q|he3%ud4+C9$=g@#d8{#(SM(o5HxWmh^VhcqB?%qAUAWK{L)*5^r9) zvNyU~Q61e8d=HVT%;9YAZSGoy5kQi?U8@FV^9fMq!JuNJWVHES3c&xz4Zq8Z=;4W= z))RKHOy}@vBvm1r28pybtY z0I7$QAG_6&pQsoeQv0fs={Tf`FlX(Rj=&vKE)7R=MYSKiM9!NZdsE`>!^cwoM0X-^ zWmhF<0Xdxf-0wRa*D6MN94bE~J4q%Zkc>bw0?7y@Banbw0?7y@Ban "$INPUT_DIR/readme.txt" +fi + +# Create USTAR tar archive +tar --format=ustar -cf "$OUTPUT_PATH" -C "$INPUT_DIR" . + +echo "mkramdisk: created $OUTPUT_PATH from $INPUT_DIR ($(wc -c < "$OUTPUT_PATH") bytes)" diff --git a/scripts/net-setup.sh b/scripts/net-setup.sh index 6993132..bf3fd92 100755 --- a/scripts/net-setup.sh +++ b/scripts/net-setup.sh @@ -66,5 +66,12 @@ if command -v nmcli &>/dev/null; then nmcli device set "$TAP" managed no 2>/dev/null || true fi +# Configure DNS on the bridge so systemd-resolved keeps working +if command -v resolvectl &>/dev/null && [ -n "$GW" ]; then + resolvectl dns "$BRIDGE" "$GW" 2>/dev/null || true + resolvectl domain "$BRIDGE" '~.' 2>/dev/null || true + echo "Configured DNS on $BRIDGE via $GW" +fi + echo "Network bridge setup complete: $PHYS -> $BRIDGE <- $TAP" ip -4 addr show dev "$BRIDGE" | head -3