use super::payload::{DebugImage, Frame};
#[derive(Debug, Clone)]
pub struct LoadedModule {
pub base: u64,
pub end: u64,
pub image: DebugImage,
pub reportable: bool,
}
fn debug_id_from_gnu_build_id(build_id: &[u8]) -> Option<String> {
if build_id.is_empty() {
return None;
}
let mut data = [0u8; 16];
let len = build_id.len().min(16);
data[..len].copy_from_slice(&build_id[..len]);
if cfg!(target_endian = "little") {
data[0..4].reverse();
data[4..6].reverse();
data[6..8].reverse();
}
Some(uuid::Uuid::from_bytes(data).to_string())
}
fn guid_le_to_uuid(mut data: [u8; 16]) -> String {
data[0..4].reverse();
data[4..6].reverse();
data[6..8].reverse();
uuid::Uuid::from_bytes(data).to_string()
}
fn debug_id_for(id: &findshlibs::SharedLibraryId) -> Option<String> {
use findshlibs::SharedLibraryId;
match id {
SharedLibraryId::GnuBuildId(bytes) => debug_id_from_gnu_build_id(bytes),
SharedLibraryId::Uuid(bytes) => {
Some(uuid::Uuid::from_bytes(*bytes).to_string().to_uppercase())
}
SharedLibraryId::PdbSignature(guid, age) => {
let uuid = guid_le_to_uuid(*guid);
Some(if *age > 0 {
format!("{uuid}-{age:x}")
} else {
uuid
})
}
_ => None,
}
}
fn native_image_type() -> &'static str {
if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
"macho"
} else if cfg!(target_os = "windows") {
"pe"
} else {
"elf"
}
}
fn normalize_arch(arch: &str) -> Option<String> {
let normalized = match arch {
"aarch64" => "arm64",
other => other,
};
(!normalized.is_empty()).then(|| normalized.to_string())
}
pub fn collect_loaded_modules() -> Vec<LoadedModule> {
use findshlibs::{IterationControl, SharedLibrary, TargetSharedLibrary};
let mut modules: Vec<LoadedModule> = Vec::new();
TargetSharedLibrary::each(|shlib| {
let base = shlib.actual_load_addr().0 as u64;
let size = shlib.len() as u64;
let name = shlib.name().to_string_lossy().into_owned();
let code_file = if name.is_empty() {
std::env::current_exe()
.ok()
.map(|p| p.to_string_lossy().into_owned())
} else {
Some(name)
};
let debug_id = shlib
.debug_id()
.as_ref()
.and_then(debug_id_for)
.unwrap_or_default();
let reportable = !debug_id.is_empty();
modules.push(LoadedModule {
base,
end: base.saturating_add(size),
image: DebugImage {
debug_id,
image_addr: format!("0x{base:x}"),
image_size: Some(size),
image_vmaddr: Some(format!("0x{:x}", shlib.stated_load_addr().0 as u64)),
code_file,
image_type: Some(native_image_type().to_string()),
arch: normalize_arch(std::env::consts::ARCH),
},
reportable,
});
IterationControl::Continue
});
modules.sort_by_key(|m| m.base);
modules
}
pub fn find_module(modules: &[LoadedModule], addr: u64) -> Option<&LoadedModule> {
let idx = modules.partition_point(|m| m.base <= addr);
let module = modules[..idx].last()?;
(addr < module.end).then_some(module)
}
pub fn referenced_images(modules: &[LoadedModule], frames: &[Frame]) -> Vec<DebugImage> {
modules
.iter()
.filter(|m| m.reportable)
.filter(|m| {
frames
.iter()
.any(|f| f.image_addr.as_deref() == Some(m.image.image_addr.as_str()))
})
.map(|m| m.image.clone())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn elf_build_id_derives_the_swapped_lowercase_uuid() {
let build_id: Vec<u8> = (1u8..=20).collect();
let got = debug_id_from_gnu_build_id(&build_id).expect("20-byte build id derives");
let want = if cfg!(target_endian = "little") {
"04030201-0605-0807-090a-0b0c0d0e0f10"
} else {
"01020304-0506-0708-090a-0b0c0d0e0f10"
};
assert_eq!(got, want);
assert_eq!(got, got.to_lowercase(), "ELF debug ids are lowercase");
}
#[test]
fn a_short_elf_build_id_zero_pads_before_the_swap() {
let build_id: Vec<u8> = (1u8..=8).collect();
let got = debug_id_from_gnu_build_id(&build_id).expect("8-byte build id derives");
let want = if cfg!(target_endian = "little") {
"04030201-0605-0807-0000-000000000000"
} else {
"01020304-0506-0708-0000-000000000000"
};
assert_eq!(got, want);
}
#[test]
fn an_empty_elf_build_id_has_no_debug_id() {
assert_eq!(debug_id_from_gnu_build_id(&[]), None);
}
#[test]
fn macho_uuid_is_uppercase() {
let bytes: [u8; 16] = [
0x67, 0xe9, 0x24, 0x7c, 0x81, 0x4e, 0x39, 0x2b, 0xa0, 0x27, 0xdb, 0xde, 0x67, 0x48,
0xfc, 0xbf,
];
let got = debug_id_for(&findshlibs::SharedLibraryId::Uuid(bytes)).expect("derives");
assert_eq!(got, "67E9247C-814E-392B-A027-DBDE6748FCBF");
assert_ne!(
got,
got.to_lowercase(),
"a lowercase Mach-O id never symbolicates"
);
}
#[test]
fn pdb_signature_with_an_age_appends_it_in_hex() {
let guid: [u8; 16] = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10,
];
let got = debug_id_for(&findshlibs::SharedLibraryId::PdbSignature(guid, 27)).expect("id");
assert_eq!(got, "04030201-0605-0807-090a-0b0c0d0e0f10-1b");
}
#[test]
fn pdb_signature_with_age_zero_has_no_suffix() {
let guid: [u8; 16] = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10,
];
let got = debug_id_for(&findshlibs::SharedLibraryId::PdbSignature(guid, 0)).expect("id");
assert_eq!(got, "04030201-0605-0807-090a-0b0c0d0e0f10");
}
#[test]
fn a_pe_timestamp_signature_yields_no_debug_id() {
assert_eq!(
debug_id_for(&findshlibs::SharedLibraryId::PeSignature(0xdead, 0x1000)),
None
);
}
fn module(base: u64, end: u64, debug_id: &str) -> LoadedModule {
LoadedModule {
base,
end,
image: DebugImage {
debug_id: debug_id.to_string(),
image_addr: format!("0x{base:x}"),
image_size: Some(end - base),
image_vmaddr: Some("0x0".into()),
code_file: Some(format!("mod{base}")),
image_type: Some("elf".into()),
arch: Some("x86_64".into()),
},
reportable: !debug_id.is_empty(),
}
}
#[test]
fn find_module_answers_below_inside_in_a_gap_and_past_the_end() {
let modules = vec![module(0x1000, 0x2000, "a"), module(0x3000, 0x4000, "b")];
assert!(find_module(&modules, 0x0fff).is_none(), "below the first");
assert_eq!(
find_module(&modules, 0x1500).map(|m| m.base),
Some(0x1000),
"inside the first"
);
assert!(find_module(&modules, 0x2500).is_none(), "in the gap");
assert_eq!(
find_module(&modules, 0x3000).map(|m| m.base),
Some(0x3000),
"the base address is inside"
);
assert!(find_module(&modules, 0x4000).is_none(), "end is exclusive");
assert!(find_module(&modules, 0x9999).is_none(), "past the end");
assert!(find_module(&[], 0x1000).is_none(), "empty list");
}
#[test]
fn referenced_images_drops_unreferenced_and_undebuggable_images() {
let modules = vec![
module(0x1000, 0x2000, "aaa"),
module(0x3000, 0x4000, "bbb"),
module(0x5000, 0x6000, ""),
];
let frames = vec![
Frame::address_only(Some("0x1100".into()), Some("0x1000".into())),
Frame::address_only(Some("0x5100".into()), Some("0x5000".into())),
];
let images = referenced_images(&modules, &frames);
assert_eq!(images.len(), 1, "got: {images:?}");
assert_eq!(images[0].debug_id, "aaa");
}
#[test]
fn referenced_images_preserves_load_order() {
let modules = vec![module(0x1000, 0x2000, "aaa"), module(0x3000, 0x4000, "bbb")];
let frames = vec![
Frame::address_only(Some("0x3100".into()), Some("0x3000".into())),
Frame::address_only(Some("0x1100".into()), Some("0x1000".into())),
];
let ids: Vec<_> = referenced_images(&modules, &frames)
.into_iter()
.map(|i| i.debug_id)
.collect();
assert_eq!(ids, vec!["aaa", "bbb"]);
}
#[test]
fn arch_is_normalized_to_the_symbolication_vocabulary() {
assert_eq!(normalize_arch("aarch64").as_deref(), Some("arm64"));
assert_eq!(normalize_arch("x86_64").as_deref(), Some("x86_64"));
assert_eq!(normalize_arch("").as_deref(), None);
}
#[test]
fn collect_loaded_modules_yields_a_sorted_self_consistent_list() {
let modules = collect_loaded_modules();
assert!(
modules.windows(2).all(|w| w[0].base <= w[1].base),
"modules must be sorted by load address"
);
for m in &modules {
assert_eq!(
m.reportable,
!m.image.debug_id.is_empty(),
"reportable must track debug_id presence"
);
assert_eq!(m.image.image_addr, format!("0x{:x}", m.base));
assert!(m.end >= m.base, "a module cannot end before it starts");
}
}
}