Linker scripts (.lnk)
A linker script (.lnk) is a UTF-8 text file that describes a complete target layout: zero-page reservations, address spaces, banking mechanism (if any), shadow-mode configuration, stack and heap placement, and the startup hook. Each shipped memory model (xl, xe, xt, rambo*, compy*, …) is a .lnk file under support/<platform>/layouts/.
xtc -m ./my-layout.lnk app.xt -o app.xex # use a custom layoutxtc -m xe-shadow app.xt -o app.xex # use a shipped layout by namextc --dump-layout -m xe # print a layout's diagramCustom hardware (cartridge slots, non-stock RAM expansions, smaller / larger screen areas) can be supported without touching the compiler — write a .lnk and pass it.
Format overview
Section titled “Format overview”INI-style sections, key = value entries, # comments to end of line.
# A trimmed example[zp]sp = $82-$83 # stack pointer (2-byte pair)tmp = $84-$85hp = $86-$87vars = $88-$AF, $BC-$FFruntime = $B0-$BB
[memory]main = $2000-$9FFF
[stack]range = after-code
[heap]range = before-screenValue syntax
Section titled “Value syntax”| Form | Meaning |
|---|---|
$XXXX | hex address (16-bit) |
1234, 42 | decimal |
$XXXX-$YYYY | inclusive address range |
$XXXX:$MM | hardware register at $XXXX with bitmask $MM |
after-code, after-system, before-screen | symbolic — linker resolves after layout |
key1 = a, b, c | comma-separated list |
true / false | booleans |
Disambiguation rule: - always means “address range”; : always means “bitmask on a hardware register”; bitmask values are always hex-prefixed ($XX). There is no “address:size” notation — use a range instead. Even a single 2-byte ZP pair is written as a range ($82-$83).
Sections
Section titled “Sections”[zp] — zero-page layout
Section titled “[zp] — zero-page layout”Names the ZP slots the codegen and runtime depend on. Every entry is a range, even for a 2-byte slot.
[zp]sp = $82-$83 # stack pointer (2 bytes)tmp = $84-$85 # scratch pairhp = $86-$87 # heap pointer (2 bytes)vars = $88-$AF, $BC-$FF # allocatable user-var region(s)runtime = $B0-$BB # reserved for runtime paramsNamed entries (sp, tmp, hp, runtime) are fixed reservations the codegen references by name. vars declares allocatable runs the ZP allocator draws from.
On banked targets, additional entries appear:
bankReg = $82-$83 # bank-select register pair (xt code-bank selector + region B)[memory] — address-space regions
Section titled “[memory] — address-space regions”Named regions as comma-separated address ranges. The linker packs code and data into the main region first-fit across all listed runs.
[memory]main = $2000-$9FFFscreen = $8000-$9FFFMulti-run main regions (shadow mode):
[memory]main = $A000-$BFFF, $C000-$CFFF, $D800-$FFF9system = $2000-$3FFFscreen = $8000-$9FFFThe linker treats each comma-separated range as an independent run: functions are packed within a single run and never straddle a gap. Overflow spills to banked pages if banking is configured, or surfaces as a diagnostic error if not.
[banking] — bank-switched memory
Section titled “[banking] — bank-switched memory”Describes the hardware banking mechanism: which register(s) control bank selection, which address range is the bank window, how large each page is. The codegen derives the bank-select instruction sequence from the descriptors.
# xt — 16-bit ZP pair (implicit :$FF on each byte)[banking]window = $4000-$5FFFpageSize = $2000register = $82-$83 # 16-bit selector pair, full byte each
# xe (128 KB, 4 banks): bits 2-3 of PORTB[banking]window = $4000-$7FFFpageSize = $4000register = $D301:$0C # PORTB, mask = bits 2-3
# rambo256 (8 banks): bits 2-3, 6 of PORTB[banking]window = $4000-$7FFFpageSize = $4000register = $D301:$4C # PORTB, mask = bits 2-3 + bit 6Bit-mask layouts that need a non-contiguous set of bits use the union of bits (e.g. $4C = $0C | $40 for bits 2-3 plus bit 6). The codegen generates the deposit pattern from the mask — you specify the bits, the compiler does the bit twiddling.
[shadow] — ROM-disable configuration
Section titled “[shadow] — ROM-disable configuration”Describes how to flip the OS ROM out and back in for shadow targets:
[shadow]register = $D301:$01 # PORTB bit 0 controls OS ROMcharsetCopy = $E000 # source address of the charset to copyThe :needsOS function annotation drives the runtime save / disable / call / restore sequence around any function flagged with it.
[cloaked] — code regions for :cloaked decls
Section titled “[cloaked] — code regions for :cloaked decls”Each [cloaked] block declares one region of the bank window where source-level :cloaked code can live. A layout may have several [cloaked] blocks; together they form a pool that auto-cloak’s overflow ladder spills through.
# Banking-off region — code lives in main RAM at $4000-$7FFF and the# call site sets PORTB to expose it.[cloaked]range = $4000-$7FFFbank = noneid = lib
# Numbered-bank region — code loads into bank 2's image of the# window; the call site sets PORTB to select bank 2.[cloaked]range = $4000-$7FFFbank = 2id = ext1The range field is the address span the region occupies inside the bank window. bank is none for a banking-off region (PORTB bit 4 = 1) or a decimal bank index for a numbered region; codegen emits the matching PORTB bracket per region. id is the user-facing name used by source-level annotations (:cloaked(<id>)); plain :cloaked (no id) auto-packs into the first declared region.
Pool form: bank = N-M + id = <prefix><n>
Section titled “Pool form: bank = N-M + id = <prefix><n>”Layouts with many available banks can declare a pool in one block:
[cloaked]range = $4000-$7FFFbank = 4-12id = ext<n>The <n> placeholder is mandatory in the id template — it’s substituted with each bank’s index, so the example above expands to nine concrete regions: ext4, ext5, …, ext12. Each becomes selectable from source as :cloaked(ext7) etc, and the auto-spill ladder walks them in declaration order on overflow.
Validation rejects:
- a range form without
<n>in the id (the expansion would yield duplicate ids), - a single-bank form with
<n>in the id (the placeholder has nothing to bind to), - duplicate ids across all
[cloaked]blocks in the file (a:cloaked(<id>)annotation would be ambiguous).
Each region’s bank is also reserved with the page tracker so :banked decls aren’t packed on top of cloaked code. Empty regions cost nothing — codegen + xta both skip zero-byte buffers — so a layout can over-declare its pool without runtime overhead. The shipped rambo* and compy* layouts use the pool form; on rambo1088 it expands to 62 regions × 16 KB = 992 KB of cloaked-code surface.
[cloaked] is xe-family-only: layouts without PORTB-style banking (xl, xt) reject the section and any source-level :cloaked annotation on non-xe targets is a compile error.
[stack] — xtc software stack
Section titled “[stack] — xtc software stack”[stack]range = after-code # symbolic — the linker places the stack right after the code/data blocksize = $0100 # optional explicit cap (otherwise grows toward the heap)[heap] — heap region (optional)
Section titled “[heap] — heap region (optional)”A layout that declares [heap] makes the coalescing free-list allocator available; one that omits the section forces -falloc=bump.
[heap]range = before-screen # symbolic — heap grows down from just below screen RAMMulti-bank heap configurations (xt, the rambo* family) reserve specific banks for the heap and the runtime selects the appropriate bank on each allocator call.
[entry] — entry point address
Section titled “[entry] — entry point address”[entry]address = $2000 # where main() lands; sets the XEX RUNAD[startup] — startup template
Section titled “[startup] — startup template”[startup]template = startup/xl.asm # hand-written .asm template under support/<platform>/startup/The template is a 6502 assembly file with placeholder substitutions (sentinels the linker fills in with addresses derived from the rest of the layout). Banked targets ship startup that loads each bank page directly into the bank window at boot via INITAD preload stubs.
[output] — binary format
Section titled “[output] — binary format”[output]format = atari-xex # or commodore-prgUsed when the user passes -o file without an extension; otherwise the extension wins.
Memory-map diagram (comment header)
Section titled “Memory-map diagram (comment header)”By convention, every shipped .lnk opens with a Unicode box-art comment showing the actual memory layout the file describes:
# xl.lnk — flat Atari 8-bit (no banking, no shadow)## ┌──────────────────────────────────────────────┐# │ $0000-$0081 OS/hardware zero page │# │ $0082-$00BB xtc ZP: SP, tmp, HP, vars │# │ $00B0-$00BB runtime params (reserved) │# │ $00BC-$00FF xtc ZP: vars (continued) │# ├──────────────────────────────────────────────┤# │ $2000-$9FFF Main region (32 KB) │# │ $2000 Code + data (entry point here) │# │ Stack ↑ (grows up after code) │# │ (free RAM) │# │ $9FFF Heap ↓ (grows down from here) │# ├──────────────────────────────────────────────┤# │ $A000-$BFFF Unused / OS area │# │ $C000-$CFFF OS self-test ROM │# │ $D000-$D7FF Hardware I/O │# │ $D800-$FFFF OS ROM │# └──────────────────────────────────────────────┘The diagram is for the human reader — the linker doesn’t parse it. xtc --dump-layout -m <name> reproduces the same diagram from the parsed config, which is how you confirm a custom file describes what you think it does.
Built-in models
Section titled “Built-in models”xtc -llPrints every shipped layout grouped by platform. Each one is a .lnk file you can copy as a starting point for a custom variant.
The shipped Atari layouts cover:
- Flat:
xl,xl-shadow - xe family:
xe,xe-shadow,rambo192,rambo256,rambo320,rambo576,rambo1088,compy320,compy576 - xt family:
xt,xt,xt
For Commodore: c64.
See Memory models for what each one is for and when to pick it.
Writing your own
Section titled “Writing your own”The fastest path is copy and modify:
- Run
xtc --dump-layout -m <closest match>to see the layout you’re starting from. - Copy
support/<platform>/layouts/<closest>.lnkto a new file. - Edit the sections you need to change — typically
[memory],[banking](if your hardware has different bank-bit pattern), and the comment-header diagram. - Run
xtc --dump-layout -m ./your-file.lnkand verify the printed diagram matches your intent. - Compile with
-m ./your-file.lnk. If the file lives next to the shipped layouts undersupport/<platform>/layouts/, you can reference it by name without the path.
If you write a layout for a piece of hardware that’s likely to be useful to others (a memory expansion, a cartridge form factor, a unique screen-RAM placement), upstream it — every shipped .lnk was a custom layout once.
CLI surface
Section titled “CLI surface”| Flag | Purpose |
|---|---|
-m <layout> | Activate a layout. Searches <layout> as a path (appends .lnk if needed), then support/layouts/<layout>.lnk, then support/<platform>/layouts/<layout>.lnk. |
-ll, --list-layouts | List every built-in layout. |
--dump-layout | Print the active layout’s parsed diagram and exit. |
-H <path> / XTC_HOME=... | Point the search at a specific xtc home (so a custom support/ tree is preferred over the system one). |