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

167 lines
9.0 KiB
Markdown

# 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 + <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)
```rust
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 `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).