univdreams 0.4.0

Byte-identical binary <-> source round-tripping: binary formats, multi-arch decode/encode, a .ud decompiler/compiler, and an x86 emulator with Win16/Win32/Linux hosts.
Documentation
//! Function discovery from ELF symbol tables (`.symtab` and `.dynsym`).
//!
//! This is the highest-confidence signal we have for function boundaries
//! (when symbols are present). The implementation deliberately ignores
//! anything that isn't a function symbol with a defined address and
//! resolved section — undefined imports (`U printf`) and pseudo-symbols
//! get filtered out.

use crate::common::VAddr;
use crate::format::elf::{Elf64File, ElfClass, SHT_DYNSYM, SHT_SYMTAB};

use crate::analysis::function_map::{Function, FunctionSource};

/// On-disk size of an `Elf64_Sym` entry.
const SYM64_SIZE: usize = 24;
/// On-disk size of an `Elf32_Sym` entry.
const SYM32_SIZE: usize = 16;

/// `st_shndx` value indicating an undefined symbol — for instance a
/// dynamic import that the loader resolves at runtime. We skip these.
const SHN_UNDEF: u16 = 0;

/// `st_info & 0xf` = symbol type. We accept `STT_FUNC` and the GNU
/// indirect-function variant; everything else is data, sections, files,
/// or absent semantics.
const STT_FUNC: u8 = 2;
const STT_GNU_IFUNC: u8 = 10;

/// Errors specific to symbol-table parsing.
#[derive(Debug, thiserror::Error)]
pub enum SymbolError {
    #[error("symbol table section #{idx} has unexpected sh_entsize {got}, expected {expected}")]
    BadEntsize { idx: usize, got: u64, expected: u64 },

    #[error("symbol table section #{idx} has size {size} not a multiple of entry size {entry}")]
    BadTableSize { idx: usize, size: u64, entry: u64 },

    #[error("symbol table section #{idx} links to invalid string table index {strtab_idx}")]
    BadStrtabIndex { idx: usize, strtab_idx: usize },

    #[error(
        "symbol #{sym} in section #{idx} has st_name = {name} which exceeds string table size {strtab_size}"
    )]
    StrtabOutOfBounds {
        idx: usize,
        sym: usize,
        name: u64,
        strtab_size: usize,
    },

    #[error("symbol #{sym} in section #{idx} has a non-UTF-8 name at strtab offset {offset}")]
    NameNotUtf8 {
        idx: usize,
        sym: usize,
        offset: usize,
    },
}

/// Sweep every `.symtab` and `.dynsym` in `elf`, return one [`Function`]
/// per qualifying entry. Names are resolved via the linked string table.
///
/// "Qualifying" means: the symbol is a function (`STT_FUNC` or
/// `STT_GNU_IFUNC`), defines a non-zero address, and resolves to a real
/// section (`st_shndx != SHN_UNDEF`).
pub fn discover_from_symbol_tables(elf: &Elf64File) -> Result<Vec<Function>, SymbolError> {
    let mut out = Vec::new();
    for (idx, shdr, _data) in elf.sections() {
        let source = match shdr.sh_type {
            SHT_SYMTAB => FunctionSource::SymTab,
            SHT_DYNSYM => FunctionSource::DynSym,
            _ => continue,
        };
        collect_one_table(elf, idx, source, &mut out)?;
    }
    Ok(out)
}

