Building in Pieces

Assembling one part of a program on its own, for something else to join to the rest later. -f obj writes a relocatable object: the bytes, plus enough description of them that a later step can decide where they go.

fantasm build kernel.asm kernel.o -f obj
fantasm link kernel.o main.o libgfx.fzlib -o game.bin -e start

The passes run in a fixed order and never iterate: resolve, collect, place, classify, relocate, package. Collection never depends on placement, reachability being a property of the reference graph, so no pass has to be run twice to agree with itself.

Separate Compilation

Every other format is the whole program. That means the whole program has to be assembled at once, every name has to resolve in that one assembly, and a change to one file re-assembles the lot.

Separate compilation is the other shape: each part is assembled on its own into an object, and a linker joins them. It buys three things.

Names that resolve later. A routine can call something another object defines, declared with extern, and the call is settled when the parts are joined.

Placement decided once, at the end. An object says what it needs — this much space, this alignment, in a page, at a fixed address — and whoever links it places every part together. Nothing has to agree an address map in advance.

Removal at the granularity of a function. --gc-libraries removes a file and --gc-modules removes a module body; a linker removes a section, and there is nothing stopping one section per routine.

It is also what a C compiler needs. One object per source file is how every C toolchain works, so this is the piece FantASM has to have before one can target it.

Syntax: fantasm link <object>… -o <file> [-e <name>] [<option>…]

fantasm link joins objects and archives into a loadable image. Each object is a .o written by -f obj or a .fzlib archive holding them.

OptionMeans
-o <file>Where to write the image
-f <format>bin, hex, sna or nex. The output’s extension decides where nothing says
-e, --entry <name>The name the program starts at, and the root collection walks from
--gc-sectionsRemove sections nothing reaches
--report-discards <file>Write what the sweep removed here
--sld <file>Write the linked image’s debug records here, as SLD
fantasm link kernel.o main.o libgfx.fzlib -o game.bin -e start

--gc-sections with no -e keeps only what is pinned. The entry point is a root, and a link that names none has nothing else to start from.

A section pinned with page= is not in a flat image: its bytes belong in a bank, which only a banked format has anywhere to put.

Packaging goes through the writer that format already has. A format writer is handed values and asks nothing about how the image was produced, so every format FantASM gains afterwards is gained by both paths at once.

The debug output is a merge rather than a copy. Every object carries its source positions and its annotations, whether or not --sld was named when it was assembled, and the link writes one file naming each line in whichever source it came from, at the address and in the page the link chose.

A label in a section the program maps for itself is left out of the SLD unless the section names a window. It has a page and an offset and no address, and an SLD record is an address.

An Object

    !format obj
    GLOBAL  print
    EXTERN  putchar

    SECTION code
print:
    ld      a,(hl)
    or      a
    ret     z
    push    hl
    call    putchar
    pop     hl
    inc     hl
    jr      print

    SECTION data, write
count:
    dw      0

    SECTION bss, write, zero
buffer:
    rs      256

fantasm dump kernel.o on what comes out:

sections
  0   code                 11 bytes  align 1    Anywhere  slot any  [code]
  1   data                  2 bytes  align 1    Anywhere  slot any  [write]
  2   bss                 256 bytes  align 1    Anywhere  slot any  [write zero]

symbols
                                    0  section   local   section        0
                                    0  section   local   section        1
                                    0  section   local   section        2
  buffer                            0  location  local   section        2
  count                             0  location  local   section        1
  print                             0  location  global  section        0
  putchar                           0  undefined global  section        -

relocations
  section 0 + 0x0005  abs16  putchar

Three sections, each starting at zero. print is global because global named it and putchar is undefined because extern did. The call putchar at offset 5 is the one thing that cannot be settled here, so it is the one relocation.

The jr print is not a relocation, and that is correct: both ends of it are in code and move together, so the displacement is already right. Patching it would break a jump that works.

No Origin

