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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user