fn collect_one_table(
    elf: &Elf64File,
    idx: usize,
    source: FunctionSource,
    out: &mut Vec<Function>,
) -> Result<(), SymbolError> {
    let shdr = &elf.shdrs[idx];
    let entry_size = match elf.class {
        ElfClass::Elf32 => SYM32_SIZE,
        ElfClass::Elf64 => SYM64_SIZE,
    };

    if shdr.sh_entsize != entry_size as u64 {
        return Err(SymbolError::BadEntsize {
            idx,
            got: shdr.sh_entsize,
            expected: entry_size as u64,
        });
    }
    let table = elf.section_data(idx).unwrap_or(&[]);
    if !table.len().is_multiple_of(entry_size) {
        return Err(SymbolError::BadTableSize {
            idx,
            size: table.len() as u64,
            entry: entry_size as u64,
        });
    }

    let strtab_idx = shdr.sh_link as usize;
    let strtab = elf
        .section_data(strtab_idx)
        .ok_or(SymbolError::BadStrtabIndex { idx, strtab_idx })?;

    for (sym_idx, chunk) in table.chunks_exact(entry_size).enumerate() {
        let (st_name, st_info, st_shndx, st_value, st_size) = match elf.class {
            ElfClass::Elf64 => parse_sym64(chunk),
            ElfClass::Elf32 => parse_sym32(chunk),
        };

        let st_type = st_info & 0xf;
        if st_type != STT_FUNC && st_type != STT_GNU_IFUNC {
            continue;
        }
        // SHN_UNDEF symbols are imports — not a defined
        // function in this object. We *don't* skip on
        // `st_value == 0` though: relocatable object files
        // (`.o`) commonly place the first function in `.text`
        // at offset 0, which is a legitimate address. Linked
        // executables don't put real symbols at vaddr 0, so
        // the stricter behaviour wasn't necessary even
        // historically.
        if st_shndx == SHN_UNDEF {
            continue;
        }

        let name = read_strtab_cstring(strtab, st_name as usize).map_err(|()| {
            SymbolError::StrtabOutOfBounds {
                idx,
                sym: sym_idx,
                name: u64::from(st_name),
                strtab_size: strtab.len(),
            }
        })?;
        let name = std::str::from_utf8(name)
            .map_err(|_| SymbolError::NameNotUtf8 {
                idx,
                sym: sym_idx,
                offset: st_name as usize,
            })?
            .to_string();

        out.push(Function {
            addr: VAddr(st_value),
            size: st_size,
            name,
            sources: vec![source],
        });
    }
    Ok(())
}

/// Decode an `Elf64_Sym` entry. Field order:
/// `st_name(4) st_info(1) st_other(1) st_shndx(2) st_value(8) st_size(8)`.
fn parse_sym64(chunk: &[u8]) -> (u32, u8, u16, u64, u64) {
    let st_name = u32::from_le_bytes(chunk[0..4].try_into().unwrap());
    let st_info = chunk[4];
    // chunk[5] is st_other (visibility); preserved on round-trip
    // but irrelevant to function discovery.
    let st_shndx = u16::from_le_bytes(chunk[6..8].try_into().unwrap());
    let st_value = u64::from_le_bytes(chunk[8..16].try_into().unwrap());
    let st_size = u64::from_le_bytes(chunk[16..24].try_into().unwrap());
    (st_name, st_info, st_shndx, st_value, st_size)
}

/// Decode an `Elf32_Sym` entry. Field order differs from `Elf64_Sym`:
/// `st_name(4) st_value(4) st_size(4) st_info(1) st_other(1) st_shndx(2)`.
fn parse_sym32(chunk: &[u8]) -> (u32, u8, u16, u64, u64) {
    let st_name = u32::from_le_bytes(chunk[0..4].try_into().unwrap());
    let st_value = u32::from_le_bytes(chunk[4..8].try_into().unwrap());
    let st_size = u32::from_le_bytes(chunk[8..12].try_into().unwrap());
    let st_info = chunk[12];
    // chunk[13] is st_other.
    let st_shndx = u16::from_le_bytes(chunk[14..16].try_into().unwrap());
    (
        st_name,
        st_info,
        st_shndx,
        u64::from(st_value),
        u64::from(st_size),
    )
}

/// Read a NUL-terminated byte slice starting at `offset` in `strtab`.
fn read_strtab_cstring(strtab: &[u8], offset: usize) -> Result<&[u8], ()> {
    let tail = strtab.get(offset..).ok_or(())?;
    let nul = tail.iter().position(|&b| b == 0).ok_or(())?;
    Ok(&tail[..nul])
}

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

    #[test]
    fn read_strtab_returns_string_at_offset() {
        let strtab = b"\0main\0puts\0";
        assert_eq!(read_strtab_cstring(strtab, 1), Ok(&b"main"[..]));
        assert_eq!(read_strtab_cstring(strtab, 6), Ok(&b"puts"[..]));
        assert_eq!(read_strtab_cstring(strtab, 0), Ok(&b""[..]));
    }

    #[test]
    fn read_strtab_rejects_offset_past_end() {
        let strtab = b"\0main\0";
        assert!(read_strtab_cstring(strtab, 100).is_err());
    }

    #[test]
    fn read_strtab_rejects_unterminated() {
        let strtab = b"main"; // no trailing NUL
        assert!(read_strtab_cstring(strtab, 0).is_err());
    }
}