Every section starts at zero and whoever links the object chooses its base. So an operand naming an address holds an offset, and a relocation says which symbol settles it.

A symbol’s value is read against the section it names, and only that one. Two sections both hold address 4.

An org inside a section is E1155, being two answers to one question. A bank or a page inside one is E1158, for the same reason: the section already says which page it wants.

org reaches nothing in an object. The same source assembled at org $8000 and at org $C000 gives byte-identical objects. Where something must be at a fixed address, say so on the section — section vectors, at=$0038 — which is the same statement made where a linker can honour it.

section

Syntax: section <name> [, <option>…]

section opens a run of lines a linker places as one. name is an identifier; each option is a flag or a name=value pair from the tables below. It runs until the next section or the end of the file.

There is no closing directive, a section not nesting. A closer would invite nesting, and a line belonging to two things that each decide where it goes has no answer to which wins. A source that never says section still writes an object — one section covering the whole program, which is what every source written before sections existed becomes. Lines written before the first section get a section of their own, called .text. A module inside a section is fine and defers to the end of that section, which is the rule it already follows one level up; a section inside a module is E1154, both moving a body and only one of them being able to decide.

    SECTION code
print:
    ret

    SECTION data, write
count:
    dw      0

Section Options

Written after the name, comma-separated. A flag goes on its own; the rest take a value.

FlagMeans
codeExecutable. The default when none of code, write or zero is given
writeWritable at run time
zeroOccupies addresses and carries no bytes — a bss
straddleMay cross from one slot into the next
gc_rootThe linker’s collection starts here
mergeIdentical copies from several objects may be folded into one
page_alignAlign to the machine’s page size
lastThe final bytes of the image
OptionMeans
align=<n>Align to n bytes, rounded up to a power of two
page=<n>Place it in page n, the linker choosing the offset
at=<addr>Place it at this address, for a vector or something hardware pins
window=<slot>The slot its page must be mapped through

window= is what a kernel routine needs when it has to answer at a particular address:

    SECTION kernel, page=5, window=0

It is window= and not slot=. slot is an alias of the page directive, so the tokeniser hands both spellings back as the same thing and slot=0 could not be told apart from page=0. A slot is an address window, so the name says what it constrains. This is the same class of trap as the reserved words in Diagnostics.

A pair that contradicts is E1153, and the message names the pair:

PairWhy not
zero with mergeMerging compares contents and a zero section has none — two of a size would merge on being equally absent
page= with at=Each places it somewhere else
at= with window=An address already names the slot it sits in
window= with straddlePinning one slot is the statement that only one of them is mapped adjacently

Section Size

A section’s size is what it spans. One holding an rs occupies more than it writes, which is the whole of what rs is for: the bss above is 256 bytes and carries none of them.

Calls Across Pages

A CALL into a page that may not be mapped needs paging around it: map the target in, call, put the mapping back. Writing that is yours, and the linker leaves such a call exactly as it was written.

That is deliberate. Which register holds the page, whether the mapping is put back, whether an interrupt may fire between the two — none of that is the same on two machines, and a sequence chosen here would be wrong on one of them while looking in the source exactly like an ordinary call. An assembler that inserted it would be deciding how far calls work on your behalf.

The one refusal is E1179: the target is pinned with page= and names no window=, so it is stored in a bank and appears at no address at all. No mapping the caller could perform reaches it, which makes it a layout fault rather than a paging one — give the section a window=, or place it in the address space. A target that does name a window links clean, and whether the program has that page mapped when the call runs is the program’s to arrange.

extern

Syntax: extern <name>

extern declares a name this assembly references and another object defines. name is an identifier. It is the other half of global, which offers a name out.

A reference to an extern name assembles to zero and the object carries the name for a linker to settle. The declaration defines nothing, so it may sit above or below the reference. The same name without extern is still E1003, declaring it being the whole of what says this one is somebody else’s — without that, a typo would stop being a message at your desk and become a link-time puzzle. An extern outside an object build is E1156: a loadable image is finished when it is written, so there is no later step to settle the name in, and one there would leave a call to address zero and nothing to say so.

    extern  putchar
    call    putchar         ; CD 00 00, and the object names `putchar`

