fix(amd64): the image runs wherever the firmware loads it -- -fpie, every symbol hidden; qemu-stick-high in acceptance
The SER5 stopped at the loader's "Loading kernel from ESP...": built -fno-pic -fno-pie with
ImageBase 0, the image carried ~9,000 32-bit absolute addresses, and the firmware loaded it above
2 GiB, where the sign-extended ones (R_X86_64_32S) point into unmapped memory -- a page fault in
debug_checkpoint() before [CKPT 001]. Reproduced on QEMU's pc machine at 3 GiB (#PF, CR2 =
FFFFFFFFBD316542, the right address sign-extended); q35 always loads it below 2 GiB, so no QEMU
boot ever showed it.
amd64 now builds -fpie with kernel/include/starkernel/pie_hidden.h force-included (#pragma GCC
visibility push(hidden)), so globals are RIP-relative and none is reached through a GOT -- the
2026-08-06 bug that put -fno-pic there. The link refuses any object carrying a 32-bit absolute or
GOT relocation (4,531 on the old objects, none now). test_runner.c's visibility("default") on
global_test_stats is kept for the -fPIC ISAs only. make -f kernel/Makefile ARCH=amd64
qemu-stick-high boots the PC stick on pc with 3 GiB and is part of acceptance (.claude/CLAUDE.md).
Captain Bob, 2026-10-29; FABRIC-3.md XLIV.
Verified: qemu-stick-high with Zuse's pair, ok> and 7 ok, RIP sampled inside .text at base
0xBD297000 (logs/20261029-070253); acceptance amd64 -070508, aarch64 -070715, riscv64 -071138,
PARITY:V4_POST tests=538 pass=538 fail=0, POST: PASSED, dict_hash=0xfec8c47cfc6f437f on all;
make -C v4 test-32, 39 suites, 0 failures. Not yet booted on the SER5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0a31e558be
commit
d6ea592c08
+46
-17
@@ -271,23 +271,28 @@ ifeq ($(ARCH),amd64)
|
||||
CC := x86_64-elf-gcc
|
||||
LD := x86_64-elf-ld
|
||||
OBJCOPY := x86_64-elf-objcopy
|
||||
READELF := x86_64-elf-readelf
|
||||
else
|
||||
CC := gcc
|
||||
LD := ld
|
||||
OBJCOPY := objcopy
|
||||
READELF := readelf
|
||||
endif
|
||||
# -fno-pic -fno-pie: this is a fixed-base, statically-linked freestanding
|
||||
# image with no dynamic linker to populate a GOT at load time. -fPIC (set
|
||||
# in COMMON_CFLAGS, needed by riscv64's -shared link pipeline) makes GCC
|
||||
# emit R_X86_64_REX_GOTPCRELX (GOT-indirect) addressing for some cross-TU
|
||||
# extern globals depending on per-call-site codegen heuristics; the "GOT
|
||||
# slot" is just an unpopulated .bss cell here, so those reads silently
|
||||
# return NULL instead of the real address. Root-caused 2026-08-06 via
|
||||
# dict_find_word_heat_aware() reading sf_fc_count as NULL while
|
||||
# vm_find_word() (same globals, same TU as the definition) read it
|
||||
# correctly. These flags come after COMMON_CFLAGS on the command line so
|
||||
# they win for amd64 only; riscv64/aarch64 keep -fPIC.
|
||||
ARCH_CFLAGS := -m64 -march=x86-64 -mno-red-zone -DARCH_AMD64 -fno-pic -fno-pie
|
||||
# -fpie + pie_hidden.h: the firmware loads this one PE image wherever it
|
||||
# chooses, and a PC's puts it above 2 GiB (the SER5 did). The -fno-pic
|
||||
# -fno-pie this replaced emitted 32-bit absolute addresses, sign-extended
|
||||
# into unmapped memory up there: a page fault before the loader's first
|
||||
# checkpoint (FABRIC-3.md XLIV). -fpie addresses globals RIP-relative.
|
||||
# pie_hidden.h makes every declaration hidden, so GCC never reaches one
|
||||
# through a GOT: this image has no dynamic linker and ld -m i386pep builds
|
||||
# no GOT, so such a read returns NULL -- the 2026-08-06 bug that -fPIC
|
||||
# caused (dict_find_word_heat_aware() reading sf_fc_count as NULL,
|
||||
# FABRIC-0.md). AMD64_RELOC_FORBIDDEN, checked before the link below,
|
||||
# makes either one fail the build. These flags come after COMMON_CFLAGS'
|
||||
# -fPIC on the command line so they win for amd64 only; riscv64/aarch64
|
||||
# keep -fPIC.
|
||||
ARCH_CFLAGS := -m64 -march=x86-64 -mno-red-zone -DARCH_AMD64 -fpie \
|
||||
-include kernel/include/starkernel/pie_hidden.h
|
||||
LOADER_LINKER_SCRIPT := kernel/linker/starkernel-loader-amd64.ld
|
||||
KERNEL_LINKER_SCRIPT := kernel/linker/starkernel-kernel-amd64.ld
|
||||
|
||||
@@ -342,7 +347,7 @@ LOADER_LD ?= $(LD)
|
||||
|
||||
# Common flags for all arches / both loader and kernel.
|
||||
# -fPIC + -fvisibility=hidden: needed by riscv64's -shared loader link
|
||||
# pipeline (amd64 overrides back to -fno-pic/-fno-pie in its ARCH_CFLAGS
|
||||
# pipeline (amd64 overrides it with -fpie and pie_hidden.h in its ARCH_CFLAGS
|
||||
# above -- see that comment for why: -fPIC made GCC emit GOT-indirect
|
||||
# addressing for some cross-TU extern globals, and this freestanding image
|
||||
# has no dynamic linker to populate a GOT, so those reads silently returned
|
||||
@@ -943,6 +948,13 @@ $(KERNEL_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.S | $(KERNEL_OBJ_DIR)
|
||||
# LINK RULES
|
||||
# ==============================================================================
|
||||
|
||||
# amd64: relocations the image may not carry. The firmware loads the PE anywhere
|
||||
# and patches only the low 32 bits of a 32-bit absolute (R_X86_64_32, _32S), so
|
||||
# above 2 GiB (_32S) or 4 GiB (_32) the address is wrong; and a GOT relocation
|
||||
# reads a slot nothing fills (no dynamic linker, no GOT from ld -m i386pep).
|
||||
# -fpie and pie_hidden.h (ARCH_CFLAGS) leave none; this keeps it so.
|
||||
AMD64_RELOC_FORBIDDEN := ^R_X86_64_(32|32S|GOT[A-Z0-9_]*|REX_GOTPCRELX|PLTOFF64)$$
|
||||
|
||||
# Link loader to PE32+
|
||||
# amd64: ld -m i386pep → PE directly
|
||||
# aarch64: lld-link links COFF objects → PE (auto-generates .reloc)
|
||||
@@ -977,6 +989,15 @@ else ifeq ($(ARCH),riscv64)
|
||||
$(BUILD_DIR)/starkernel_loader_stripped.elf $@
|
||||
@echo "Loader built: $@"
|
||||
else
|
||||
@bad=$$(for o in $(LOADER_OBJS); do \
|
||||
$(READELF) -rW $$o | awk -v o=$$o '$$3 ~ /$(AMD64_RELOC_FORBIDDEN)/ { print " " o ": " $$3 " " $$5 }'; \
|
||||
done); \
|
||||
if [ -n "$$bad" ]; then \
|
||||
echo "$$bad" | head -20; \
|
||||
echo "FATAL: $$(echo "$$bad" | wc -l) GOT or 32-bit absolute relocation(s) in the amd64 image --"; \
|
||||
echo " wrong at a load address above 2 GiB (FABRIC-3.md XLIV)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "LD (loader PE) $@"
|
||||
@$(LD) $(LOADER_LDFLAGS_PE) $(LOADER_OBJS) -o $@
|
||||
@echo "Loader built: $@"
|
||||
@@ -1273,8 +1294,16 @@ else ifeq ($(ARCH),riscv64)
|
||||
# on the xHCI controller, booted by OVMF from its ESP. No -cdrom, no virtio disk, no ZUSEDISK:
|
||||
# the kernel finds the stick through the USB storage driver and GPT code a PC uses. amd64 only
|
||||
# (ARM and RISC-V sticks are later, STICK.md 10). Serial, QMP and logs/<TS>/amd64/ as `qemu`.
|
||||
#
|
||||
# qemu-stick-high -- the same boot on the pc machine with 3 GiB, where OVMF loads the image above
|
||||
# 2 GiB, as a PC's firmware does (the SER5 put it there; q35 keeps RAM below 2 GiB and the image
|
||||
# with it). Part of acceptance: an image that works only loaded low fails here (FABRIC-3.md XLIV).
|
||||
STICK_PC_IMG := build/boards/pc/lithos-pc.img
|
||||
.PHONY: qemu-stick
|
||||
STICK_MACHINE ?= q35
|
||||
STICK_MEM ?= 1024
|
||||
.PHONY: qemu-stick qemu-stick-high
|
||||
qemu-stick-high:
|
||||
@$(MAKE) -f kernel/Makefile ARCH=$(ARCH) STICK_MACHINE=pc STICK_MEM=3G qemu-stick
|
||||
qemu-stick:
|
||||
ifneq ($(ARCH),amd64)
|
||||
@echo "qemu-stick: the stick is the PC's -- ARCH=amd64 (ARM and RISC-V sticks are later, STICK.md 10)"; exit 1
|
||||
@@ -1292,15 +1321,15 @@ else
|
||||
CSV=$(DOE_RUNS_DIR)/doe-amd64-$$TS.csv; \
|
||||
SERIAL_SOCK=$(BUILD_DIR)/qemu-serial-amd64-$$TS.sock; \
|
||||
QMP_SOCK=$(BUILD_DIR)/qemu-qmp-amd64-$$TS.sock; \
|
||||
echo "=== Launching QEMU (amd64): the PC stick over USB ==="; \
|
||||
echo "=== Launching QEMU (amd64): the PC stick over USB ($(STICK_MACHINE), $(STICK_MEM)) ==="; \
|
||||
echo " Serial log: $$LOG"; \
|
||||
echo " QMP socket: $$QMP_SOCK"; \
|
||||
touch $$LOG; \
|
||||
tail -n +1 -f $$LOG & TAILPID=$$!; \
|
||||
qemu-system-x86_64 \
|
||||
-machine q35,accel=tcg \
|
||||
-machine $(STICK_MACHINE),accel=tcg \
|
||||
-cpu qemu64,+rdrand \
|
||||
-m 1024 \
|
||||
-m $(STICK_MEM) \
|
||||
-vga std \
|
||||
-drive if=pflash,format=raw,readonly=on,file=$(OVMF_CODE) \
|
||||
-drive if=pflash,format=raw,file=$(BUILD_DIR)/OVMF_VARS-stick.fd \
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* pie_hidden.h -- every symbol in the amd64 image is the image's own.
|
||||
*
|
||||
* Force-included (-include) ahead of every amd64 kernel and loader source by
|
||||
* kernel/Makefile's ARCH_CFLAGS, together with -fpie. The image is one PE file,
|
||||
* linked by ld -m i386pep, loaded by the firmware wherever it chooses: a PC's
|
||||
* firmware puts it above 2 GiB (the Beelink SER5 did), where the 32-bit absolute
|
||||
* addresses of -fno-pic code are sign-extended into unmapped memory and the
|
||||
* loader page-faults before its first checkpoint (FABRIC-3.md XLIV). -fpie
|
||||
* addresses every global relative to RIP, which is right at any load address.
|
||||
*
|
||||
* -fpie alone is not enough: GCC treats an extern declaration of default
|
||||
* visibility as possibly defined in another module and reaches it through the
|
||||
* GOT. This image has no dynamic linker and ld -m i386pep builds no GOT, so
|
||||
* such a read returns zero -- the 2026-08-06 bug (FABRIC-0.md). -fvisibility=
|
||||
* hidden covers definitions only; this pragma covers declarations too, so GCC
|
||||
* never uses the GOT. (EDK2's GCC X64 ProcessorBind.h does the same.)
|
||||
*
|
||||
* The Makefile checks the result: the link fails if any object still carries
|
||||
* a GOT or 32-bit absolute relocation.
|
||||
*/
|
||||
#ifndef STARKERNEL_PIE_HIDDEN_H
|
||||
#define STARKERNEL_PIE_HIDDEN_H
|
||||
|
||||
#pragma GCC visibility push(hidden)
|
||||
|
||||
#endif /* STARKERNEL_PIE_HIDDEN_H */
|
||||
Reference in New Issue
Block a user