crt0Gsos: honor Loader DP + DP-aware __jsl_indir

This commit is contained in:
Scott Duensing 2026-06-25 03:13:57 -05:00
parent c745821c7d
commit 6a90b2ec19
4 changed files with 125 additions and 22 deletions

Binary file not shown.

View file

@ -34,18 +34,38 @@ __start:
phk
plb
; Set DP=0. The C compiler assumes DP=0 for all `sta dp` and
; `[dp],y`-style accesses; GS/OS hands us a Memory-Manager-
; allocated DP page that we discard.
; HONOR the Loader-allocated DP. GS/OS' System Loader hands us a
; Memory-Manager-reserved direct-page (via the ~Direct OMF segment,
; or its 4 KB default) and points D at it on entry to __start.
; Earlier this crt did `lda #0 ; tcd` to force DP=0 because the
; libgcc `__jsl_indir` trampoline hard-coded `jmp ($00B8)` for
; indirect dispatch (which only matches DP-relative `sta $B8` when
; DP=0). That put every codegen DP slot — the indirect-call vector
; at $B8, the PBR stash at $BE, the libcall scratch at $E0..$FF —
; on top of bank-0 system zero page, which ROM IRQ handlers and
; GS/OS dispatcher state freely scribble. Clang-built S16 apps
; with C++ ctors crashed in the init_array indirect-dispatch loop
; for exactly that reason.
;
; The fix: keep DP at the Loader's allocation (no `tcd #0` here),
; AND patch `__jsl_indir_op` (the operand of the JMP (abs) inside
; libgcc's __jsl_indir trampoline) to (DP + $B8) so the trampoline
; reads the indirect target from the right bank-0 slot. See
; libgcc.s commentary on the SMC anchor. Every codegen DP slot
; now lands in Loader-reserved memory that no system code touches.
rep #0x30
lda #0
tcd
tdc
clc
adc #0xb8
sta __jsl_indir_op
; Persistent "current data bank" byte at DP $BE. Set to PBR
; (= our load bank) so the LDAptr/STAptr/STBptr inserters'
; "LDA $BE; STA $E2" sequence puts pointer derefs in our bank,
; matching DBR-relative absolute stores. $BE is outside the
; libcall scratch range ($E0..$FF used by libgcc.s for i64 ops).
; The DP form `sta $BE` writes to (DP+$BE) — Loader-allocated
; memory under GS/OS, bank-0 zero page under bare-metal/GNO.
; See crt0.s.
sep #0x20
phk

View file

@ -34,19 +34,39 @@
; ... arg pushes ...
; jsl __jsl_indir
;
; __indirTarget MUST live at a fixed bank-0 storage slot. On the
; 65816, JMP (abs) (opcode 0x6C) reads its vector from BANK 0
; unconditionally — it ignores both PBR and DBR. If __indirTarget
; were in normal BSS, GS/OS Loader / GNO would relocate the BSS
; storage into the program's bank (e.g., $01:9180), but the JMP would
; still read from $00:9180 — wherever, garbage, crash. Pinning it
; at DP $B8 sidesteps the relocation: DP=0 in our runtime, so DP slot
; $B8 IS bank-0 byte $00:00B8. Callers store via `sta $B8` (DP form),
; which writes to the same $00:00B8 regardless of DBR; the trampoline
; below reads via `jmp ($00B8)` (16-bit abs operand into bank-0
; indirect). Single-bank assumption on the *target's* code remains
; (JMP indirect transfers to current PBR), but program data location
; no longer matters.
; __indirTarget is a DP-relative slot at offset $B8. Callers store the
; target's 16-bit address with `sta $B8` (DP-form, writes to (DP+$B8)
; in bank 0). The trampoline below is a JMP (abs) whose operand has
; been patched by the crt0 variant at startup to point at the runtime
; DP+$B8 anchor; the JSL → JMP → target flow then takes a single CPU
; instruction past the JSL, so A and X (which hold arg0 for the 65816
; C ABI) survive the indirection unmodified.
;
; Why the SMC dispatch instead of a fixed `jmp ($00B8)`:
; Under bare-metal (crt0.s) and GNO/ME (crt0Gno.s) the runtime sets
; DP=0, so the patched operand stays $00B8 and the dispatch is byte-
; identical to the legacy fixed form. Under the GS/OS Loader
; (crt0Gsos.s) we now keep DP at the Loader-allocated value rather
; than forcing DP=0 — that keeps every codegen scratch slot (the
; indirect-call anchor at $B8, the PBR stash at $BE, the libcall
; scratch at $E0..$FF) in Loader-reserved bank-0 memory instead of
; on top of system zero page that ROM IRQ handlers and GS/OS
; dispatcher state freely scribble. Forcing DP=0 was making clang-
; built S16 apps with C++ ctors crash during the init_array
; indirect-dispatch loop for exactly that reason.
;
; Constraint preserved: the JMP-indirect anchor must be in bank 0 (the
; 65816 JMP (abs) opcode 0x6C reads its PC vector from bank-0
; unconditionally). DP is always in bank 0 on the 65816, so
; (DP+$B8) is automatically a bank-0 address. The target that gets
; jumped to runs in the current PBR (intra-bank function pointer);
; cross-bank function pointers need a different dispatch (the Phase
; C.2 cINTERSEG path on the JSL, not on this vector).
;
; SMC patch site: crt0 variants compute (DP + $B8) at startup with
; tdc ; clc ; adc #0xb8 ; sta __jsl_indir_op
; The default operand below is $00B8 so a program that links without
; patching (e.g. an asm-only test) still works under DP=0.
; --------------------------------------------------------------------
.globl __indirTarget
__indirTarget = 0xb8
@ -54,9 +74,14 @@ __indirTarget = 0xb8
.text
.globl __jsl_indir
__jsl_indir:
; PC <- mem[$00:00B8]. Indirect dispatch through the bank-0-
; anchored function-pointer slot — opcode 0x6C (JMP (abs)).
jmp (__indirTarget)
.byte 0x6c ; JMP (abs) opcode (Apple 65816 uses 16-bit operand;
; the resulting effective address is bank-0:operand,
; from which the 16-bit target PC is read).
.globl __jsl_indir_op
__jsl_indir_op:
.byte 0xb8, 0x00 ; operand: bank-0 address of the target slot.
; Patched at startup by crt0 to (DP + $B8); default
; $00B8 is correct when DP=0 (bare-metal, GNO).
; --------------------------------------------------------------------
; __run_cxa_atexit — weak no-op fallback.