Refusals

Each is an error rather than a warning, and for one reason: the alternative is an object that links, loads, and is wrong on somebody else’s machine.

--gc-modules, --gc-librariesE1159
org inside a sectionE1155
bank or page inside a sectionE1158
section inside a moduleE1154
!nex or !snaE1163
!testE1164

A header describes an image and an object is one fragment of one. Two units may both name a pc, and whichever one said it is not a rule anyone can predict. [target.nex] and [target.sna] are where they belong — see Building Several Images.

A !test block needs a program too. It runs its body on the simulator and checks what the registers hold afterwards, which takes a placed, resolved image — and every section of an object starts at zero with its imports still zeroes. Assemble the tests in the build that produces the image; a library’s tests are already a separate assembly, per Testing. Running a unit’s tests against the linked image is the version worth having, where its references actually point, and it does not exist yet.

Collection is refused because an assembler looking at one object cannot answer what the program reaches. Every reference into it may arrive from a unit that has not been assembled yet. The linker collects instead, per section, once everything is present.

keep keeps its meaning and changes who acts on it. In a direct build it pins a body against the sweep; in an object it is written as a collection root and the linker honours it. One spelling, one intention.

Relocation

--reloc describes one image moved by one delta. An object describes sections moved apart, and two operand shapes follow from the difference.

db label is a whole address in one byte. A section placed low enough for it to fit is the linker’s to arrange, so an object records it and reports at link time where the value does not fit. --reloc has no 8-bit field for a 16-bit delta, so the same line is E1092 there.

jr label crossing a section boundary depends on two bases. One inside a section is not recorded, both ends moving together.

_page(label), _bank(label), _slot(label) and _window(label) ask where a name sits, which the page layout decides and the linker chooses. Each becomes its own kind, and under --reloc all four are E1092 — see Memory and Banking.

_window(label) is not an ordinary address. Its value depends on which slot the linker assigned the page to, so it cannot be folded into an abs16 even for a name this object defines. Doing so produces banked code that works until the layout moves.

Everything else is classified as it is for --reloc, and an address in a shape no kind describes is E1092 either way — including arithmetic around a placement, which no kind describes.

fantasm ar

Syntax: fantasm ar <archive> <object>… | --list | -x

fantasm ar writes an archive of objects, so a library is one file rather than a directory of them. archive is the .fzlib to write or read; each object is a .o to embed.

fantasm ar libgfx.fzlib draw.o sprite.o blit.o   # create
fantasm ar libgfx.fzlib --list                   # what it holds
fantasm ar libgfx.fzlib -x                       # take every member out
fantasm dump libgfx.fzlib                        # members and the names they export

Naming objects creates the archive, replacing whatever was there. An archive is rebuilt from its inputs as an image is, so there is no add-to-existing to get wrong. Each member is embedded verbatim, so extracting one gives back the bytes that went in — the records come first and the members after them, which is what makes an extraction a copy.

It carries an index of every name its members export: globals only, not their local symbols and not the names they import. Pulling a member in to satisfy a name nothing outside it can reach is code nobody asked for. An archive whose index is absent is read the slow way rather than refused.

--list is long only. ar spells it -t, which this tool reads out of the raw arguments as the withdrawn spelling of --device, so a short there warns about a flag nobody wrote.

fantasm link takes an archive and pulls in the members something wants, which is what an archive is for.

One Object Per Assembly

An assembly is what a [[target]] describes, so one object per target. include splices files into one assembly exactly as it always did — it is not a way to make two objects.

Two targets over one tree produce two objects, and should: a target fixes the defines and the machine, and an object records the machine it was built for. Two objects link only if they agree on that. One built for zx48 and one for zxnext describe different address spaces, and joining them would place code in pages the machine does not have.

Further Reading

Documentation