65816-llvm-mos/runtime/src/crt0.s
2026-05-30 19:40:29 -05:00

245 lines
8.5 KiB
ArmAsm

; crt0 — C runtime startup for the W65816 backend.
;
; Entry point invoked by the loader (or the OMF dispatcher). Sets up
; the processor mode the rest of the runtime expects, zeroes BSS,
; calls main, and halts on return.
;
; Conventions:
; - Native mode (E=0), 16-bit M and X (REP #$30) on entry to main.
; - DP=0, DBR=0 — assumed by the C runtime.
; - Linker-emitted symbols: __bss_start, __bss_end (16-bit addrs).
.text
.globl __start
__start:
; Disable IRQ first — the IIgs ROM hands a vsync IRQ on every frame,
; and its handler runs in 8-bit M/X mode, corrupting our state if
; we leave I clear. SEI is fine in either emulation or native
; mode and is always 1 byte / 2 cycles.
sei
; Native mode + 16-bit registers.
clc
xce
rep #0x30
; Disable IIgs peripheral interrupt sources at the chip level —
; SEI alone leaves the hardware lines asserted, and the IRQ trap
; in ROM keeps re-firing if the source isn't quiesced. STZ
; stores zero without going through A; in M=8 it stores 1 byte
; (matching the 8-bit registers), so no LDA #0 prelude is needed.
sep #0x20
stz 0xc041 ; INTEN = 0 (clear AN3/mouse/0.25s/VBL/mouse-IRQ enables)
stz 0xc023 ; VGCINT = 0 (clear external/1-sec/scan-line IRQ enables)
stz 0xc032 ; SCANINT clear
rep #0x20
; Top-of-stack at $0FFF. Native-mode S is 16-bit, so we don't need
; to stay in page 1. Soft-double frames can be ~170 bytes plus the
; usual call-chain overhead — at $01FF stack growth wraps into the
; direct page ($0000-$00FF) which holds our libcall scratch
; ($E0-$F4) and IMG slots ($D0-$DE), corrupting them. $0FFF gives
; ~3.5 KB of headroom and stays below the text base ($1000).
lda #0x0fff
tcs
; Enable Language Card RAM at $D000-$FFFF for read+write. This
; is 12KB (4KB at $D000-$DFFF in LC bank 1, plus 8KB at
; $E000-$FFFF common LC area). The IIgs LC area defaults to
; ROM-mapped; two reads of $C083 enable bank-1 RAM read AND
; write for the whole $D000-$FFFF range. link816 may auto-
; relocate BSS / heap into this area when text+rodata grows
; past $BFFF — without this enable, writes drop on the floor
; and reads return ROM bytes. The reads must be 8-bit (one
; byte at a time) — a 16-bit M read at $C083 would also touch
; $C084 (a different soft switch), wiping the LC enable.
sep #0x20
lda 0xc083
lda 0xc083
rep #0x20
; Persistent "current data bank" byte at DP $BE. The LDAptr/
; STAptr/STBptr inserters load this into $E2 before each [dp],Y
; deref so pointer-deref lands in the user's bank, matching where
; DBR-relative absolute stores go. Under MAME (no Loader), DBR=0
; and PBR=0 here, so $BE=0 — equivalent to the prior STZ $E2
; behavior. Under GS/OS Loader, crt0Gsos.s sets it to PBR.
;
; $BE chosen because it's outside both the libcall scratch range
; ($E0..$FF used by libgcc.s for i64 ops) and the IMG slot range
; ($C0..$DE). PHK pushes 1 byte; PLA in M=8 to pull just 1 byte.
sep #0x20
phk
pla ; A's low byte = current PBR
sta 0xbe ; persistent data bank
stz 0xbf ; pad: `lda 0xbe` in 16-bit M loads $00PBR
; — used by codegen for &symbol bank halves
rep #0x20
; Zero BSS. Up to 4 segments — linker emits __bss_seg{0..3}_lo16
; / _bank / _size symbols. Segments with size=0 are skipped.
; Each segment is cleared with DBR-relative STZ abs,X after
; setting DBR to the segment's bank. Original DBR restored at
; end via PLB.
rep #0x10 ; ensure X is 16-bit
phb ; save current DBR
; ---- segment 0 ----
; The `sep #0x20` that sets the segment's data bank also puts us in
; M=8 for the whole store loop (X stays 16-bit), so we don't flip
; SEP/REP per byte; each segment label below re-enters via `rep`.
rep #0x20
ldx #__bss_seg0_size
beq .Lbss_seg1
sep #0x20
.a8 ; following lda #imm8 uses 1-byte immediate
lda #__bss_seg0_bank
pha
plb
ldx #0
.Lbss_loop0:
cpx #__bss_seg0_size
bcs .Lbss_seg1
stz __bss_seg0_lo16, x
inx
bra .Lbss_loop0
.Lbss_seg1:
; ---- segment 1 ----
rep #0x20
ldx #__bss_seg1_size
beq .Lbss_seg2
sep #0x20
.a8 ; following lda #imm8 uses 1-byte immediate
lda #__bss_seg1_bank
pha
plb
ldx #0
.Lbss_loop1:
cpx #__bss_seg1_size
bcs .Lbss_seg2
stz __bss_seg1_lo16, x
inx
bra .Lbss_loop1
.Lbss_seg2:
; ---- segment 2 ----
rep #0x20
ldx #__bss_seg2_size
beq .Lbss_seg3
sep #0x20
.a8 ; following lda #imm8 uses 1-byte immediate
lda #__bss_seg2_bank
pha
plb
ldx #0
.Lbss_loop2:
cpx #__bss_seg2_size
bcs .Lbss_seg3
stz __bss_seg2_lo16, x
inx
bra .Lbss_loop2
.Lbss_seg3:
; ---- segment 3 ----
rep #0x20
ldx #__bss_seg3_size
beq .Lbss_done
sep #0x20
.a8 ; following lda #imm8 uses 1-byte immediate
lda #__bss_seg3_bank
pha
plb
ldx #0
.Lbss_loop3:
cpx #__bss_seg3_size
bcs .Lbss_done
stz __bss_seg3_lo16, x
inx
bra .Lbss_loop3
.Lbss_done:
rep #0x20 ; back to M=16 after the M=8 store loop
plb ; restore caller's DBR
; Run static constructors. The linker emits
; __init_array_start / __init_array_end around the .init_array
; section; under p:32:16 each entry is a 32-bit function pointer
; (low 16 bits = function offset, high 16 bits = bank, 0 for our
; single-bank programs). Walk in 4-byte stride and JSL each via
; __jsl_indir using only the low half.
rep #0x30 ; native, 16-bit M and X
ldx #__init_array_start
.Linit_loop:
cpx #__init_array_end
bcs .Linit_done
; __jsl_indir does `JMP (__indirTarget)` — reads a 16-bit ptr
; from __indirTarget and JMPs there. So __indirTarget must
; hold the function pointer itself (NOT the address of the
; init_array slot). Dereference the entry: ($E0)→A.
stx 0xe0 ; entry addr -> DP scratch
ldy #0
lda (0xe0), y ; A = mem[X] (DP-indirect-Y, opcode 0xb1)
sta 0xb8 ; __indirTarget pinned at DP $B8 (bank-0 fixed; see libgcc.s)
phx ; preserve X across the call
jsl __jsl_indir
plx
; Step by 4 bytes (sizeof(void*) under p:32:16).
inx
inx
inx
inx
bra .Linit_loop
.Linit_done:
; Tool Locator init is the OS's job, not crt0's:
; - GS/OS Loader (crt0Gsos.s path) pre-inits TL before transferring
; control to __start. helloBeep proves this: it does `SysBeep`
; (JSL $E10000 dispatch) with zero init in user code and works.
; - GNO kernel (crt0Gno.s path) uses JSL $E10000 itself, so TL is
; up before __start runs.
; - Bare metal (this crt0.s): ROM pre-boot has already run
; TLBootInit/MMBootInit; libc.c's iigsToolboxInit() does a per-
; process TLStartUp + explicit SEI for clients of time()/clock().
; Do NOT inline TLStartUp here — it CLI's internally, and the ROM
; VBL handler runs in M=X=8 and would corrupt our in-progress
; M=16 register state (the most plausible root of the earlier
; attempt's "MAME segfault" mystery).
; Programs that use per-process tools (MM/QD/EM/WM) must call
; MMStartUp() and per-tool StartUps themselves — typically via
; iigs/desktop.h's startdesk(640) helper.
; Set DBR to the BSS bank before calling main. Without this, the
; user has to manually switch the data bank register (with a
; `pha;plb` inline-asm idiom) before doing absolute data
; accesses. Calypsi's "small data model" assumes DBR is
; pre-set; we now follow the same convention.
;
; Linker emits __bss_seg0_bank as the bank byte of the first
; BSS segment (typically 2 with the default --bss-base 0x020000).
; Programs that need bank-0 access (e.g., for $C0xx soft
; switches) can use STA_Long or temporarily switch DBR back.
sep #0x20
.a8 ; following lda #imm8 uses 1-byte immediate
lda #__bss_seg0_bank
pha
plb
rep #0x20
.a16 ; restore M=16 for the code that follows
; Call main. Standard W65816 ABI: i16 first arg in A; we pass
; nothing. After return, A holds the exit code.
jsl main
; Run global dtors registered via __cxa_atexit (LIFO order). No
; OS exit on bare metal — they run before we spin. A holds main()'s
; return; preserve it (harmless on bare metal but keeps the
; convention uniform across crt0 variants).
pha
jsl __run_cxa_atexit
pla
; main returned. Bare metal has no OS to return to, so spin.
; (Previously a BRK $00 here, but headless MAME mis-vectors BRK to
; $0000 and wild-jumps, which reads as a crash. A tight loop is the
; conventional bare-metal halt; IRQs are already masked above, so
; this just idles.)
.Lhalt:
bra .Lhalt
.size __start, . - __start