feat: TLS runtime - PT_TLS loading and per-thread FS base
MontaukOS binaries could not use thread-local storage: the ELF loader ignored PT_TLS and no FS base was ever programmed, so any %fs access (initial-exec/local-exec TLS, e.g. libbfd's _Thread_local error state) page faulted at address 0. This is the kernel prerequisite for cc1plus/libstdc++ in the GCC port. ELF loader: parse PT_TLS and build the main thread's TLS block above the loaded image (x86-64 variant II ABI: block below the thread pointer, TP = base + align_up(memsz, align), .tdata copied from the template, .tbss zeroed, ABI self-pointer stored at [TP]). The template description is kept on the process for thread spawns. Scheduler: fsBase per thread, loaded into IA32_FS_BASE at every dispatch site through a per-CPU cache - TLS-free processes cost one MSR write per CPU ever, and userspace cannot desync the cache since CR4.FSGSBASE stays off. SYS_THREAD_SPAWN gives each new thread its own TLS block copied from the template (allocated from the process heap; reclaimed at process teardown). Ships tls-test.elf in the SDK (built by the devkit target): checks .tdata values, .tbss zeroing, and the %fs:0 self-pointer, exiting nonzero on failure. Boot-smoke verified; existing TLS-free programs are unaffected. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 15
|
||||
#define MONTAUK_BUILD_NUMBER 16
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
__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;
|
||||
}
|
||||
Reference in New Issue
Block a user