diff --git a/GNUmakefile b/GNUmakefile index a23668e..aaf96ec 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -156,6 +156,13 @@ run-hdd-bios: $(IMAGE_NAME).hdd toolchain: ./toolchain/build-toolchain.sh +# Build the Montauk SDK: native binutils + GCC that run on MontaukOS, +# shipped into the image at 0:/sdk by the programs/ devkit target. +# One-time (long) build; idempotent afterwards. +.PHONY: sdk +sdk: + ./toolchain/build-native-sdk.sh + limine/limine: rm -rf limine git clone https://github.com/limine-bootloader/limine.git --branch=v9.x-binary --depth=1 diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 676bf2f..0a5fc31 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 26 +#define MONTAUK_BUILD_NUMBER 29 diff --git a/kernel/src/Api/Heap.hpp b/kernel/src/Api/Heap.hpp index 8d34ead..80843ef 100644 --- a/kernel/src/Api/Heap.hpp +++ b/kernel/src/Api/Heap.hpp @@ -52,7 +52,16 @@ namespace montauk::abi { if (userVa + size < userVa || userVa + size > USER_SPACE_END) return 0; uint64_t numPages = size / 0x1000; - if (g_heapAllocCount[slot] >= MaxHeapAllocs) return 0; + if (g_heapAllocCount[slot] >= MaxHeapAllocs) { + // Out of allocation records, not out of memory. Log it: a + // silent 0 here surfaced as bogus downstream errors (BFD + // turned NULL mallocs into "file format not recognized"). + Kt::KernelLogStream(Kt::ERROR, "Heap") + << "pid " << proc->pid << " (" << proc->name + << ") hit MaxHeapAllocs (" << (uint64_t)MaxHeapAllocs + << "), SYS_ALLOC refused"; + return 0; + } // Allocate physical pages and map them into the process uint64_t mappedPages = 0; diff --git a/kernel/src/Hal/IDT.cpp b/kernel/src/Hal/IDT.cpp index 6585805..04d4da2 100644 --- a/kernel/src/Hal/IDT.cpp +++ b/kernel/src/Hal/IDT.cpp @@ -78,8 +78,10 @@ namespace Hal { return frame; } - template - __attribute__((interrupt)) void ExceptionHandler(System::PanicFrame* frame) + // Shared fatal-exception path: kill the faulting user process (with a + // crash report) or panic the kernel. `frame` is the RAW interrupt frame + // (error code at offset 0 for vectors that push one). Never returns. + static void HandleFatalException(uint8_t i, System::PanicFrame* frame) { uint64_t cs = GetExceptionCS(i, frame); bool fromUser = (cs & 3) == 3; @@ -165,6 +167,41 @@ namespace Hal { if (fromUser) asm volatile("swapgs"); } + template + __attribute__((interrupt)) void ExceptionHandler(System::PanicFrame* frame) + { + HandleFatalException(i, frame); + } + + // Page faults get a dedicated handler with the proper error-code + // signature (so GCC pops the error code before IRET) because, unlike + // the generic handler, this one can RETURN: a non-present fault in the + // user stack growth region maps a fresh zeroed page and retries the + // faulting instruction. With the two-argument form, `frame` points past + // the error code, directly at the saved IP. + __attribute__((interrupt)) void PageFaultHandler(System::PanicFrame* frame, uint64_t errorCode) + { + bool fromUser = (frame->CS & 3) == 3; + if (fromUser) asm volatile("swapgs"); + + uint64_t cr2; + asm volatile("mov %%cr2, %0" : "=r"(cr2)); + + // Bit 0 of the error code: 0 = non-present page. Covers both user + // pushes past the mapped stack and kernel accesses to not-yet-grown + // user stack buffers passed into syscalls. + if ((errorCode & 1) == 0 && Sched::GetCurrentPid() >= 0 + && Sched::TryGrowUserStack(cr2)) { + if (fromUser) asm volatile("swapgs"); + return; + } + + // Not a growable fault. Hand the RAW frame (error code at offset 0) + // to the fatal path, which re-derives fromUser and swaps GS itself. + if (fromUser) asm volatile("swapgs"); + HandleFatalException(0x0E, (System::PanicFrame*)((uint8_t*)frame - 8)); + } + void LoadIDT(IDTRStruct& idtr) { asm("lidt %0" : : "m"(idtr)); } @@ -208,7 +245,10 @@ namespace Hal { // Use IST1 for NMI (2) and Double Fault (8) so they get a // known-good stack even if the kernel stack has overflowed. uint8_t ist = (I == 2 || I == 8) ? 1 : 0; - IDTEncodeInterrupt(I, (void*)ExceptionHandler, InterruptGate, ist); + // Vector 14 uses the dedicated page fault handler (stack growth). + void* handler = (I == 14) ? (void*)PageFaultHandler + : (void*)ExceptionHandler; + IDTEncodeInterrupt(I, handler, InterruptGate, ist); SetHandler::run(); } }; diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 8384844..b0136b9 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -1103,6 +1103,48 @@ namespace Sched { return &processTable[slot]; } + // Serializes stack growth so two threads faulting on the same page do not + // both map it (the loser's page would leak: FreeUserHalf only frees pages + // still referenced by the page tables). + static kcp::Spinlock stackGrowLock; + + bool TryGrowUserStack(uint64_t faultAddr) { + constexpr uint64_t growBase = UserStackTop - UserStackMax; + constexpr uint64_t eagerBase = UserStackTop - UserStackSize; + if (faultAddr < growBase || faultAddr >= eagerBase) return false; + + Process* proc = GetCurrentProcessPtr(); + if (proc == nullptr) return false; + + uint64_t pageVa = faultAddr & ~0xFFFULL; + + stackGrowLock.Acquire(); + + // Another thread may have mapped this page between the fault and here. + if (Memory::VMM::Paging::IsUserRangeAccessible(proc->pml4Phys, pageVa, 0x1000, true)) { + stackGrowLock.Release(); + return true; + } + + void* page = Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) { + stackGrowLock.Release(); + Kt::KernelLogStream(Kt::ERROR, "Sched") + << "Out of memory growing user stack for pid " << proc->pid; + return false; + } + uint64_t physAddr = Memory::SubHHDM((uint64_t)page); + if (!Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, pageVa)) { + stackGrowLock.Release(); + Memory::g_pfa->Free(page); + return false; + } + stackGrowLock.Release(); + + asm volatile("invlpg (%0)" :: "r"(pageVa) : "memory"); + return true; + } + void ExitProcess() { auto* cpu = Smp::GetCurrentCpuData(); int slot = cpu->currentSlot; diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index a1e1e15..d6dc66d 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -19,9 +19,14 @@ namespace Sched { static constexpr int MaxProcesses = 256; static constexpr uint64_t StackPages = 4; // 16 KiB kernel stack per thread static constexpr uint64_t StackSize = StackPages * 0x1000; - static constexpr uint64_t UserStackPages = 8; // 32 KiB user stack (main thread) + static constexpr uint64_t UserStackPages = 8; // 32 KiB user stack (main thread, eagerly mapped) static constexpr uint64_t UserStackSize = UserStackPages * 0x1000; static constexpr uint64_t UserStackTop = 0x7FFFFFF000ULL; // Main-thread user stack top VA + // The main-thread stack demand-grows below the eager region up to this + // limit: page faults in [UserStackTop - UserStackMax, UserStackTop - + // UserStackSize) map fresh zeroed pages instead of killing the process. + // Heavy recursive programs (GCC's cc1plus) need megabytes of stack. + static constexpr uint64_t UserStackMax = 64 * 1024 * 1024; // 64 MiB static constexpr uint64_t UserHeapBase = 0x40000000ULL; // User heap start VA static constexpr uint32_t UserReadDirSlots = 64; // rotating scratch pages for SYS_READDIR static constexpr uint64_t UserReadDirBase = @@ -139,6 +144,12 @@ namespace Sched { // Get a pointer to the currently running thread's slot (may be a sibling). Process* GetCurrentThreadPtr(); + // Demand-grow the current process's main-thread user stack. Called from + // the page fault handler when a non-present fault lands in the stack + // growth region. Maps one zeroed page at the faulting address and returns + // true if the faulting instruction should be retried. + bool TryGrowUserStack(uint64_t faultAddr); + // Called by terminated processes to mark themselves done. // Tears down the entire process (kills all sibling threads, frees address space). void ExitProcess(); diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 5cc1ade..2b1bbab 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -424,10 +424,18 @@ ifneq ($(wildcard ../toolchain/native-gcc/sdk/bin/gcc),) mkdir -p $(BINDIR)/tmp printf 'scratch space for compiler intermediates\n' > $(BINDIR)/tmp/readme.txt else - @echo "devkit: toolchain/native-gcc/ not built; skipping native GCC" + @echo "======================================================================" + @echo "devkit: WARNING: toolchain/native-gcc/ not built." + @echo " The image will ship WITHOUT the native GCC (g++, cc1plus, ...)." + @echo " Run 'make sdk' from the repo root to build the full Montauk SDK." + @echo "======================================================================" endif else - @echo "devkit: toolchain/native/ not built; skipping native dev tools" + @echo "======================================================================" + @echo "devkit: WARNING: toolchain/native/ not built." + @echo " The image will ship WITHOUT the Montauk SDK (0:/sdk dev tools)." + @echo " Run 'make sdk' from the repo root to build it (one-time, long)." + @echo "======================================================================" endif clean: diff --git a/programs/include/montauk/heap.h b/programs/include/montauk/heap.h index 8d306d7..5ed05ce 100644 --- a/programs/include/montauk/heap.h +++ b/programs/include/montauk/heap.h @@ -123,13 +123,28 @@ namespace heap_detail { return nullptr; } - static inline bool grow(uint64_t bytes) { - uint64_t pages = (bytes + 0xFFF) / 0x1000; - if (pages < 4) pages = 4; + // Next slab size for heap growth. The kernel tracks a finite number + // of SYS_ALLOC records per process (MaxHeapAllocs), so growing once + // per large allocation exhausts them under allocation-heavy loads + // (the native ld ran out mid-link). Doubling slabs keep the syscall + // count logarithmic in total heap size. + inline uint64_t g_grow_slab = 16 * 0x1000; - void* mem = montauk::alloc(pages * 0x1000); + static inline bool grow(uint64_t bytes) { + uint64_t want = (bytes + 0xFFF) & ~0xFFFULL; + if (want < 0x4000) want = 0x4000; + + uint64_t slab = (want > g_grow_slab) ? want : g_grow_slab; + if (g_grow_slab < 4 * 1024 * 1024) g_grow_slab *= 2; + + void* mem = montauk::alloc(slab); + if (mem == nullptr && slab > want) { + // Big slab refused (low memory): retry with the exact need. + slab = want; + mem = montauk::alloc(slab); + } if (mem == nullptr) return false; - insert_overflow(mem, pages * 0x1000); + insert_overflow(mem, slab); return true; } diff --git a/programs/lib/libc/libc.c b/programs/lib/libc/libc.c index 42541c6..c450c01 100644 --- a/programs/lib/libc/libc.c +++ b/programs/lib/libc/libc.c @@ -687,12 +687,28 @@ static void *heap_take_overflow(uint64_t needed) { return NULL; } +/* Next slab size for heap growth. The kernel tracks a finite number of + SYS_ALLOC records per process (MaxHeapAllocs), so growing once per + large allocation exhausts them: ld ran out mid-link and BFD reported + the resulting NULL mallocs as "file format not recognized". Doubling + slabs keep the syscall count logarithmic in total heap size. */ +static uint64_t g_heap_slab = 16 * 0x1000; + static void heap_grow(uint64_t bytes) { - uint64_t pages = (bytes + 0xFFF) / 0x1000; - if (pages < 4) pages = 4; - void *mem = (void *)_zos_syscall1(SYS_ALLOC, (long)(pages * 0x1000)); + uint64_t want = (bytes + 0xFFF) & ~0xFFFULL; + if (want < 0x4000) want = 0x4000; + + uint64_t slab = (want > g_heap_slab) ? want : g_heap_slab; + if (g_heap_slab < 4 * 1024 * 1024) g_heap_slab *= 2; + + void *mem = (void *)_zos_syscall1(SYS_ALLOC, (long)slab); + if (mem == NULL && slab > want) { + /* Big slab refused (low memory): retry with the exact need. */ + slab = want; + mem = (void *)_zos_syscall1(SYS_ALLOC, (long)slab); + } if (mem != NULL) - heap_insert_overflow(mem, pages * 0x1000); + heap_insert_overflow(mem, slab); } /* Refill a small-block bucket from overflow */ @@ -1504,6 +1520,25 @@ int vsnprintf(char *buf, size_t size, const char *fmt, va_list ap) { } break; } + case 'o': { + unsigned long val; + if (is_long >= 1) val = va_arg(ap, unsigned long); + else val = va_arg(ap, unsigned int); + /* %#o: force a leading zero digit */ + if (alt && val != 0 && precision < 0) { + _pf_putc(&st, '0'); + if (width > 0) width--; + } + if (left_align) { + size_t before = st.pos; + _pf_putnum(&st, val, 8, 0, 0, pad, 0, precision); + size_t len = st.pos - before; + for (size_t w = len; (int)w < width; w++) _pf_putc(&st, ' '); + } else { + _pf_putnum(&st, val, 8, 0, width, pad, 0, precision); + } + break; + } case 'p': { void *val = va_arg(ap, void *); _pf_puts(&st, "0x"); diff --git a/programs/lib/libc/obj/libc.o b/programs/lib/libc/obj/libc.o index 98948f1..7e82445 100644 Binary files a/programs/lib/libc/obj/libc.o and b/programs/lib/libc/obj/libc.o differ diff --git a/toolchain/README.md b/toolchain/README.md index d299721..60df51e 100644 --- a/toolchain/README.md +++ b/toolchain/README.md @@ -64,6 +64,20 @@ toolchain/ ## Native binutils (runs on MontaukOS) +The whole native SDK (binutils + GCC below, staged for the OS image) is +built by one idempotent script — this is what a fresh clone should run: + +```bash +make sdk # from the repo root; wraps toolchain/build-native-sdk.sh +``` + +It invokes build-montauk-toolchain.sh first (cross compiler + sysroot), +applies the host-build fix-ups listed under "Host-build gotchas", and +strips the staged binaries. To force a rebuild after libc changes: +`rm -rf toolchain/native toolchain/native-gcc toolchain/build/binutils-native +toolchain/build/gcc-native` and re-run. The manual steps below are kept +as a reference for what the script does. + Binutils can be cross-compiled to run *on* MontaukOS itself (`--host=x86_64-montauk`), the first step toward a self-hosted GCC: diff --git a/toolchain/build-native-sdk.sh b/toolchain/build-native-sdk.sh new file mode 100755 index 0000000..4caaf7b --- /dev/null +++ b/toolchain/build-native-sdk.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# +# build-native-sdk.sh — Build the Montauk SDK: binutils + GCC compiled +# to run *on* MontaukOS (--host=x86_64-montauk), staged for the OS image. +# +# Usage: ./toolchain/build-native-sdk.sh (or: make sdk) +# +# Produces: +# toolchain/native/sdk/ — native as/ld/ar/nm/... (stripped) +# toolchain/native-gcc/sdk/ — native gcc/g++ driver + cc1/cc1plus/collect2 +# +# The `devkit` target in programs/GNUmakefile ships these into the OS +# image at 0:/sdk. Without them the ISO builds WITHOUT the SDK. +# +# Requires the x86_64-montauk cross toolchain (build-montauk-toolchain.sh +# is invoked automatically; it also downloads/patches the shared sources +# and refreshes toolchain/sysroot from the current libc). +# +# This script is idempotent — each stage is skipped once its output +# exists. To force a rebuild (e.g. after libc changes, so the native +# tools relink against the fixed libc): +# rm -rf toolchain/native toolchain/native-gcc \ +# toolchain/build/binutils-native toolchain/build/gcc-native + +set -euo pipefail + +BINUTILS_VERSION="2.43.1" +GCC_VERSION="14.2.0" +HOST="x86_64-montauk" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PREFIX="${SCRIPT_DIR}/local" +SRC_DIR="${SCRIPT_DIR}/src" +BUILD_DIR="${SCRIPT_DIR}/build" +SYSROOT="${SCRIPT_DIR}/sysroot" +NATIVE_BINUTILS="${SCRIPT_DIR}/native" +NATIVE_GCC="${SCRIPT_DIR}/native-gcc" + +JOBS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" + +bold() { printf '\033[1m%s\033[0m\n' "$*"; } +green() { printf '\033[1;32m%s\033[0m\n' "$*"; } + +bold "=== Building the Montauk SDK (native ${HOST} binutils + GCC) ===" + +# ── Cross toolchain + sources + sysroot ────────────────────────────────────── +# build-montauk-toolchain.sh is idempotent: it downloads and patches the +# shared binutils/GCC sources, always refreshes the sysroot from the +# current libc, and skips compiler builds once installed. +"${SCRIPT_DIR}/build-montauk-toolchain.sh" + +export PATH="${PREFIX}/bin:${PATH}" + +# ── Host-build fix-ups in the GCC source tree ──────────────────────────────── +# The bundled gmp/mpfr/mpc/isl/gettext carry their own config.sub copies +# that predate the montauk target; overwrite each with the patched +# top-level one so they configure for --host=x86_64-montauk. +GCC_SRC="${SRC_DIR}/gcc-${GCC_VERSION}" +for sub in gmp mpfr mpc isl gettext; do + for cs in "${GCC_SRC}/${sub}/config.sub" "${GCC_SRC}/${sub}/build-aux/config.sub"; do + if [[ -f "${cs}" ]] && ! grep -q montauk "${cs}"; then + bold "Patching config.sub: ${cs#"${SRC_DIR}/"}" + cp "${GCC_SRC}/config.sub" "${cs}" + fi + done +done + +# --disable-nls does not skip the bundled gettext, and its gnulib needs +# more locale surface than the Montauk libc has. Drop the symlink so the +# toplevel treats it as absent. +if [[ -L "${GCC_SRC}/gettext" ]]; then + bold "Removing gettext symlink (not buildable against the Montauk libc)" + rm "${GCC_SRC}/gettext" +fi + +# The gcc subdir resolves --with-native-system-header-dir=/sdk/include +# inside the build sysroot at build time; give the sysroot an sdk -> usr +# alias so that path exists. +if [[ ! -e "${SYSROOT}/sdk" ]]; then + ln -s usr "${SYSROOT}/sdk" +fi + +# ── Native binutils ────────────────────────────────────────────────────────── +if [[ -x "${NATIVE_BINUTILS}/sdk/bin/ld" ]]; then + bold "Native binutils already staged — skipping." +else + bold "Building native binutils ${BINUTILS_VERSION} (host ${HOST})..." + rm -rf "${BUILD_DIR}/binutils-native" + mkdir -p "${BUILD_DIR}/binutils-native" + ( + cd "${BUILD_DIR}/binutils-native" + "${SRC_DIR}/binutils-${BINUTILS_VERSION}/configure" \ + --build=x86_64-pc-linux-gnu \ + --host="${HOST}" \ + --target="${HOST}" \ + --prefix=/sdk \ + --disable-nls \ + --disable-werror \ + --disable-gprofng \ + --disable-gold \ + --disable-plugins \ + --disable-shared \ + --enable-static + # Only gas/ld/binutils: gprof needs fscanf %[] scansets the + # Montauk libc lacks; gold and gprofng are disabled outright. + make -j"${JOBS}" all-gas all-ld all-binutils + make install-strip-gas install-strip-ld install-strip-binutils \ + DESTDIR="${NATIVE_BINUTILS}" + ) + green "Native binutils staged into toolchain/native/sdk." +fi + +# ── Native GCC ─────────────────────────────────────────────────────────────── +if [[ -x "${NATIVE_GCC}/sdk/bin/gcc" ]]; then + bold "Native GCC already staged — skipping." +else + bold "Building native GCC ${GCC_VERSION} (host ${HOST})..." + rm -rf "${BUILD_DIR}/gcc-native" + mkdir -p "${BUILD_DIR}/gcc-native" + ( + cd "${BUILD_DIR}/gcc-native" + "${SRC_DIR}/gcc-${GCC_VERSION}/configure" \ + --build=x86_64-pc-linux-gnu \ + --host="${HOST}" \ + --target="${HOST}" \ + --prefix=/sdk \ + --with-native-system-header-dir=/sdk/include \ + --with-build-sysroot="${SYSROOT}" \ + --enable-languages=c,c++ \ + --disable-nls \ + --disable-shared \ + --disable-multilib \ + --disable-gcov \ + --disable-lto \ + --disable-plugin \ + --disable-bootstrap \ + --disable-fixincludes \ + --with-newlib \ + --enable-initfini-array \ + --disable-wchar_t \ + --disable-libstdcxx-pch \ + --with-gnu-as \ + --with-gnu-ld + # The gcc subdir configure cannot run host (montauk) binaries to + # probe endianness; preset it. + ac_cv_c_bigendian=no make -j"${JOBS}" all-gcc + make install-gcc DESTDIR="${NATIVE_GCC}" + ) + + # install-gcc does not strip, and cc1/cc1plus carry ~700 MB of debug + # info between them (the ISO balloons to 1.7 GB if this is skipped). + bold "Stripping native GCC binaries..." + GCC_LIBEXEC="${NATIVE_GCC}/sdk/libexec/gcc/${HOST}/${GCC_VERSION}" + for b in "${GCC_LIBEXEC}/cc1" "${GCC_LIBEXEC}/cc1plus" "${GCC_LIBEXEC}/collect2" \ + "${NATIVE_GCC}/sdk/bin/gcc" "${NATIVE_GCC}/sdk/bin/g++" \ + "${NATIVE_GCC}/sdk/bin/cpp"; do + if [[ -f "${b}" ]]; then + "${PREFIX}/bin/${HOST}-strip" "${b}" + fi + done + green "Native GCC staged into toolchain/native-gcc/sdk." +fi + +# ── Done ───────────────────────────────────────────────────────────────────── +echo +green "=== Montauk SDK ready ===" +bold "Staged trees:" +bold " ${NATIVE_BINUTILS}/sdk (as, ld, ar, nm, objdump, ...)" +bold " ${NATIVE_GCC}/sdk (gcc, g++, cpp, cc1, cc1plus, collect2)" +bold "The next 'make' ships them into the OS image at 0:/sdk (devkit target)." diff --git a/toolchain/files/sdk-diag.c b/toolchain/files/sdk-diag.c index 8298c48..325da73 100644 --- a/toolchain/files/sdk-diag.c +++ b/toolchain/files/sdk-diag.c @@ -1,17 +1,22 @@ /* * sdk-diag.c * Probes the exact libc/kernel layers the GCC driver depends on: - * access/stat on the compiler backends, and a direct posix_spawn - * of cc1plus. Prints one line per probe. + * access/stat on the compiler backends, a direct posix_spawn of + * cc1plus, and BFD-style ar archive reads on /sdk/lib/libc.a + * (armap, member header at an armap offset, nested second fopen + * of the same file, member ELF magic). Prints one line per probe. */ #include +#include +#include #include #include #include #include #define CC1PLUS "/sdk/libexec/gcc/x86_64-montauk/14.2.0/cc1plus" +#define LIBC_A_DOTS "/sdk/bin/../lib/gcc/x86_64-montauk/14.2.0/../../../libc.a" static void probe(const char *p) { struct stat st; @@ -24,12 +29,101 @@ static void probe(const char *p) { printf("\n"); } +/* Replay the sequence BFD/ld performs on an ar archive. Each step + prints ok/FAIL so the first broken layer is visible directly. */ +static void ar_probe(const char *path) { + unsigned char buf[64]; + char hdr[61]; + long armap_size, member_off, member_size, i; + + printf("ar_probe %s\n", path); + + FILE *f = fopen(path, "rb"); + if (f == NULL) { printf(" FAIL fopen\n"); return; } + + /* Global magic */ + if (fread(buf, 1, 8, f) != 8) { printf(" FAIL read magic\n"); fclose(f); return; } + printf(" magic %s: %.7s\n", memcmp(buf, "!\n", 8) == 0 ? "ok" : "FAIL", (char *)buf); + + /* Armap ("/") member header at offset 8; size at header offset 48 */ + if (fseek(f, 8, SEEK_SET) != 0 || fread(hdr, 1, 60, f) != 60) { + printf(" FAIL read armap header\n"); fclose(f); return; + } + hdr[60] = '\0'; + armap_size = strtol(hdr + 48, NULL, 10); + printf(" armap hdr %s: name='%.4s' size=%ld fmag=%s\n", + hdr[0] == '/' ? "ok" : "FAIL", hdr, armap_size, + (hdr[58] == 0x60 && hdr[59] == '\n') ? "ok" : "FAIL"); + + /* First armap entry: 4-byte BE count, then BE member offsets */ + if (fread(buf, 1, 8, f) != 8) { printf(" FAIL read armap data\n"); fclose(f); return; } + member_off = ((long)buf[4] << 24) | ((long)buf[5] << 16) | ((long)buf[6] << 8) | (long)buf[7]; + printf(" armap syms=%ld first member offset=%ld\n", + ((long)buf[0] << 24) | ((long)buf[1] << 16) | ((long)buf[2] << 8) | (long)buf[3], + member_off); + + /* Member header at the armap offset, via the SAME stream */ + if (fseek(f, member_off, SEEK_SET) != 0 || fread(hdr, 1, 60, f) != 60) { + printf(" FAIL read member header\n"); fclose(f); return; + } + hdr[60] = '\0'; + member_size = strtol(hdr + 48, NULL, 10); + printf(" member hdr: name='%.16s' size=%ld fmag=%s\n", + hdr, member_size, (hdr[58] == 0x60 && hdr[59] == '\n') ? "ok" : "FAIL"); + + /* Nested second fopen of the same archive (ld does this per member), + ELF magic read at the member data offset while `f` stays open. */ + FILE *g = fopen(path, "rb"); + if (g == NULL) { printf(" FAIL nested fopen\n"); fclose(f); return; } + if (fseek(g, member_off + 60, SEEK_SET) != 0 || fread(buf, 1, 16, g) != 16) { + printf(" FAIL nested read\n"); fclose(g); fclose(f); return; + } + printf(" member ELF magic %s:", memcmp(buf, "\177ELF", 4) == 0 ? "ok" : "FAIL"); + for (i = 0; i < 8; i++) printf(" %02x", buf[i]); + printf("\n"); + + /* Whole-member read through the nested stream + checksum */ + { + unsigned long sum = 0; + long total = 0; + if (fseek(g, member_off + 60, SEEK_SET) != 0) { printf(" FAIL re-seek\n"); } + while (total < member_size) { + unsigned char chunk[4096]; + long want = member_size - total; + size_t got; + if (want > (long)sizeof(chunk)) want = sizeof(chunk); + got = fread(chunk, 1, (size_t)want, g); + if (got == 0) break; + for (i = 0; i < (long)got; i++) sum = sum * 31 + chunk[i]; + total += (long)got; + } + printf(" member read %ld/%ld bytes %s (sum=%08lx)\n", + total, member_size, total == member_size ? "ok" : "FAIL", sum); + } + + /* Interleave: original stream must still read correctly after the + nested stream was used (independent positions). */ + if (fseek(f, 0, SEEK_SET) == 0 && fread(buf, 1, 8, f) == 8) { + printf(" interleaved re-read %s\n", memcmp(buf, "!\n", 8) == 0 ? "ok" : "FAIL"); + } else { + printf(" FAIL interleaved re-read\n"); + } + + fclose(g); + fclose(f); +} + int main(void) { probe(CC1PLUS); probe("/sdk/libexec/gcc/x86_64-montauk/14.2.0"); probe("/sdk/libexec/gcc"); probe("/sdk/bin/as.elf"); probe("/tmp"); + probe("/sdk/lib/libc.a"); + probe(LIBC_A_DOTS); + + ar_probe("/sdk/lib/libc.a"); + ar_probe(LIBC_A_DOTS); pid_t pid = -1; char *argv[] = { (char *)"cc1plus", (char *)"--version", 0 }; diff --git a/toolchain/patches/gcc-14.2.0-montauk.patch b/toolchain/patches/gcc-14.2.0-montauk.patch index 827c69f..d5f732b 100644 --- a/toolchain/patches/gcc-14.2.0-montauk.patch +++ b/toolchain/patches/gcc-14.2.0-montauk.patch @@ -59,3 +59,32 @@ $as_echo "#define HAVE_HYPOT 1" >>confdefs.h +--- a/gcc/collect2.cc ++++ b/gcc/collect2.cc +@@ -1049,6 +1049,26 @@ + prefix_from_env ("COMPILER_PATH", &cpath); + prefix_from_env ("PATH", &path); + ++#ifdef __montauk__ ++ /* MontaukOS does not pass environment variables across spawn yet, so ++ COMPILER_PATH and PATH above are always empty. Derive the compiler ++ search path from argv[0] instead: the driver invokes collect2 by its ++ full path (the kernel provides the real exec path in argv[0]), and ++ `ld` lives in the same libexec directory. Also seed PATH with the ++ SDK bin directories so the nm/strip/tooldir fallbacks work. */ ++ if (cpath.plist == NULL && argv[0] != NULL) ++ { ++ const char *slash = strrchr (argv[0], '/'); ++ if (slash != NULL) ++ add_prefix (&cpath, xstrndup (argv[0], slash - argv[0] + 1)); ++ } ++ if (path.plist == NULL) ++ { ++ add_prefix (&path, "/sdk/x86_64-montauk/bin/"); ++ add_prefix (&path, "/sdk/bin/"); ++ } ++#endif ++ + /* Try to discover a valid linker/nm/strip to use. */ + + /* Maybe we know the right file to use (if not cross). */