ZXF
The **Zuzu eXecutable Format (ZXF) is the native binary format for zuzuOS executables.
| Magic | 7A 5A 58 46 (zZXF) |
| Version | 0x01 |
| Header size | 64 bytes, fixed |
| Endianness | little |
| Since | zuzuOS v0.8 |
ZXF replaces ELF as the format the kernel loads. It carries only what a loader needs: an entry point, a segment table, and the segment bytes as well as an integrity checksum and a build identifier.
For netd, a representative service: 57,088 bytes as ELF, 38,221 bytes as
ZXF. The segment data is byte-identical between the two; the difference is
metadata the loader never touched. Format overhead is 112 bytes (a 64-byte
header plus two 24-byte segment entries) against roughly 19 KB of unused ELF
structure.
Who parses ZXF
| Component | Formats accepted |
|---|---|
kernel (KernelProcessLoad) |
ZXF only |
sysd (exec_inject) |
ZXF, ELF |
The kernel loads exactly two binaries: sysd and devmgr
which are all static and all produced by the zuzu build system. Using ZXF here allows
for a smaller parser on the kernel side as well as a smaller sysd and devmgr size.
File layout
offset 0 ZXF header 64 bytes, fixed
segment table seg_count * 24 bytes
block table block_count * 12 bytes (absent if block_count = 0)
block data variable
segment data raw bytes, packed
All offsets are absolute from the start of the file. Segment data is packed
back-to-back with no padding, so segments are copied into the target address
space by asinject, never mapped directly from the file, so file alignment
buys nothing.
Header
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | magic |
7A 5A 58 46 |
| 4 | 1 | version |
format version, currently 0x01 |
| 5 | 1 | arch |
target architecture (see below) |
| 6 | 1 | flags |
binary flags (see below) |
| 7 | 1 | reserved |
zero |
| 8 | 8 | entry |
entry point VA |
| 16 | 8 | load_base |
base VA of the image |
| 24 | 4 | image_size |
memory span when fully loaded |
| 28 | 2 | seg_count |
number of segment table entries |
| 30 | 2 | block_count |
number of block table entries |
| 32 | 4 | seg_table_offset |
file offset to the segment table |
| 36 | 4 | block_table_offset |
file offset to the block table (0 if block_count is 0) |
| 40 | 4 | checksum |
CRC32 over the whole file, this field treated as zero |
| 44 | 16 | build_id |
build identifier |
| 60 | 4 | reserved2 |
zero |
typedef struct {
uint8_t magic[4];
uint8_t version;
uint8_t arch;
uint8_t flags;
uint8_t reserved;
uint64_t entry;
uint64_t load_base;
uint32_t image_size;
uint16_t seg_count;
uint16_t block_count;
uint32_t seg_table_offset;
uint32_t block_table_offset;
uint32_t checksum;
uint8_t build_id[16];
uint8_t reserved2[4];
} ZXFHeader;
entry and load_base are 64-bit for cross-architecture use; on ARMv7-A the
upper four bytes are zero. load_base is the lowest vaddr across all segments. For a static
ET_EXEC-derived binary the segments carry absolute addresses. image_size is the span: max(vaddr + mem_size) − min(vaddr), including any gaps between segments. It is what a loader reserves before mapping individual segments, not the sum of segment sizes.
checksum is CRC32 over the entire file with bytes 40–43 treated as zero. A
loader verifies this before reading anything else; a mismatch is a corrupt
file and aborts the load.
build_id is the first 16 bytes of the SHA-256 of the whole file, computed
with both checksum (bytes 40–43) and build_id (bytes 44–59) zeroed. It is
content-derived rather than random, so builds are reproducible: identical
input produces an identical file, and the build system can tell that nothing
changed.
arch values
| Value | Architecture |
|---|---|
0x01 |
ARMv7-A |
0x02 |
ARMv8-A (AArch64) |
0x03 |
x86-64 |
0x04 |
ARMv7-M (tinyZuzu) |
0x05 |
RISC-V |
0x06 |
PowerPC |
A loader compares this against the running system and aborts on mismatch.
Only 0x01 is produced or accepted today.
flags bits
| Bit | Name | Meaning |
|---|---|---|
| 0 | PIC |
position-independent; load_base is ignored |
| 1 | STATIC |
no dynamic linking; no DYNLINK block will be present |
| 2 | HAS_DEBUG |
a DEBUGINFO block is present |
| 3 | COMPRESSED |
segment data is stored compressed |
| 4–7 | — | reserved, must be zero |
Warning: elf2zxf sets STATIC and nothing else. PIC, HAS_DEBUG, and COMPRESSED are reserved for future use and are not produced or honoured today.
Segment table
Each entry is 24 bytes. Every entry is a load segment; there is no type field and nothing to filter.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | file_offset |
offset from start of file to this segment’s data |
| 4 | 4 | file_size |
bytes stored in the file |
| 8 | 8 | vaddr |
virtual address to map at |
| 16 | 4 | mem_size |
bytes occupied in memory; >= file_size |
| 20 | 1 | flags |
R = bit 0, W = bit 1, X = bit 2 |
| 21 | 1 | align |
alignment as log2 (12 = 4096-byte pages) |
| 22 | 2 | reserved |
zero |
typedef struct {
uint32_t file_offset;
uint32_t file_size;
uint64_t vaddr;
uint32_t mem_size;
uint8_t flags;
uint8_t align;
uint16_t reserved;
} ZXFSegment;
ZXF uses R = 1, W = 2, X = 4; ELF’s PF_R/PF_W/PF_X are 4/2/1. Converters must remap rather than copy.
Where mem_size > file_size, the loader zero-fills the tail. Initialised data and BSS share one R|W segment:
file_size bytes read from the file, followed by mem_size − file_size bytes of zeros that occupy no file space.
Block table
Each entry is 12 bytes. Blocks carry optional metadata that is not required to load and run the binary.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 2 | type |
block type identifier |
| 2 | 2 | flags |
REQUIRED = 0x0001, OPTIONAL = 0x0000 |
| 4 | 4 | offset |
file offset to the block’s data |
| 8 | 4 | size |
size of the block data in bytes |
A loader that encounters a REQUIRED block whose type it does not recognise
must abort. An unrecognised OPTIONAL block is skipped silently. New block
types can therefore be introduced without breaking existing loaders, provided
they are marked OPTIONAL.
Block type registry
| Value | Name | Consumer | Status |
|---|---|---|---|
0x0001 |
DYNLINK |
loader | reserved, not implemented |
0x0002 |
HINTS |
sysd |
reserved, not implemented |
0x0003 |
DEBUGINFO |
debugger | reserved, not implemented |
0x0004 |
SIGNATURE |
devmgr |
reserved, not implemented |
No block types are produced or consumed today. elf2zxf emits
block_count = 0 and omits the block table entirely.
Loading sequence
- Verify
magic,version, andarch. - Recompute CRC32 over the file with bytes 40–43 zeroed; compare against
checksum. Abort on mismatch. - Read
seg_countentries atseg_table_offset. - For each segment: copy
file_sizebytes fromfile_offsettovaddrwith the given permissions, then zero-fillmem_size − file_sizebytes past the end. - Walk the block table if
block_countis non-zero; abort on an unrecognisedREQUIREDblock, skip unrecognisedOPTIONALones. - Set up the stack and start at
entry.
elf2zxf
scripts/elf2zxf.py converts a linked ELF into ZXF on the build machine. It
requires pyelftools.
elf2zxf.py <input.elf> [-o output.zxf]
Input must be ET_EXEC, EM_ARM, 32-bit, little-endian. The conversion is a one-to-one copy of PT_LOAD program headers into segment table entries. Everything outside PT_LOAD is discarded.
Field derivation:
| ZXF field | Source |
|---|---|
entry |
e_entry |
load_base |
min(p_vaddr) over PT_LOAD |
image_size |
max(p_vaddr + p_memsz) − min(p_vaddr) |
seg_count |
number of PT_LOAD headers |
flags |
STATIC |
block_count |
0 |
The two computed fields must be written in order, since each covers the previous:
- Assemble the file with
checksumandbuild_idboth zeroed. - Compute SHA-256 over the buffer; write the first 16 bytes to
build_id. - Compute CRC32 over the buffer,
checksumstill zeroed; write it tochecksum.
The CRC therefore covers the build_id; the build_id does not cover the
CRC.