rewrote assembler backend to support custom executable format (DSE) and also refactored (moving crates around)

This commit is contained in:
2026-07-16 00:34:13 +01:00
parent 7a84073bc4
commit a4d42bdad6
60 changed files with 1486 additions and 1669 deletions
+1
View File
@@ -7,4 +7,5 @@ edition = "2024"
path = "src/lib.rs"
[dependencies]
binrw = "0.15.1"
strum = { version = "0.28.0", features = ["derive"] }
+264
View File
@@ -0,0 +1,264 @@
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
use binrw::{binrw, BinRead, binwrite, NullString, BinReaderExt, BinWriterExt, BinWrite};
use std::fmt;
use crate::prelude::Instruction;
#[binrw]
#[brw(magic = b"DSAX", little)]
#[derive(Debug, Clone)]
pub struct DseHeader {
version: u16,
flags: u16,
pub entry_point: u32,
symbol_table_offset: u32,
symbol_table_size: u32,
string_table_offset: u32,
string_table_size: u32,
data_offset: u32,
data_size: u32,
pub text_offset: u32,
text_size: u32
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone)]
pub struct SymbolEntry {
pub name_offset: u32,
pub value: u32,
pub section: u8,
pub binding: u8,
pub _pad: u16
}
#[derive(Debug, Clone)]
pub struct DseExecutable {
pub header: DseHeader,
pub symbols: Vec<SymbolEntry>,
pub string_table: Vec<u8>,
pub data: Vec<u8>,
pub text: Vec<u32>
}
impl DseExecutable {
pub fn load(path: PathBuf) -> binrw::BinResult<Self> {
let mut file = File::open(path).map_err(|e| binrw::Error::Io(e))?;
Self::read(&mut file)
}
pub fn read<R: Read + Seek>(reader: &mut R) -> binrw::BinResult<Self> {
let header = DseHeader::read(reader)?;
reader.seek(SeekFrom::Start(header.symbol_table_offset as u64))?;
let symbols: Vec<SymbolEntry> = (0..header.symbol_table_size)
.map(|_| SymbolEntry::read(reader))
.collect::<binrw::BinResult<_>>()?;
reader.seek(SeekFrom::Start(header.string_table_offset as u64))?;
let mut string_table = vec![0u8; header.string_table_size as usize];
reader.read_exact(&mut string_table).map_err(|e| binrw::Error::Io(e))?;
reader.seek(SeekFrom::Start(header.data_offset as u64))?;
let mut data = vec![0u8; header.data_size as usize];
reader.read_exact(&mut data).map_err(|e| binrw::Error::Io(e))?;
reader.seek(SeekFrom::Start(header.text_offset as u64))?;
let text: Vec<u32> = (0..header.text_size)
.map(|_| reader.read_le::<u32>())
.collect::<binrw::BinResult<_>>()?;
Ok(DseExecutable { header, symbols, string_table, data, text })
}
pub fn save(&self, path: PathBuf) -> binrw::BinResult<()> {
let mut file = File::create(path)
.map_err(|e| binrw::Error::Io(e))?;
self.write(&mut file)
}
pub fn write<W: Write + Seek>(&self, writer: &mut W) -> binrw::BinResult<()> {
self.header.write(writer)?;
for symbol in &self.symbols {
symbol.write(writer)?;
}
writer.write_all(&self.string_table)?;
writer.write_all(&self.data)?;
for word in &self.text {
writer.write_le(word)?;
}
Ok(())
}
pub fn new(
symbols: &[SymbolEntry],
string_table: &[u8],
data: &[u8],
text: &[u32],
entry_point: u32,
) -> binrw::BinResult<DseExecutable> {
// Derive sizes from real serialization instead of hand-counted constants.
let header_size = {
let mut buf = Cursor::new(Vec::new());
// dummy header just to measure length; fields' values don't matter here
let dummy = DseHeader {
version: 0, flags: 0, entry_point: 0,
symbol_table_offset: 0, symbol_table_size: 0,
string_table_offset: 0, string_table_size: 0,
data_offset: 0, data_size: 0,
text_offset: 0, text_size: 0,
};
dummy.write(&mut buf)?;
buf.into_inner().len() as u32
};
let symbol_entry_size = if symbols.is_empty() {
// measure using a zeroed entry even if symbols is empty
let mut buf = Cursor::new(Vec::new());
SymbolEntry { name_offset: 0, value: 0, section: 0, binding: 0, _pad: 0 }
.write(&mut buf)?;
buf.into_inner().len() as u32
} else {
let mut buf = Cursor::new(Vec::new());
symbols[0].write(&mut buf)?;
buf.into_inner().len() as u32
};
let symbol_table_offset = header_size;
let symbol_table_bytes = symbols.len() as u32 * symbol_entry_size;
let string_table_offset = symbol_table_offset + symbol_table_bytes;
let data_offset = string_table_offset + string_table.len() as u32;
let text_offset = data_offset + data.len() as u32;
let header = DseHeader {
version: 1,
flags: 0,
entry_point,
symbol_table_offset,
symbol_table_size: symbols.len() as u32,
string_table_offset,
string_table_size: string_table.len() as u32,
data_offset,
data_size: data.len() as u32,
text_offset,
text_size: text.len() as u32,
};
Ok(DseExecutable {
header,
symbols: symbols.to_vec(),
string_table: string_table.to_vec(),
data: data.to_vec(),
text: text.to_vec(),
})
}
/// Produces the runtime memory image: TEXT followed immediately by DATA,
/// matching the exact layout `resolve_address` assumed when the assembler
/// resolved symbol addresses. This is NOT the same as the on-disk file
/// layout (header/symtab/strtab/data/text) — this is what actually needs
/// to end up in emulator memory.
pub fn to_memory_image(&self) -> Vec<u8> {
let mut image = Vec::with_capacity(self.text.len() * 4 + self.data.len());
for word in &self.text {
image.extend_from_slice(&word.to_le_bytes());
}
image.extend_from_slice(&self.data);
image
}
/// Runtime address where DATA begins, given TEXT is loaded at `text_base`.
/// Matches the assembler's `resolve_address` convention exactly.
pub fn data_base(&self, text_base: u32) -> u32 {
text_base + (self.text.len() as u32 * 4)
}
/// Absolute runtime entry point address, given TEXT is loaded at `text_base`.
/// `self.header.entry_point` is already an absolute address under the
/// assumption text_base == 0, so this just re-bases it if you're loading
/// somewhere other than address 0.
pub fn entry_point(&self, text_base: u32) -> u32 {
text_base + self.header.entry_point
}
}
// Display implementations for dumping binary contents
impl fmt::Display for DseHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DseHeader {{ version: {}, flags: {}, entry_point: 0x{:08X}, symbol_table_offset: 0x{:08X}, symbol_table_size: {}, string_table_offset: 0x{:08X}, string_table_size: {}, data_offset: 0x{:08X}, data_size: {}, text_offset: 0x{:08X}, text_size: {} }}",
self.version,
self.flags,
self.entry_point,
self.symbol_table_offset,
self.symbol_table_size,
self.string_table_offset,
self.string_table_size,
self.data_offset,
self.data_size,
self.text_offset,
self.text_size)
}
}
impl fmt::Display for SymbolEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SymbolEntry {{ name_offset: 0x{:08X}, value: 0x{:08X}, section: {}, binding: {} }}",
self.name_offset,
self.value,
self.section,
self.binding)
}
}
impl fmt::Display for DseExecutable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Header
writeln!(f, "{}", self.header)?;
// Symbols
writeln!(f, "Symbols:")?;
for s in &self.symbols {
writeln!(f, " {}", s)?;
}
// String table as hex dump
writeln!(f, "String Table ({} bytes):", self.string_table.len())?;
for chunk in self.string_table.chunks(16) {
write!(f, "{}", String::from_utf8_lossy(chunk))?;
// for byte in chunk { write!(f, "{:02X} ", byte)?; }
writeln!(f)?;
}
// Data section
writeln!(f, "Data ({} bytes):", self.data.len())?;
for chunk in self.data.chunks(16) {
write!(f, " ")?;
for byte in chunk { write!(f, "{:02X} ", byte)?; }
for byte in chunk { write!(f, "{} ", *byte as char)?; }
writeln!(f)?;
}
// Text section as Instructions
writeln!(f, "Text ({} words):", self.text.len())?;
for word in &self.text {
writeln!(f, " {:#010X} {:?}", word, Instruction(*word))?;
}
Ok(())
}
}
#[test]
fn symbol_entry_size_is_12_bytes() {
let mut buf = Cursor::new(Vec::new());
SymbolEntry { name_offset: 0, value: 0, section: 0, binding: 0, _pad: 0 }
.write(&mut buf).unwrap();
assert_eq!(buf.into_inner().len(), 12);
}
+2
View File
@@ -0,0 +1,2 @@
pub mod binary_dse;
pub mod binary_dso;
+1
View File
@@ -1,5 +1,6 @@
pub mod asm;
pub mod isa;
pub mod formats;
pub mod prelude {
pub use crate::isa::instructions::Instruction;