use anyhow::{Context, Result};
use object::{Object, ObjectSymbol, SymbolKind};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymbolInfo {
pub address: u64,
pub kind: SymbolKind,
pub size: u64,
pub is_undefined: bool,
pub is_weak: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SymbolTable {
pub by_name: HashMap<String, SymbolInfo>,
}
impl SymbolTable {
pub fn count_kind(&self, kind: SymbolKind) -> usize {
self.by_name.values().filter(|s| s.kind == kind).count()
}
}
pub fn parse_symbol_table(path: &Path) -> Result<SymbolTable> {
let bytes = std::fs::read(path).with_context(|| format!("read {}", path.display()))?;
parse_symbol_table_from_bytes(&bytes).with_context(|| format!("parse {}", path.display()))
}
pub fn parse_symbol_table_from_bytes(bytes: &[u8]) -> Result<SymbolTable> {
let file = object::File::parse(bytes).context("object::File::parse")?;
let mut by_name = HashMap::new();
for sym in file.symbols() {
let name = match sym.name() {
Ok(n) if !n.is_empty() => normalize_symbol_name(n),
_ => continue, };
by_name.insert(
name,
SymbolInfo {
address: sym.address(),
kind: sym.kind(),
size: sym.size(),
is_undefined: sym.is_undefined(),
is_weak: sym.is_weak(),
},
);
}
Ok(SymbolTable { by_name })
}
fn normalize_symbol_name(name: &str) -> String {
match name.split_once(".llvm.") {
Some((stem, _suffix)) => stem.to_string(),
None => name.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
fn ensure_whisker_binary() -> std::path::PathBuf {
let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let bin = workspace_root.join("target/debug/whisker");
if !bin.is_file() {
let status = Command::new("cargo")
.args(["build", "-p", "whisker-cli", "--bin", "whisker"])
.current_dir(&workspace_root)
.status()
.expect("spawn cargo");
assert!(status.success(), "cargo build failed");
}
bin
}
#[test]
fn parses_a_real_host_binary_and_finds_known_function_symbols() {
let bin = ensure_whisker_binary();
let table = parse_symbol_table(&bin).expect("parse");
assert!(
!table.by_name.is_empty(),
"expected symbols in {}",
bin.display(),
);
assert!(
table.count_kind(SymbolKind::Text) > 10,
"expected dozens of function symbols, got {}",
table.count_kind(SymbolKind::Text),
);
}
#[test]
fn parses_a_real_host_binary_with_at_least_one_named_function() {
let bin = ensure_whisker_binary();
let table = parse_symbol_table(&bin).expect("parse");
let any_named_text = table
.by_name
.values()
.any(|s| s.kind == SymbolKind::Text && !s.is_undefined);
assert!(any_named_text, "no defined function symbols");
}
#[test]
fn normalize_strips_llvm_internalization_suffix() {
let host_form = "_ZN11hello_world3app17h04c91e1b6c02c8b4E.llvm.9162950015328890148";
let patch_form = "_ZN11hello_world3app17h04c91e1b6c02c8b4E";
assert_eq!(normalize_symbol_name(host_form), patch_form);
assert_eq!(normalize_symbol_name(patch_form), patch_form);
}
#[test]
fn normalize_leaves_unrelated_dots_alone() {
assert_eq!(normalize_symbol_name("foo.cold.0"), "foo.cold.0",);
assert_eq!(normalize_symbol_name("plain_C_symbol"), "plain_C_symbol");
}
#[test]
fn rejects_non_object_bytes_with_an_error() {
let err = parse_symbol_table_from_bytes(b"not an object file at all").unwrap_err();
let _ = err.to_string();
}
#[test]
fn count_kind_is_a_simple_filter() {
let mut t = SymbolTable::default();
t.by_name.insert(
"f".into(),
SymbolInfo {
address: 0x1000,
kind: SymbolKind::Text,
size: 32,
is_undefined: false,
is_weak: false,
},
);
t.by_name.insert(
"g".into(),
SymbolInfo {
address: 0x2000,
kind: SymbolKind::Data,
size: 8,
is_undefined: false,
is_weak: false,
},
);
assert_eq!(t.count_kind(SymbolKind::Text), 1);
assert_eq!(t.count_kind(SymbolKind::Data), 1);
assert_eq!(t.count_kind(SymbolKind::Tls), 0);
}
}