65816-llvm-mos/runtime/src/crt0.s
Scott Duensing 07544f49f2 Checkpoint
2026-05-02 16:48:56 -05:00

109 lines
4 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 1 RAM at $D000-$DFFF for read+write.
; By default the IIgs maps that range to ROM (read-only). Two
; reads of $C083 enable RAM-bank-1, second read also enables
; writes. Without this, BSS auto-relocated past $C000 lands on
; ROM and globals never initialise (writes drop on the floor;
; reads return ROM bytes). Caught by the expression-parser
; smoke test (#92) when runtime growth pushed bss past $BFFF.
; 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 we just set.
sep #0x20
lda 0xc083
lda 0xc083
rep #0x20
; Zero BSS. X iterates from __bss_start to __bss_end; each
; iteration writes one byte of zero at addr X (via DP=0 +
; offset 0 — which is just X). STZ in M=8 stores 1 byte and
; doesn't touch A, so we don't need the LDA #0 prelude.
rep #0x10 ; ensure X is 16-bit
ldx #__bss_start
.Lbss_loop:
cpx #__bss_end
bcs .Lbss_done ; X >= end -> done
sep #0x20 ; 8-bit M for 1-byte store
stz 0x0, x ; *(uint8_t *)X = 0 (DP=0)
rep #0x20
inx
bra .Lbss_loop
.Lbss_done:
; Run static constructors. The linker emits
; __init_array_start / __init_array_end around the .init_array
; section; each entry is a 16-bit function pointer. Walk and
; JSL each via __jsl_indir.
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 __indirTarget ; __indirTarget = function pointer
phx ; preserve X across the call
jsl __jsl_indir
plx
inx
inx
bra .Linit_loop
.Linit_done:
; Call main. Standard W65816 ABI: i16 first arg in A; we pass
; nothing. After return, A holds the exit code.
jsl main
; Halt via BRK $00. MAME / debuggers catch this as a clean
; program termination.
.byte 0x00, 0x00
.size __start, . - __start