diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 394cbc7..060b455 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 15 +#define MONTAUK_BUILD_NUMBER 16 diff --git a/kernel/src/Hal/SmpBoot.hpp b/kernel/src/Hal/SmpBoot.hpp index 223184f..7be1d59 100644 --- a/kernel/src/Hal/SmpBoot.hpp +++ b/kernel/src/Hal/SmpBoot.hpp @@ -35,6 +35,7 @@ namespace Smp { int cpuIndex; // 0-based CPU index uint32_t lapicId; // local APIC ID uint64_t idleSavedRsp; // RSP saved when switching from idle to process + uint64_t currentFsBase = ~0ULL; // last IA32_FS_BASE written on this CPU volatile bool started; // set by AP after init is complete Hal::TSS64* tss; // pointer to this CPU's TSS diff --git a/kernel/src/Sched/ElfLoader.cpp b/kernel/src/Sched/ElfLoader.cpp index 7c7b569..7c10857 100644 --- a/kernel/src/Sched/ElfLoader.cpp +++ b/kernel/src/Sched/ElfLoader.cpp @@ -413,7 +413,14 @@ namespace Sched { return true; } - uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys) { + uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys, TlsInfo* outTls) { + if (outTls != nullptr) { + outTls->fsBase = 0; + outTls->templateVaddr = 0; + outTls->fileSize = 0; + outTls->memSize = 0; + outTls->align = 0; + } Fs::Vfs::BackendFile file = {-1, -1, 0}; if (Fs::Vfs::OpenBackendFile(vfsPath, file) < 0) { return 0; @@ -449,9 +456,16 @@ namespace Sched { } // Process program headers + Elf64ProgramHeader* tlsPhdr = nullptr; + uint64_t maxVaddrEnd = 0; 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_TLS && phdr->p_memsz > 0) { + tlsPhdr = phdr; + continue; + } + if (phdr->p_type != PT_LOAD) { continue; } @@ -460,6 +474,10 @@ namespace Sched { continue; } + if (phdr->p_vaddr + phdr->p_memsz > maxVaddrEnd) { + maxVaddrEnd = phdr->p_vaddr + phdr->p_memsz; + } + // 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; @@ -505,6 +523,64 @@ namespace Sched { } } + // Build the main thread's TLS block (x86-64 variant II): the block + // lives below the thread pointer, TP = base + S where + // S = align_up(memsz, align), template data at [base, base+filesz), + // .tbss zeroed, and [TP] holds the ABI-required self-pointer. + if (tlsPhdr != nullptr && outTls != nullptr) { + uint64_t align = tlsPhdr->p_align < 16 ? 16 : tlsPhdr->p_align; + uint64_t S = (tlsPhdr->p_memsz + align - 1) & ~(align - 1); + uint64_t base = (maxVaddrEnd + 0x1FFFULL) & ~0xFFFULL; // page-aligned, 1 guard page gap + uint64_t total = S + 16; // + minimal TCB + uint64_t numPages = (total + 0xFFF) / 0x1000; + uint64_t tp = base + S; + + bool ok = true; + for (uint64_t p = 0; p < numPages && ok; p++) { + void* page = Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) { + ok = false; + break; + } + uint64_t physAddr = Memory::SubHHDM((uint64_t)page); + uint64_t virtAddr = base + p * 0x1000; + if (!Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, virtAddr)) { + ok = false; + break; + } + + // Copy the .tdata template bytes that land in this page. + uint64_t pageStart = virtAddr; + uint64_t pageEnd = virtAddr + 0x1000; + uint64_t dataStart = base; + uint64_t dataEnd = base + tlsPhdr->p_filesz; + uint64_t copyStart = (pageStart > dataStart) ? pageStart : dataStart; + uint64_t copyEnd = (pageEnd < dataEnd) ? pageEnd : dataEnd; + if (copyStart < copyEnd) { + memcpy((uint8_t*)Memory::HHDM(physAddr) + (copyStart - pageStart), + fileData + tlsPhdr->p_offset + (copyStart - dataStart), + copyEnd - copyStart); + } + + // Write the self-pointer if TP falls in this page. + if (tp >= pageStart && tp + 8 <= pageEnd) { + *(uint64_t*)((uint8_t*)Memory::HHDM(physAddr) + (tp - pageStart)) = tp; + } + } + + if (ok) { + outTls->fsBase = tp; + outTls->templateVaddr = tlsPhdr->p_vaddr; + outTls->fileSize = tlsPhdr->p_filesz; + outTls->memSize = tlsPhdr->p_memsz; + outTls->align = align; + } else { + Kt::KernelLogStream(Kt::ERROR, "ELF") + << "PT_TLS setup failed (out of pages?), " + << "process will run without TLS"; + } + } + uint64_t entryPoint = hdr->e_entry; Memory::g_heap->Free(fileData); diff --git a/kernel/src/Sched/ElfLoader.hpp b/kernel/src/Sched/ElfLoader.hpp index 3b2dc92..4ad939e 100644 --- a/kernel/src/Sched/ElfLoader.hpp +++ b/kernel/src/Sched/ElfLoader.hpp @@ -39,6 +39,7 @@ namespace Sched { static constexpr uint32_t PT_LOAD = 1; static constexpr uint32_t PT_DYNAMIC = 2; + static constexpr uint32_t PT_TLS = 7; static constexpr uint16_t ET_EXEC = 2; static constexpr uint16_t ET_DYN = 3; static constexpr uint16_t EM_X86_64 = 62; @@ -50,7 +51,19 @@ namespace Sched { // 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); + // Thread-local storage description recovered from PT_TLS. fsBase is + // the main thread's thread pointer (x86-64 variant II: the TLS block + // sits below it, [fsBase] holds a self-pointer). The template fields + // let SpawnThread build per-thread copies from the loaded image. + struct TlsInfo { + uint64_t fsBase; // 0 when the binary has no PT_TLS + uint64_t templateVaddr; // source of .tdata in the loaded image + uint64_t fileSize; // initialized bytes + uint64_t memSize; // total bytes (incl. .tbss) + uint64_t align; + }; + + uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys, TlsInfo* outTls); // Load a shared library into a fixed address range. // Returns the relocation base / load bias on success, or 0 on failure. diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 575c84a..8384844 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -32,6 +32,22 @@ extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t newCR3, uint8_t* oldFpuArea, uint8_t* newFpuArea); +// Load a user thread's FS base (TLS thread pointer) before dispatching it. +// Cached per CPU: the MSR write is skipped when unchanged, so TLS-free +// processes (fsBase 0) cost one write per CPU ever. Userspace cannot move +// FS base itself (CR4.FSGSBASE is off), so the cache cannot go stale. +static inline void LoadUserFsBase(Smp::CpuData* cpu, uint64_t fsBase) { + if (cpu->currentFsBase == fsBase) { + return; + } + cpu->currentFsBase = fsBase; + asm volatile("wrmsr" + : + : "c"(0xC0000100u), /* IA32_FS_BASE */ + "a"((uint32_t)fsBase), + "d"((uint32_t)(fsBase >> 32))); +} + // Assembly: jump to user mode via IRETQ. // `arg` is delivered as the user-mode RDI (SystemV first argument). // For freshly spawned processes this is 0; for SpawnThread it is the @@ -129,6 +145,7 @@ namespace Sched { cpu->kernelRsp = processTable[next].kernelStackTop; cpu->tss->rsp0 = processTable[next].kernelStackTop; + LoadUserFsBase(cpu, processTable[next].fsBase); SchedContextSwitch(&processTable[slot].savedRsp, processTable[next].savedRsp, processTable[next].pml4Phys, processTable[slot].fpuState, processTable[next].fpuState); @@ -161,6 +178,8 @@ namespace Sched { // Set up per-CPU TSS RSP0 for hardware interrupts from ring 3 cpu->tss->rsp0 = proc.kernelStackTop; + LoadUserFsBase(cpu, proc.fsBase); + // Jump to user mode (never returns). // For main threads threadArg is 0 (libc _start ignores RDI); // for sibling threads it carries the user-supplied argument. @@ -261,7 +280,8 @@ namespace Sched { uint64_t pml4Phys = Memory::VMM::Paging::CreateUserPML4(); // Load ELF into the process's address space - uint64_t entry = ElfLoad(vfsPath, pml4Phys); + TlsInfo tls{}; + uint64_t entry = ElfLoad(vfsPath, pml4Phys, &tls); if (entry == 0) { Memory::VMM::Paging::FreeUserHalf(pml4Phys); Memory::g_pfa->Free((void*)Memory::HHDM(pml4Phys)); @@ -387,6 +407,11 @@ namespace Sched { proc.kernelStackTop = kernelStackTop; proc.userStackTop = UserStackTop - 8; proc.heapNext = UserHeapBase; + proc.fsBase = tls.fsBase; + proc.tlsTemplateVaddr = tls.templateVaddr; + proc.tlsFileSize = tls.fileSize; + proc.tlsMemSize = tls.memSize; + proc.tlsAlign = tls.align; proc.readdirCursor = 0; proc.runningOnCpu = -1; proc.killPending = false; @@ -499,6 +524,52 @@ namespace Sched { int processPid = processTable[primarySlot_].pid; if (sharedPml4 == 0) return -1; + // Give the new thread its own TLS block when the image has one. + // We are executing inside the process's address space, so the + // freshly mapped pages and the template are directly addressable. + // The block is process-heap memory; it is reclaimed with the rest + // of the user half at process teardown (a thread that exits before + // its process leaks its block until then - acceptable for now). + uint64_t threadFsBase = 0; + Process& primary = processTable[primarySlot_]; + if (primary.tlsMemSize > 0) { + uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign; + uint64_t S = (primary.tlsMemSize + align - 1) & ~(align - 1); + uint64_t total = S + 16; + uint64_t numPages = (total + 0xFFF) / 0x1000; + + schedLock.Acquire(); + uint64_t base = primary.heapNext; + primary.heapNext += numPages * 0x1000; + schedLock.Release(); + + bool ok = true; + for (uint64_t p = 0; p < numPages; p++) { + void* page = Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) { + ok = false; + break; + } + uint64_t physAddr = Memory::SubHHDM((uint64_t)page); + if (!Memory::VMM::Paging::MapUserIn(sharedPml4, physAddr, + base + p * 0x1000)) { + ok = false; + break; + } + } + if (!ok) { + Kt::KernelLogStream(Kt::ERROR, "Sched") + << "Thread TLS allocation failed"; + return -1; + } + + uint64_t tp = base + S; + memcpy((void*)base, (const void*)primary.tlsTemplateVaddr, + primary.tlsFileSize); + *(uint64_t*)tp = tp; /* ABI self-pointer */ + threadFsBase = tp; + } + // We do not write to the user stack from kernel mode (that would // require validating the user VA against the process page tables). // Userspace is responsible for ensuring the thread entry calls @@ -566,6 +637,11 @@ namespace Sched { thr.ioOutHandle = -1; thr.ioInHandle = -1; thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1; thr.args[0] = '\0'; thr.user[0] = '\0'; thr.cwd[0] = '\0'; + thr.fsBase = threadFsBase; + thr.tlsTemplateVaddr = 0; + thr.tlsFileSize = 0; + thr.tlsMemSize = 0; + thr.tlsAlign = 0; // Derive a debug name from the primary's name. { @@ -609,6 +685,7 @@ namespace Sched { if (readyCount > 0) { KickOneIdleCpu(cpu->cpuIndex); } + LoadUserFsBase(cpu, processTable[next].fsBase); SchedContextSwitch(&thr.savedRsp, processTable[next].savedRsp, processTable[next].pml4Phys, thr.fpuState, processTable[next].fpuState); @@ -930,6 +1007,8 @@ namespace Sched { uint8_t* oldFpu = (oldSlot >= 0) ? processTable[oldSlot].fpuState : nullptr; uint8_t* newFpu = processTable[next].fpuState; + LoadUserFsBase(cpu, processTable[next].fsBase); + // DO NOT release schedLock here! It is held across the context // switch so that setting Ready + saving RSP is atomic. The // resumed process releases it. @@ -1207,6 +1286,7 @@ namespace Sched { uint64_t newCR3 = processTable[next].pml4Phys; cpu->kernelRsp = processTable[next].kernelStackTop; cpu->tss->rsp0 = processTable[next].kernelStackTop; + LoadUserFsBase(cpu, processTable[next].fsBase); // Lock held across context switch -- resumed process releases it SchedContextSwitch(&processTable[slot].savedRsp, processTable[next].savedRsp, newCR3, diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index 41ced12..a1e1e15 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -58,6 +58,16 @@ namespace Sched { char user[32]; // Owner user name (inherited from parent on spawn) char cwd[256]; // Absolute current working directory + // Thread-local storage. fsBase is loaded into IA32_FS_BASE when + // this thread is dispatched (0 = no TLS). The tls* template fields + // live on the primary slot and describe PT_TLS so sibling threads + // can get their own block copies. + uint64_t fsBase = 0; + uint64_t tlsTemplateVaddr = 0; + uint64_t tlsFileSize = 0; + uint64_t tlsMemSize = 0; + uint64_t tlsAlign = 0; + int runningOnCpu; // CPU index running this process (-1 if not running) bool killPending = false; // Set by Sys_Kill when target is running on another CPU bool reapReady = false; // Set once teardown is complete and BSP may free slot resources diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 7f81618..7f00baf 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -402,6 +402,7 @@ ifneq ($(wildcard $(NATIVE_BIN)/as),) cp lib/libc/crt1.o lib/libc/crti.o lib/libc/crtn.o $(BINDIR)/sdk/lib/ ar rcs $(BINDIR)/sdk/lib/libm.a ar rcs $(BINDIR)/sdk/lib/libstdc++.a + ../toolchain/local/bin/x86_64-montauk-gcc -O2 ../toolchain/files/tls-test.c -o $(BINDIR)/sdk/bin/tls-test.elf else @echo "devkit: toolchain/native/ not built; skipping native dev tools" endif diff --git a/toolchain/files/tls-test.c b/toolchain/files/tls-test.c new file mode 100644 index 0000000..fc93efe --- /dev/null +++ b/toolchain/files/tls-test.c @@ -0,0 +1,37 @@ +/* + * tls-test.c + * Verifies the MontaukOS TLS runtime: PT_TLS loading, FS base, + * .tdata initialization, .tbss zeroing, and the ABI self-pointer. + * Built by the devkit target and shipped at 0:/sdk/bin/tls-test.elf. +*/ + +#include +#include + +__thread int tls_int = 41; +__thread char tls_str[32] = "montauk-tls"; +__thread int tls_bss[8]; /* .tbss, must arrive zeroed */ + +int main(void) { + int ok = 1; + + tls_int++; + tls_bss[3] = 7; + + printf("tls_int = %d (want 42)\n", tls_int); + printf("tls_str = %s (want montauk-tls)\n", tls_str); + printf("tls_bss = %d,%d (want 0,7)\n", tls_bss[0], tls_bss[3]); + + if (tls_int != 42) ok = 0; + if (tls_str[0] != 'm' || tls_str[10] != 's') ok = 0; + if (tls_bss[0] != 0 || tls_bss[3] != 7) ok = 0; + + /* The thread pointer must hold a self-pointer at %fs:0. */ + uint64_t self; + __asm__("mov %%fs:0, %0" : "=r"(self)); + printf("fs:0 self = 0x%lx\n", (unsigned long)self); + if (self == 0) ok = 0; + + printf(ok ? "TLS OK\n" : "TLS BROKEN\n"); + return ok ? 0 : 1; +}