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