use crate::headrun::binary_digest;
use crate::invoke::{self, CommonArgs};
use crate::itemreuse::Bin;
use crate::runcache;
const KEEP: usize = 64;
#[derive(Clone, Debug, PartialEq)]
pub struct Extent {
pub vma: u64,
pub size: u64,
pub entry: u64,
}
fn entry_size(section: &str) -> u64 {
section
.strip_prefix(".rodata.cst")
.and_then(|n| n.parse().ok())
.unwrap_or(0)
}
pub fn parse(text: &str) -> Option<Vec<Extent>> {
let header = text.lines().next()?;
let mut extents = if header.contains("VMA") && header.contains("LMA") {
parse_columns(text, 16, 16)
} else if header.contains("VMA") {
parse_columns(text, 16, 10)
} else if text.contains("Linker script and memory map") {
parse_gnu(text)
} else {
return None;
};
extents.retain(|e| e.size > 0);
extents.sort_by_key(|e| e.vma);
(!extents.is_empty()).then_some(extents)
}
fn parse_columns(text: &str, vma_radix: u32, size_radix: u32) -> Vec<Extent> {
let lma = size_radix == 16;
text.lines()
.filter(|line| line.ends_with(')') && line.contains(":("))
.filter_map(|line| {
let mut f = line.split_whitespace();
let vma = f.next()?;
let vma = u64::from_str_radix(vma.strip_prefix("0x").unwrap_or(vma), vma_radix).ok()?;
if lma {
f.next()?;
}
let size = u64::from_str_radix(f.next()?, size_radix).ok()?;
let section = line.rsplit_once(":(")?.1.strip_suffix(')')?;
Some(Extent {
vma,
size,
entry: entry_size(section),
})
})
.collect()
}
fn parse_gnu(text: &str) -> Vec<Extent> {
let hex = |s: &str| u64::from_str_radix(s.strip_prefix("0x")?, 16).ok();
let mut extents = Vec::new();
let mut named = false;
let mut section = "";
for line in text.lines() {
let f: Vec<&str> = line.split_whitespace().collect();
let nested = line.starts_with(' ');
match f[..] {
[name] if nested && name.starts_with('.') => {
named = true;
section = name;
}
[name, addr, size, ..] if nested && name.starts_with('.') => {
named = false;
if let (Some(vma), Some(size)) = (hex(addr), hex(size)) {
let entry = entry_size(name);
extents.push(Extent { vma, size, entry });
}
}
[addr, size, ..] if named => {
named = false;
if let (Some(vma), Some(size)) = (hex(addr), hex(size)) {
let entry = entry_size(section);
extents.push(Extent { vma, size, entry });
}
}
_ => named = false,
}
}
extents
}
fn encode(extents: &[Extent]) -> String {
extents
.iter()
.map(|e| format!("{:x} {:x} {:x}\n", e.vma, e.size, e.entry))
.collect()
}
fn decode(text: &str) -> Vec<Extent> {
text.lines()
.filter_map(|line| {
let mut f = line.split(' ').map(|n| u64::from_str_radix(n, 16).ok());
Some(Extent {
vma: f.next()??,
size: f.next()??,
entry: f.next()??,
})
})
.collect()
}
pub fn extents(cmd: &str, common: &CommonArgs, bin: &Bin) -> Vec<Extent> {
match obtain(common, bin) {
Ok(extents) => extents,
Err(why) => {
println!(
"{cmd}: no link map for {} ({why}); unnamed constants hash as whole sections",
bin.exe.display()
);
Vec::new()
}
}
}
fn obtain(common: &CommonArgs, bin: &Bin) -> Result<Vec<Extent>, String> {
let digest = bin.digest.ok_or("binary unreadable")?;
let maps = runcache::state_dir("maps").ok_or("no .soothfast directory")?;
let stored = maps.join(format!("{digest}.extents"));
if let Ok(text) = std::fs::read_to_string(&stored)
&& let extents = decode(&text)
&& !extents.is_empty()
{
return Ok(extents);
}
let map = maps.join(format!("{digest}.map"));
let built = invoke::build_link_map(common, bin.dir, bin.target_dir, &map);
let text = std::fs::read_to_string(&map);
let built = built.ok_or("the map build failed")?;
if binary_digest(&built).as_deref() != Some(digest) {
return Err("the map build links different code".into());
}
let extents = parse(&text.map_err(|e| e.to_string())?).ok_or("unrecognized map format")?;
if std::fs::write(&stored, encode(&extents)).is_ok() {
runcache::prune_oldest(&maps, KEEP);
}
Ok(extents)
}
#[cfg(test)]
mod tests {
use super::*;
const LLD: &str = "\
VMA LMA Size Align Out In Symbol
2e0 2e0 1c 1 .interp
2e0 2e0 1c 1 <internal>:(.interp)
ce40 ce40 93d8 16 .rodata
ce40 ce40 d8 8 <internal>:(.rodata.cst8)
cf18 cf18 7 1 /w/target/deps/bench-1.o:(.rodata..Lanon.1.0)
cf50 cf50 0 1 /w/target/deps/bench-1.o:(.rodata.EMPTY)
cf50 cf50 0 1 soothfast::EMPTY
cf64 cf64 11 1 /w path/deps/bench-1.o:(.rodata..Lanon.1.1)
cf80 cf80 0 1 std::thread::Thread::new(x:(y))
cf90 cf90 10 8 <internal>:(.rodata.cst8)
";
const GNU: &str = "\
Memory Configuration
Linker script and memory map
.rodata 0x0000000000002000 0x30
*(.rodata .rodata.* .gnu.linkonce.r.*)
.rodata 0x0000000000002000 0x4 /usr/lib/crt1.o
0x0000000000002000 _IO_stdin_used
.rodata.foo 0x0000000000002008 0x10 foo.o
.rodata._ZN3bar17h0123456789abcdefE
0x0000000000002018 0x18 bar.o
.rodata.cst16
0x0000000000002030 0x20 baz.o
.data 0x0000000000003000 0x8
";
fn at(vma: u64, size: u64) -> Extent {
Extent {
vma,
size,
entry: 0,
}
}
fn pool(vma: u64, size: u64, entry: u64) -> Extent {
Extent { vma, size, entry }
}
#[test]
fn an_lld_map_yields_its_input_sections() {
assert_eq!(
parse(LLD).expect("parsed"),
[
at(0x2e0, 0x1c),
pool(0xce40, 0xd8, 8),
at(0xcf18, 7),
at(0xcf64, 0x11),
pool(0xcf90, 0x10, 8)
]
);
}
const MOLD: &str = "\
VMA Size Align Out In Symbol
0 64 8 EHDR
0xd850 16120 8 .rodata
0xd850 1 1 m.cgu.0.rcgu.o:(.rodata..Lanon.1)
0xd851 20 1 m.cgu.0.rcgu.o:(.rodata..Lanon.2)
0xd868 16 16 <internal>:(.rodata.cst16)
";
#[test]
fn a_mold_map_reads_its_hex_addresses_and_decimal_sizes() {
assert_eq!(
parse(MOLD).expect("parsed"),
[at(0xd850, 1), at(0xd851, 20), pool(0xd868, 16, 16)]
);
}
#[test]
fn a_gnu_ld_map_yields_its_input_sections_with_wrapped_names() {
assert_eq!(
parse(GNU).expect("parsed"),
[
at(0x2000, 4),
at(0x2008, 0x10),
at(0x2018, 0x18),
pool(0x2030, 0x20, 16)
]
);
}
#[test]
fn an_unknown_map_format_yields_nothing() {
assert_eq!(parse("not a map\n"), None);
assert_eq!(parse(""), None);
}
#[test]
fn extents_round_trip_through_their_stored_form() {
let extents = vec![at(0x2e0, 0x1c), at(0xcf18, 7)];
assert_eq!(decode(&encode(&extents)), extents);
}
}