diff --git a/demos/rsrcProbe.apl b/demos/rsrcProbe.apl index 427b355..1648aec 100644 Binary files a/demos/rsrcProbe.apl and b/demos/rsrcProbe.apl differ diff --git a/runtime/src/crt0Gsos.s b/runtime/src/crt0Gsos.s index 9792e6e..62c4425 100644 --- a/runtime/src/crt0Gsos.s +++ b/runtime/src/crt0Gsos.s @@ -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 diff --git a/runtime/src/libgcc.s b/runtime/src/libgcc.s index 8410117..e0b1636 100644 --- a/runtime/src/libgcc.s +++ b/runtime/src/libgcc.s @@ -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. diff --git a/scripts/smokeTest.sh b/scripts/smokeTest.sh index 322adeb..f76f524 100755 --- a/scripts/smokeTest.sh +++ b/scripts/smokeTest.sh @@ -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;