Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

crates/mimir-binary/src/elf.rs

//! Reading one linked artifact: its symbol table, its sections, and the DWARF
//! compile-unit names that tie a crate's name to its StableCrateId.
//!
//! The symbol walk follows `nornir-introspect::size::analyze_sizes`
//! (copied-then-owned, not called: that function drops undefined and
//! zero-sized symbols and pulls the gatling pool + cargo_metadata; the binary
//! layer needs the imports and nothing else).
//!
//! ## CU names carry the crate identity
//!
//! rustc names a crate's codegen units `<crate>.<StableCrateId hex>-cgu.N`
//! (`rustc_middle::mono::CodegenUnitNameBuilder`), and LLVM's DWARF CU name is
//! `<crate root file>/@/<cgu name>` with `DW_AT_comp_dir` = rustc's cwd. So
//! every crate that contributed code to the artifact names itself: crate name
//! plus StableCrateId plus crate root file — the exact key v0 symbols carry,
//! `C s<base62(id-2)>_ <name>`. A CGU named `<x>.<hex>-in-<local>.<hex>` is an
//! upstream instantiation unit and carries the LOCAL crate's id; skipped.

use std::borrow::Cow;
use std::path::{Path, PathBuf};

use object::{Object, ObjectSection, ObjectSymbol, SymbolKind, SymbolSection};

/// One symbol of the artifact.
#[derive(Debug, Clone)]
pub struct BinSymbol {
    pub name: String,
    pub addr: u64,
    pub size: u64,
    pub section: Option<String>,
    pub defined: bool,
    /// code (`STT_FUNC`) vs data (`STT_OBJECT` / TLS)
    pub is_data: bool,
    pub weak: bool,
}

/// One crate-identity record from a DWARF CU name.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CuCrate {
    pub crate_name: String,
    pub stable_crate_id: u64,
    /// crate root file as rustc saw it (comp_dir-joined when relative)
    pub root_file: PathBuf,
}

#[derive(Debug, Clone, Default)]
pub struct Binary {
    pub file_size: u64,
    pub sections: Vec<(String, u64)>,
    pub symbols: Vec<BinSymbol>,
    pub cus: Vec<CuCrate>,
    /// DWARF present at all (`.debug_info` non-empty)
    pub has_dwarf: bool,
}

impl Binary {
    pub fn section_size(&self, name: &str) -> u64 {
        self.sections.iter().filter(|(n, _)| n == name).map(|(_, s)| *s).sum()
    }
}

pub fn read_binary(path: &Path) -> Result<Binary, String> {
    let data = std::fs::read(path).map_err(|e| format!("read {}: {e}", path.display()))?;
    let obj = object::File::parse(&*data).map_err(|e| format!("parse {}: {e}", path.display()))?;
    let mut b = Binary { file_size: data.len() as u64, ..Default::default() };
    for sec in obj.sections() {
        b.sections.push((sec.name().unwrap_or("").to_string(), sec.size()));
    }
    b.symbols = symbols(&obj);
    b.has_dwarf = obj.section_by_name(".debug_info").is_some_and(|s| s.size() > 0);
    b.cus = cu_crates(&obj);
    Ok(b)
}

fn symbols(obj: &object::File) -> Vec<BinSymbol> {
    let mut out = Vec::new();
    // `.symtab` when present (static symbols, locals included); a stripped
    // artifact still has `.dynsym`.
    let use_dynamic = obj.symbols().next().is_none();
    let iter: Box<dyn Iterator<Item = object::Symbol>> =
        if use_dynamic { Box::new(obj.dynamic_symbols()) } else { Box::new(obj.symbols()) };
    for sym in iter {
        let Ok(name) = sym.name() else { continue };
        if name.is_empty() {
            continue;
        }
        let kind = sym.kind();
        if matches!(kind, SymbolKind::Section | SymbolKind::File | SymbolKind::Label) {
            continue;
        }
        let defined = !sym.is_undefined();
        let section = match sym.section() {
            SymbolSection::Section(idx) => obj.section_by_index(idx).ok().and_then(|s| s.name().ok().map(str::to_string)),
            _ => None,
        };
        out.push(BinSymbol {
            name: name.to_string(),
            addr: sym.address(),
            size: sym.size(),
            section,
            defined,
            is_data: matches!(kind, SymbolKind::Data | SymbolKind::Tls),
            weak: sym.is_weak(),
        });
    }
    out
}

