# 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 `include`s 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 + `. 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) ```rust struct DseExecutable { header: DseHeader, symbols: Vec, string_table: Vec, data: Vec, text: Vec, } ``` - `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 `include`d 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).