rigless 0.2.0

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The firmware's own names for its code and data, read from its ELF symbol table.
//!
//! A fault can fire at an instruction a case names by the firmware's symbol, the name `nm` prints,
//! mangled where the firmware's language mangles. They come from the symbol table and the strings
//! it names them from, and nothing else of the file.

use std::collections::BTreeMap;

use crate::elf::{self, File, Section};

/// A section that holds the symbol table.
const SYMBOL_TABLE: u32 = 2;
/// A symbol that names a function.
const FUNCTION: u8 = 2;
/// A symbol's binding when it's local to the file it came from.
const LOCAL: u8 = 0;

/// Why a firmware's symbols couldn't be read.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// The file isn't an ELF file this reads, or ends before something it says it holds.
    #[error("the firmware {0}")]
    Elf(#[from] elf::Error),
    /// No symbol has the name.
    #[error("the firmware has no symbol {0}")]
    Missing(String),
    /// Two symbols have the name at different addresses, each local to its own file.
    #[error("the firmware has two symbols {0}, each local to its own file")]
    Twice(String),
}

/// Every named symbol of a firmware with its address.
///
/// A function's address is where its first instruction is, which the core family says from the
/// value its symbol has, [`CoreSpec::code_address`](crate::emulator::CoreSpec::code_address). A name
/// the linker bound for the whole program names its symbol over any local to one file, and a name two
/// files each keep local at different addresses names neither.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Symbols(BTreeMap<String, Named>);

/// What a name names.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Named {
    Global(u64),
    Local(u64),
    Twice,
}

impl Named {
    /// What a name names once `next` has it too.
    fn with(self, next: Self) -> Self {
        match (self, next) {
            (Self::Global(_), _) => self,
            (_, Self::Global(_)) => next,
            (Self::Local(held), Self::Local(value)) if held == value => self,
            _ => Self::Twice,
        }
    }
}

impl Symbols {
    /// The symbols of the ELF file `image`, each function's at the address `code_address` gives its
    /// first instruction from the value its symbol has. A file with no symbol table has none.
    ///
    /// # Errors
    ///
    /// A file that isn't an ELF file of a class and byte order this reads, or one that ends before
    /// what it says it holds.
    pub fn read(image: &[u8], code_address: fn(u64) -> u64) -> Result<Self, Error> {
        let file = File::new(image)?;
        let mut symbols = BTreeMap::new();
        for section in 0..file.sections()? {
            let Section {
                kind,
                offset,
                size,
                link,
                entry,
                ..
            } = file.section(section)?;
            if kind != SYMBOL_TABLE || entry == 0 {
                continue;
            }
            let strings = file.section(link)?;
            let names = file.bytes(strings.offset, strings.size, "symbol names")?;
            file.bytes(offset, size, "symbols")?;
            for index in 0..size / entry {
                let (name, value, info) = file.symbol(offset + index * entry)?;
                let Some(name) = names
                    .get(
                        usize::try_from(name)
                            .map_err(|_too_large| elf::Error::Short("symbol names"))?..,
                    )
                    .and_then(|rest| rest.split(|&byte| byte == 0).next())
                    .filter(|name| !name.is_empty())
                else {
                    continue;
                };
                let value = if info & 0xF == FUNCTION {
                    code_address(value)
                } else {
                    value
                };
                let named = if info >> 4 == LOCAL {
                    Named::Local(value)
                } else {
                    Named::Global(value)
                };
                symbols
                    .entry(String::from_utf8_lossy(name).into_owned())
                    .and_modify(|held: &mut Named| *held = held.with(named))
                    .or_insert(named);
            }
        }
        Ok(Self(symbols))
    }

    /// Whether the firmware has any symbols at all.
    pub(crate) fn any(&self) -> bool {
        !self.0.is_empty()
    }