/// Parse `<crate>.<hex>-cgu.N` (the CGU part of a CU name).
pub fn parse_cgu_name(cgu: &str) -> Option<(String, u64)> {
    let (crate_name, rest) = cgu.split_once('.')?;
    let hex_end = rest.find(|c: char| !c.is_ascii_hexdigit())?;
    let (hex, tail) = rest.split_at(hex_end);
    // `-in-<local>.<hex>`: an upstream-instantiation unit, carries the LOCAL id
    if !tail.starts_with("-cgu") || hex.is_empty() || hex.len() > 16 {
        return None;
    }
    Some((crate_name.to_string(), u64::from_str_radix(hex, 16).ok()?))
}

/// Every `(crate, StableCrateId, root file)` named by a DWARF CU of `obj`.
pub fn cu_crates(obj: &object::File) -> Vec<CuCrate> {
    let endian = if obj.is_little_endian() { gimli::RunTimeEndian::Little } else { gimli::RunTimeEndian::Big };
    let load = |id: gimli::SectionId| -> Result<Cow<'_, [u8]>, gimli::Error> {
        Ok(obj
            .section_by_name(id.name())
            .and_then(|s| s.uncompressed_data().ok())
            .unwrap_or(Cow::Borrowed(&[])))
    };
    let Ok(sections) = gimli::DwarfSections::load(load) else { return Vec::new() };
    let dwarf = sections.borrow(|s| gimli::EndianSlice::new(s, endian));
    let mut out: Vec<CuCrate> = Vec::new();
    let mut units = dwarf.units();
    while let Ok(Some(header)) = units.next() {
        let Ok(unit) = dwarf.unit(header) else { continue };
        let Some(name) = unit.name.map(|n| n.to_string_lossy().into_owned()) else { continue };
        let comp_dir = unit.comp_dir.map(|d| d.to_string_lossy().into_owned());
        let Some((root, cgu)) = name.rsplit_once("/@/") else { continue };
        let Some((crate_name, id)) = parse_cgu_name(cgu) else { continue };
        let root = Path::new(root);
        let root_file = match (&comp_dir, root.is_absolute()) {
            (Some(d), false) => Path::new(d).join(root),
            _ => root.to_path_buf(),
        };
        let c = CuCrate { crate_name, stable_crate_id: id, root_file };
        if !out.contains(&c) {
            out.push(c);
        }
    }
    out
}

/// What one rlib in a cargo deps dir says about its crate.
#[derive(Debug, Clone, Default)]
pub struct RlibCrate {
    pub crate_name: String,
    /// StableCrateIds seen as the root of DEFINED v0 symbols named after this
    /// crate in its own objects, and in its own CU names
    pub ids: Vec<u64>,
    /// crate root file from the CU name, or the first input of cargo's
    /// `<crate>-<hash>.d` dep-info next to it
    pub root_file: Option<PathBuf>,
    pub path: PathBuf,
}

/// Scan a cargo build dir: every `lib<crate>-<hash>.rlib` below `dir` (the
/// classic `target/<profile>/deps/` layout and cargo's newer
/// `build/<pkg>/<hash>/out/` layout alike) → its crate ids + root file.
/// Rlibs carry the crate's own objects even when the final link dropped them
/// (generic-only crates, inlined code), so this resolves crates whose code
/// reached the artifact only as instances or inlined bodies.
pub fn scan_deps_dir(dir: &Path) -> Vec<RlibCrate> {
    let mut out = Vec::new();
    let mut paths = Vec::new();
    collect_rlibs(dir, 0, &mut paths);
    paths.sort();
    for p in paths {
        let dir = p.parent().unwrap_or(dir).to_path_buf();
        let Some(stem) = p.file_stem().and_then(|s| s.to_str()) else { continue };
        let stem = stem.strip_prefix("lib").unwrap_or(stem);
        let crate_name = stem.rsplit_once('-').map(|(c, _)| c).unwrap_or(stem).to_string();
        let mut rc = RlibCrate { crate_name: crate_name.clone(), path: p.clone(), ..Default::default() };
        if let Ok(data) = std::fs::read(&p) {
            if let Ok(ar) = object::read::archive::ArchiveFile::parse(&*data) {
                for m in ar.members().flatten() {
                    let Ok(mname) = std::str::from_utf8(m.name()) else { continue };
                    if !mname.ends_with(".o") {
                        continue;
                    }
                    let Ok(mdata) = m.data(&*data) else { continue };
                    let Ok(obj) = object::File::parse(mdata) else { continue };
                    for cu in cu_crates(&obj) {
                        if cu.crate_name == crate_name {
                            if !rc.ids.contains(&cu.stable_crate_id) {
                                rc.ids.push(cu.stable_crate_id);
                            }
                            rc.root_file.get_or_insert(cu.root_file);
                        }
                    }
                    for s in obj.symbols() {
                        if s.is_undefined() {
                            continue;
                        }
                        let Ok(n) = s.name() else { continue };
                        if let Some(id) = own_crate_id(n, &crate_name) {
                            if !rc.ids.contains(&id) {
                                rc.ids.push(id);
                            }
                        }
                    }
                }
            }
        }
        if rc.root_file.is_none() {
            rc.root_file = dep_info_root(&dir.join(format!("{stem}.d")));
        }
        out.push(rc);
    }
    out
}

