rewrote assembler backend to support custom executable format (DSE) and also refactored (moving crates around)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod binary_dse;
|
||||
pub mod binary_dso;
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod asm;
|
||||
pub mod isa;
|
||||
pub mod formats;
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::isa::instructions::Instruction;
|
||||
|
||||
Reference in New Issue
Block a user