Files
DSA/doc/reference/dse-executable-spec.md
T

9.0 KiB

DSE Executable Format Specification (v1)

Status: Implemented — matches DseHeader/SymbolEntry/DseExecutable as built in the assembler crate's format module. Scope: Single-module, fully-linked executables produced by assembling one .dsa program (with its includes already concatenated in). Does not yet cover the planned DsoBinary (relocatableobject/.o-equivalent) format or a real multi-unit linker — see §7.


1. Design Goals

  • Minimal, purpose-built for DSA — not an ELF-compatible or ELF-inspired-to-the-letter format; borrows only ELF's useful idea of offset+size pairs in the header rather than assuming fixed contiguous section layout.
  • Separate DATA and TEXT payloads, addressed independently — no jump-over-data hack required to skip past a data blob embedded in the instruction stream.
  • Symbol table sufficient for post-hoc disassembly/debugging (name, resolved address, section, binding), even though the current single-file assembler doesn't need cross-file linking yet.

2. File Layout

All multi-byte fields are little-endian. On-disk section order is:

[ Header ] [ Symbol Table ] [ String Table ] [ Data ] [ Text ]

This on-disk order is not the same as the runtime memory layout — see §5. Do not assume file byte offsets correspond to runtime addresses.

2.1 Header

Magic bytes "DSAX" (4 bytes) precede the struct fields below. Total header size on disk is 48 bytes (4-byte magic + 44 bytes of fields).

Field Type Meaning
version u16 Format version. Currently 1.
flags u16 Reserved, currently 0.
entry_point u32 Absolute runtime address of the program's entry point, assuming TEXT is loaded at address 0 (see §5).
symbol_table_offset u32 Byte offset from start of file to the first SymbolEntry.
symbol_table_size u32 Count of symbol entries (not byte size).
string_table_offset u32 Byte offset to the string table.
string_table_size u32 Byte length of the string table.
data_offset u32 Byte offset to the DATA section.
data_size u32 Byte length of the DATA section.
text_offset u32 Byte offset to the TEXT section.
text_size u32 Count of 32-bit words in TEXT (not byte size).

2.2 Symbol Table

An array of symbol_table_size fixed-size entries, each 12 bytes:

Field Type Meaning
name_offset u32 Byte offset into the string table where this symbol's name begins.
value u32 Resolved runtime address (see §5 for the base-address convention this assumes).
section u8 0 = absolute, 1 = data, 2 = text, any other value = undefined/unknown.
binding u8 0 = local, 1 = global, 2 = weak.
_pad u16 Reserved, currently unused; always 0.

2.3 String Table

Raw bytes: a concatenation of symbol names, each null-terminated, referenced by name_offset into this buffer. No length prefix — read to the next 0x00 byte from the given offset.

2.4 Data Section

Raw bytes — whatever db/dh/dw/resb/resh/resw directives produced, concatenated in program order. Not guaranteed to be internally word-aligned at every sub-item boundary — a string literal can leave the next data item starting at an arbitrary byte offset within the section. Consumers that walk this section looking for structure (disassemblers, memory viewers) must scan byte-by-byte, not assume 4-byte-aligned entries throughout.

2.5 Text Section

text_size raw 32-bit words, each a fully-encoded Instruction per the ISA spec. No padding between words; word i begins at text_offset + 4*i on disk.


3. Symbol Resolution Convention

Symbol addresses (SymbolEntry.value, and the header's entry_point) are computed under a fixed runtime-layout assumption, not relative to anything about the file's on-disk layout:

data_base = text_len_words * 4     // where text_len_words = TEXT section's word count
  • A TEXT-section symbol's value is its offset within TEXT (i.e. assumes TEXT is loaded starting at runtime address 0).
  • A DATA-section symbol's value is data_base + <offset within DATA>.

This is a v1 stopgap, not a general relocation scheme — see §7.


4. On-Disk Offset Computation (Writer Responsibilities)

When constructing a DseExecutable, offsets must be derived from the actual serialized size of each preceding section — not hand-counted constants. The header magic (4 bytes) and each SymbolEntry's true packed size (12 bytes, not a naively-summed field-width guess) have both been sources of real, silent file corruption during development, each shifting every subsequent offset by a fixed amount and making downstream sections unreadable at their claimed positions. Compute:

symbol_table_offset = header_size                              // 48
string_table_offset = symbol_table_offset + (symbol_count * 12)
data_offset          = string_table_offset + string_table.len()
text_offset          = data_offset + data.len()

A unit test that round-trips a DseExecutable through write() then read() and asserts structural equality is the recommended guard against this class of bug recurring — this is far more reliable than re-deriving constants by hand whenever a struct's fields change.


5. Loading Into Runtime Memory

The on-disk file layout and the runtime memory image are different things. Loading a .dse file means:

  1. Parse the header and extract data and text as separate in-memory byte/word buffers (via DseExecutable::load/read).
  2. Copy text into memory starting at whatever base address the loader chooses (text_base, conventionally 0 for the current single-program, single-load-address model).
  3. Copy data into memory immediately following, at text_base + text.len() * 4.
  4. Set the program counter to text_base + header.entry_point.

Important: copying the raw file bytes wholesale into memory and treating a file offset (e.g. header.text_offset) as if it were a runtime address is incorrect — the header/symtab/strtab/data sections sitting ahead of TEXT in the file have no relationship to what sits ahead of TEXT in the runtime memory image the assembler assumed (which is nothing — TEXT is assumed to start at address 0). The loader must unpack data/text into the layout described above, not memory-map the file as-is.

If text_base != 0, every address already baked into instruction words and the symbol table (computed under the text_base == 0 assumption at assembly time) will be wrong at runtime, since nothing currently patches them for a different load address. Until relocations exist (§7), load at text_base = 0.


6. Reading and Writing (Reference Implementation Shape)

struct DseExecutable {
    header: DseHeader,
    symbols: Vec<SymbolEntry>,
    string_table: Vec<u8>,
    data: Vec<u8>,
    text: Vec<u32>,
}
  • DseExecutable::load(path) / ::read(reader) — parses the header, then seeks to each *_offset field in turn to read the symbol table, string table, data, and text sections. Sections are addressed by absolute offset, not read sequentially by assumed contiguous layout, so this must seek rather than just read-through.
  • DseExecutable::save(path) / ::write(writer) — writes header, then symtab, then strtab, then data, then text, in that fixed order, with offsets computed per §4.
  • DseExecutable::to_memory_image() — produces the runtime image described in §5 (TEXT bytes followed immediately by DATA bytes), ready to be copied into emulator memory at text_base.

7. Known Limitations / Planned Extensions

  • No relocations. All addresses assume a single fixed load address (TEXT at 0). Loading at any other base, or supporting a multi-program/OS environment with per-process load addresses, requires adding a relocation table — additive to this format (new optional header fields, 0/absent in existing files) rather than a breaking change.
  • Single compilation unit only. The current assembler concatenates all included files into one node list before producing a DseExecutable — there is no independent per-file compilation or linking step yet. The planned DsoBinary format (a .o-equivalent, carrying local symbols + unresolved imports + a relocation table per compiled unit) plus a dedicated linker step will decouple "compile a module" from "produce a runnable program," enabling per-file caching and parallel builds. DseExecutable is intended to remain the final, fully-linked output format even after that change — only the path that produces it will change.
  • No debug info section. Symbol names/addresses are present, but there's no line-number or source-mapping table. Worth considering if source-level debugging in the egui debugger becomes a priority.
  • flags field is currently unused — reserved for future use (e.g. marking an executable as requiring a specific kernel/loader feature).