/// The StableCrateId of a NON-generic v0 symbol whose path roots in `crate_name`
/// (`_RNv…C s<id>_ <len><crate_name>…` with no instantiating-crate suffix):
/// such a symbol is an item of that crate, defined by that crate's own compile.
fn own_crate_id(sym: &str, crate_name: &str) -> Option<u64> {
    let s = crate::v0::decode(sym).ok()?;
    if s.instantiating_crate.is_some() {
        return None;
    }
    let mut p = &s.path;
    loop {
        match p {
            crate::v0::Path::Nested { parent, .. } => p = parent,
            crate::v0::Path::InherentImpl { parent, .. } | crate::v0::Path::TraitImpl { parent, .. } => p = parent,
            crate::v0::Path::Crate { name, id } => return (name == crate_name).then_some(*id),
            _ => return None,
        }
    }
}

fn collect_rlibs(dir: &Path, depth: usize, out: &mut Vec<PathBuf>) {
    if depth > 6 {
        return;
    }
    let Ok(rd) = std::fs::read_dir(dir) else { return };
    for e in rd.flatten() {
        let p = e.path();
        let Ok(ft) = e.file_type() else { continue };
        if ft.is_dir() {
            collect_rlibs(&p, depth + 1, out);
        } else if p.extension().is_some_and(|x| x == "rlib") && p.file_stem().and_then(|s| s.to_str()).is_some_and(|s| s.contains('-')) {
            out.push(p);
        }
    }
}

/// Every rlib of a toolchain sysroot (`<sysroot>/lib/rustlib/<target>/lib`).
pub fn scan_sysroot(sysroot: &Path) -> Vec<RlibCrate> {
    let mut out = Vec::new();
    let Ok(rd) = std::fs::read_dir(sysroot.join("lib/rustlib")) else { return out };
    for t in rd.flatten() {
        let lib = t.path().join("lib");
        if lib.is_dir() {
            out.extend(scan_deps_dir(&lib));
        }
    }
    out
}

/// First input of a rustc dep-info file: the crate root.
fn dep_info_root(d: &Path) -> Option<PathBuf> {
    let text = std::fs::read_to_string(d).ok()?;
    let line = text.lines().find(|l| l.contains(": ") && !l.trim_start().starts_with('#'))?;
    let (_, deps) = line.split_once(": ")?;
    // first space-separated token, `\ ` is an escaped space
    let mut tok = String::new();
    let mut chars = deps.trim_start().chars().peekable();
    while let Some(c) = chars.next() {
        if c == '\\' && chars.peek() == Some(&' ') {
            tok.push(' ');
            chars.next();
        } else if c == ' ' {
            break;
        } else {
            tok.push(c);
        }
    }
    (!tok.is_empty()).then(|| PathBuf::from(tok))
}

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

    #[test]
    fn cgu_names() {
        assert_eq!(parse_cgu_name("std.6c98fd8553dbae28-cgu.0"), Some(("std".into(), 0x6c98fd8553dbae28)));
        // `{:08x}` does not zero-pad to 16
        assert_eq!(parse_cgu_name("alloc.12e961f32105355-cgu.0"), Some(("alloc".into(), 0x12e961f32105355)));
        assert_eq!(parse_cgu_name("a.1234abcd-in-b.99887766-cgu.0"), None);
    }

    #[test]
    fn own_crate_ids() {
        assert_eq!(own_crate_id("_RNvCsfPIvSHfysBz_1b3run", "b"), Some(0xb86e25d42022e20b));
        assert_eq!(own_crate_id("_RNvCsfPIvSHfysBz_1b3run", "a"), None);
        // an instance (instantiating-crate suffix) says nothing about its root's compile
        assert_eq!(own_crate_id("_RINvCsbR8JNGoZHix_1a5identmECsfPIvSHfysBz_1b", "a"), None);
    }
}