65816-llvm-mos/runtime/src/crt0Gsos.s
2026-07-07 18:33:48 -05:00

194 lines
8.2 KiB
ArmAsm

; crt0Gsos.s — GS/OS S16 application crt0.
;
; Use this INSTEAD OF crt0.s when building an OMF that the real
; GS/OS Loader will launch. Differences from crt0.s:
; - No SEI / interrupt-source clearing (GS/OS owns the IRQ chain).
; - No language-card RAM enable (GS/OS configures memory).
; - No stack base reset (GS/OS allocated and set our SP).
; - Honors GS/OS's DBR=our-bank, DP=allocated-page setup.
; - On main() return, calls GS/OS QUIT(pcount=0) to return control to
; the launching program (Finder / shell / boot chain) — the standard
; GS/OS application exit.
;
; Entry from the System Loader (per Apple IIgs Toolbox Reference):
; E=0 (native), M=0 (16-bit accumulator), X=0 (16-bit index)
; DBR = the bank into which our entry segment was placed
; DP = pointer to a Memory-Manager-allocated DP page
; Stack at entry, top-down:
; PCL PCH PBR (3 bytes — JSL return addr to launcher)
; flags-lo flags-hi (2 bytes — launch flags)
; path-low path-mid path-bank pad (4 bytes — pathname long ptr)
;
; QUIT discards the entire stack so we never need to pop the launch
; frame ourselves.
.text
.globl __start
__start:
; Set DBR := PBR so absolute (DBR-relative) loads/stores reach
; symbols within our segment. The Loader's documented contract is
; "DBR set to your bank" but verified empirically NOT always true
; for KIND=0x1000 segments — `lda absConst` was reading from the
; wrong bank without this. PHK + PLB copies PBR into DBR.
phk
plb
; 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
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
pla
sta 0xbe
stz 0xbf ; pad byte so `lda 0xbe` in 16-bit M reads $00PBR
; (codegen uses this as the bank-half of `&symbol`
; 32-bit pointers — see W65816AsmPrinter
; LDAi16imm_bank expansion)
rep #0x20
; BSS zero-init: NOT NEEDED under GS/OS. omfEmit embeds the BSS
; region as zeros inside the LCONST data, so the GS/OS Loader
; allocates+fills our BSS during segment load — by the time __start
; runs, BSS is already zero. A redundant `stz` loop here was found
; to HANG fopen / gsosOpen (Phase 1.1 root cause, 2026-06-02): when
; BSS extends past runtime offset ~$9E00 in the placed bank,
; re-zeroing that region corrupts GS/OS Memory-Manager / dispatcher
; state that lives in our allocated chunk between the Loader's
; LCONST-fill and our __start entry. Skipping the redundant zero
; eliminates the corruption; semantics are preserved because the
; Loader already did it.
; See feedback_gsos_fopen_partial_diagnosis (root-caused this session).
.Lbss_done:
; (M is already 16-bit; the heap-init block below re-asserts rep #0x30
; before it needs a 16-bit index, so no width fixup is needed here.)
; Back the C malloc heap with a locked Memory-Manager handle so the
; heap span is MM-owned -- otherwise a later NewHandle (ours or the
; system's) can place a block on top of live malloc data (JoeyLib
; item 5, heap channel). MMStartUp ($0202) returns our Memory-Manager
; user ID in A; pass it as arg0 to __heapInitMM (strong impl in libc.o;
; weak no-op in libgcc.s for programs that don't link libc). crt0Gno.s
; does the SAME (it reuses the MM user ID GNO hands it in A -- stashed at
; DP $B6 -- instead of calling MMStartUp, since GNO already started the
; Memory Manager). Only crt0.s (bare-metal, no Memory Manager) skips this
; and keeps malloc's link-time window.
rep #0x30
pha ; MMStartUp result space (word)
ldx #0x0202 ; MMStartUp call number
jsl 0xe10000 ; Tool Locator dispatch -> A = our MM user ID
pla
jsl __heapInitMM ; __heapInitMM(userID); arg0 in A
; Walk .init_array (C++ ctors). The `jsl __jsl_indir` and the
; `jsl main` below are relocated by the GS/OS Loader: omfEmit emits
; a cRELOC (0xF5) IMM24 site for each intra-segment JSL, so the
; Loader patches the full 24-bit operand (bank included) to the
; placed address. Non-zero-bank placement works as long as the OMF
; is built with --relocs (demos/build.sh / buildGno.sh do).
rep #0x30
ldx #__init_array_start
.Linit_loop:
cpx #__init_array_end
bcs .Linit_done
stx 0xe0
ldy #0
lda (0xe0), y
sta 0xb8 ; __indirTarget pinned at DP $B8 (bank-0 fixed; see libgcc.s)
phx
jsl __jsl_indir
plx
; Step by 4 bytes (sizeof(void*) under p:32:16).
inx
inx
inx
inx
bra .Linit_loop
.Linit_done:
; NOTE: do NOT add per-process TOOL startup here (TLStartUp, or a full
; MM/QD/EM/WM StartUp chain). The GS/OS Loader has already initialised
; the Tool Locator before transferring control to __start (helloBeep
; proves this — it does a SysBeep JSL $E10000 with zero init code), and
; per-process tool init is the program's responsibility (the desktop
; demos use startdesk(640) from runtime/include/iigs/desktop.h). The
; bare MMStartUp above is NOT that: it only fetches our existing MM user
; ID for __heapInitMM and starts no tool set.
; Seed rand() from the IIgs RTC via ReadTimeHex ($0D03). Without
; this, srand defaults to seed=1 and every run produces an identical
; PRNG sequence -- a correctness bug for mkstemp / tmpnam and any
; user code relying on Monte-Carlo-style uniqueness. TL is up
; (Loader brought it up), so the JSL inside __srandInitFromTime is
; safe. Linker drops the symbol when no rand-consuming code is in
; the link, so this costs ~0 bytes for non-PRNG programs (the
; reference is one weak-resolved JSL).
rep #0x30
jsl __srandInitFromTime
; Call main. Standard W65816 C ABI: arg0 in A; we pass none.
rep #0x30
jsl main
; Run global dtors registered via __cxa_atexit (LIFO order) before
; QUIT. A holds main()'s return value; QUIT itself doesn't consume
; it (we set up the parm block immediately below), but keep the
; save/restore for parity with crt0Gno.
pha
jsl __run_cxa_atexit
pla
; ---- QUIT (pcount=0): return to the launching program ----------
; The standard GS/OS application exit. A pcount=0 parm block (just
; a zero pcount word) tells QUIT to return control to whoever
; launched us (Finder, a shell, the boot chain) with default flags
; (non-restartable) — no hardcoded chain path needed. A program
; that wants to chain elsewhere can provide its own _exit/crt0.
rep #0x30
stz 0x80 ; pcount = 0 (parm block at DP $0080, bank 0)
tdc
clc
adc #0x80
pha ; parm-block ptr: low 16 (offset)
pea 0 ; parm-block ptr: high 16 (bank 0)
ldx #0x2029 ; QUIT class-1 call number
jsl 0xe100a8 ; GS/OS dispatcher
; QUIT only returns on failure. Clean up the pushed pointer and
; spin (don't fall into a BRK, which headless MAME mis-vectors).
pla
pla
.Lhang:
bra .Lhang
.size __start, . - __start