    /// The address `name` names.
    ///
    /// # Errors
    ///
    /// A name the firmware doesn't have, or has twice.
    pub fn get(&self, name: &str) -> Result<u64, Error> {
        match self.0.get(name) {
            Some(Named::Global(value) | Named::Local(value)) => Ok(*value),
            Some(Named::Twice) => Err(Error::Twice(name.to_owned())),
            None => Err(Error::Missing(name.to_owned())),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// An ELF file of `class`, 1 for 32-bit and 2 for 64-bit, and byte `order`, 1 for little-endian
    /// and 2 for big, with one symbol table, laid out as the format says: a function `f` at 0x1001, an
    /// object `d` at 0x2001, a global function `g` at 0x3001 after a local one at 0x4000, two local
    /// objects `l` at 0x5000 and 0x6000, and a symbol with no name.
    fn elf(class: u8, order: u8) -> Vec<u8> {
        let (wide, big) = (class == 2, order == 2);
        let put = |out: &mut Vec<u8>, value: u64, width: usize| {
            let bytes = value.to_le_bytes();
            let mut field = bytes[..width].to_vec();
            if big {
                field.reverse();
            }
            out.extend(field);
        };
        let word = if wide { 8 } else { 4 };
        let header = if wide { 64 } else { 52 };
        let section_size: usize = if wide { 64 } else { 40 };
        let symbol_size: usize = if wide { 24 } else { 16 };
        let strings = b"\0f\0d\0g\0l\0".to_vec();
        let global = 0x10;
        let mut symbols = Vec::new();
        for (name, value, info) in [
            (0, 0, 0),
            (1, 0x1001, FUNCTION),
            (3, 0x2001, 1),
            (5, 0x4000, FUNCTION),
            (5, 0x3001, global | FUNCTION),
            (7, 0x5000, 1),
            (7, 0x6000, 1),
            (0, 9, 0),
        ] {
            put(&mut symbols, name, 4);
            if wide {
                put(&mut symbols, info.into(), 1);
                put(&mut symbols, 0, 3);
                put(&mut symbols, value, 8);
                put(&mut symbols, 0, 8);
            } else {
                put(&mut symbols, value, 4);
                put(&mut symbols, 0, 4);
                put(&mut symbols, info.into(), 1);
                put(&mut symbols, 0, 3);
            }
        }
        let strings_at = header;
        let symbols_at = strings_at + strings.len();
        let sections_at = symbols_at + symbols.len();
        let mut out = vec![
            0x7f,
            b'E',
            b'L',
            b'F',
            if wide { 2 } else { 1 },
            if big { 2 } else { 1 },
        ];
        out.resize(if wide { 40 } else { 32 }, 0);
        put(&mut out, sections_at as u64, word);
        out.resize(if wide { 58 } else { 46 }, 0);
        put(&mut out, section_size as u64, 2);
        put(&mut out, 3, 2);
        out.resize(header, 0);
        out.extend(&strings);
        out.extend(&symbols);
        let section =
            |out: &mut Vec<u8>, kind: u64, at: usize, size: usize, link: u64, entry: usize| {
                put(out, 0, 4);
                put(out, kind, 4);
                if wide {
                    put(out, 0, 8);
                    put(out, 0, 8);
                    put(out, at as u64, 8);
                    put(out, size as u64, 8);
                    put(out, link, 4);
                    put(out, 0, 4);
                    put(out, 0, 8);
                    put(out, entry as u64, 8);
                } else {
                    put(out, 0, 8);
                    put(out, at as u64, 4);
                    put(out, size as u64, 4);
                    put(out, link, 4);
                    put(out, 0, 8);
                    put(out, entry as u64, 4);
                }
            };
        section(&mut out, 0, 0, 0, 0, 0);
        section(&mut out, 3, strings_at, strings.len(), 0, 0);
        section(
            &mut out,
            u64::from(SYMBOL_TABLE),
            symbols_at,
            symbols.len(),
            1,
            symbol_size,
        );
        out
    }

    /// What a family whose ABI keeps a mode in bit 0 of a function's symbol says its code is at.
    fn without_bit_0(value: u64) -> u64 {
        value & !1
    }

    #[test]
    fn every_class_and_byte_order_reads_its_named_symbols_with_a_functions_bit_0_clear() {
        for class in [1, 2] {
            for order in [1, 2] {
                let symbols = Symbols::read(&elf(class, order), without_bit_0).unwrap();
                assert_eq!(symbols.get("f"), Ok(0x1000), "class {class}, order {order}");
                let kept = Symbols::read(&elf(class, order), std::convert::identity).unwrap();
                assert_eq!(kept.get("f"), Ok(0x1001), "a family that keeps bit 0");
                assert_eq!(symbols.get("d"), Ok(0x2001), "an object keeps its bit 0");
                assert_eq!(symbols.0.len(), 4, "a symbol with no name isn't one");
            }
        }
    }

    #[test]
    fn a_global_name_wins_over_a_local_one_and_two_local_ones_name_neither() {
        let symbols = Symbols::read(&elf(2, 1), without_bit_0).unwrap();
        assert_eq!(symbols.get("g"), Ok(0x3000));
        assert_eq!(symbols.get("l"), Err(Error::Twice("l".to_owned())));
        assert_eq!(symbols.get("m"), Err(Error::Missing("m".to_owned())));
    }

    #[test]
    fn a_file_that_isnt_elf_or_ends_early_is_refused() {
        assert_eq!(
            Symbols::read(b"#!/bin/sh", without_bit_0),
            Err(Error::Elf(elf::Error::NotElf))
        );
        let whole = elf(1, 1);
        assert!(matches!(
            Symbols::read(&whole[..whole.len() - 8], without_bit_0),
            Err(Error::Elf(elf::Error::Short(_)))
        ));
        let mut far = elf(2, 1);
        far[40..48].copy_from_slice(&(u64::MAX - 8).to_le_bytes());
        assert_eq!(
            Symbols::read(&far, without_bit_0),
            Err(Error::Elf(elf::Error::Short("section headers"))),
            "section headers said to start near the end of the address space"
        );
    }
}