|
|
|
@@ -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);
|
|
|
|
|
}
|