View file

@ -6569,6 +6569,61 @@ EOF
"${binPhc2%.bin}".seg*.bin "${relPhc2}".seg*.reloc
fi
# crt0Gsos indirect-dispatch regression: ensure the __jsl_indir SMC patch
# (`tdc; clc; adc #$B8; sta __jsl_indir_op` in crt0Gsos.s) lets a clang-
# built S16 app survive an init_array-style indirect call under the
# real GS/OS Loader. Before the fix, crt0Gsos forced DP=0 and the
# indirect dispatcher hard-coded $00B8 — but bank-0 zero page is
# scribbled by ROM IRQ handlers + GS/OS dispatcher state, so the first
# indirect call (C++ static ctor, etc.) raced and crashed. The probe
# below uses a function-pointer table to force the codegen indirect
# path through __jsl_indir. Marker $70 = 0x0B = twice(7) + neg(3).
if [ "${SMOKE_SKIP_GSOSINDIR:-0}" != "1" ] \
&& [ -x "$CLANG" ] && [ -x "$CADIUS" ] && [ -f "$SYSDISK" ] \
&& command -v mame >/dev/null 2>&1; then
log "check: crt0Gsos indirect-dispatch under real GS/OS Loader"
cGin="$(mktemp --suffix=.c)"
oGin="$(mktemp --suffix=.o)"
binGin="$(mktemp --suffix=.bin)"
mapGin="$(mktemp --suffix=.map)"
relGin="$(mktemp --suffix=.reloc)"
omfGin="$(mktemp --suffix=.omf)"
cat > "$cGin" <<'EOF'
typedef int (*FnT)(int);
static int twice(int x) { return x * 2; }
static int neg(int x) { return -x; }
volatile int gI = 0;
int main(void) {
FnT tab[2] = { twice, neg };
int r = tab[gI](7) + tab[1](3);
*(volatile unsigned char *)0x70 = (unsigned char)r;
return 0;
}
EOF
"$CLANG" --target=w65816 -I"$PROJECT_ROOT/runtime/include" -O2 \
-ffunction-sections -c "$cGin" -o "$oGin"
"$PROJECT_ROOT/tools/link816" -o "$binGin" --text-base 0x1000 \
--bss-base 0xA000 --map "$mapGin" --reloc-out "$relGin" \
"$PROJECT_ROOT/runtime/crt0Gsos.o" "$oGin" \
"$PROJECT_ROOT/runtime/libc.o" "$PROJECT_ROOT/runtime/snprintf.o" \
"$PROJECT_ROOT/runtime/extras.o" \
"$PROJECT_ROOT/runtime/softFloat.o" "$PROJECT_ROOT/runtime/softDouble.o" \
"$PROJECT_ROOT/runtime/iigsGsos.o" \
"$PROJECT_ROOT/runtime/iigsToolbox.o" "$PROJECT_ROOT/runtime/iigsToolboxExtra.o" \
"$PROJECT_ROOT/runtime/libgcc.o" >/dev/null 2>&1 \
|| die "crt0Gsos indirect: link failed"
"$PROJECT_ROOT/tools/omfEmit" --input "$binGin" --map "$mapGin" \
--base 0x1000 --entry __start --output "$omfGin" \
--name INDIR --expressload --relocs "$relGin" >/dev/null 2>&1
if ! bash "$PROJECT_ROOT/scripts/runViaFinder.sh" "$omfGin" \
--check 0x70=0x0b >/dev/null 2>&1; then
bash "$PROJECT_ROOT/scripts/runViaFinder.sh" "$omfGin" \
--check 0x70=0x0b 2>&1 | tail -5 >&2
die "crt0Gsos indirect: marker != 0x0B (DP scribble?)"
fi
rm -f "$cGin" "$oGin" "$binGin" "$mapGin" "$relGin" "$omfGin"
fi
# Stack-size end-to-end: omfEmit --stack-size must actually propagate a
# larger DP/Stack chunk to the GS/OS Loader. Background: prior to the
# 2026-06-02 fix, the ~Direct DP/Stack segment was appended to the OMF
@ -6595,12 +6650,15 @@ if [ "${SMOKE_SKIP_STACKSIZE:-0}" != "1" ] \
// store its high byte at $71 so the harness can verify Loader honored
// --stack-size. $70 = 0x99 marker = program ran.
int main(void) {
// sta long $00:0071 — force bank-0 absolute (not DP form). crt0Gsos
// now honors the Loader-allocated DP rather than forcing DP=0, so a
// bare `sta $71` would land at (LoaderDP+$71) and miss bank-0:$71.
__asm__ volatile (
"rep #0x30\n"
"tsc\n"
"xba\n"
"sep #0x20\n"
"sta 0x71\n"
".byte 0x8f, 0x71, 0x00, 0x00\n"
"rep #0x20\n"
);
*(volatile unsigned char *)0x70 = 0x99;