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
valueis its offset within TEXT (i.e. assumes TEXT is loaded starting at runtime address 0). - A DATA-section symbol's
valueisdata_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:
- Parse the header and extract
dataandtextas separate in-memory byte/word buffers (viaDseExecutable::load/read). - Copy
textinto memory starting at whatever base address the loader chooses (text_base, conventionally0for the current single-program, single-load-address model). - Copy
datainto memory immediately following, attext_base + text.len() * 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*_offsetfield 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 attext_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 aDseExecutable— there is no independent per-file compilation or linking step yet. The plannedDsoBinaryformat (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.DseExecutableis 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.
flagsfield is currently unused — reserved for future use (e.g. marking an executable as requiring a specific kernel/loader feature).