diff --git a/one_collect/src/helpers/exporting/mappings.rs b/one_collect/src/helpers/exporting/mappings.rs index 45b1137d..535ca6ff 100644 --- a/one_collect/src/helpers/exporting/mappings.rs +++ b/one_collect/src/helpers/exporting/mappings.rs @@ -208,6 +208,20 @@ impl ExportMapping { unique_ips.sort(); sym_reader.reset(); + // When more than one symbol source is unioned onto the same mapping + // (e.g. ELF symtab/dynsym followed by the Go gopclntab), a function + // present in multiple sources must not be added twice. We detect this + // transparently: if the mapping already holds symbols from an earlier + // source, we de-duplicate by start address against them (and against + // symbols added during this pass); the first source to add a given + // start wins. Single-source callers add onto an empty mapping and take + // this cost only when the scenario actually requires it. + let mut seen: Option> = if initial_symbol_count > 0 { + Some(self.symbols.iter().map(|s| s.start()).collect()) + } else { + None + }; + loop { if !sym_reader.next() { break; @@ -230,6 +244,16 @@ impl ExportMapping { } } + // Skip duplicates (by start address) when de-duplicating across + // unioned symbol sources. + if add_sym { + if let Some(seen) = seen.as_mut() { + if !seen.insert(start_ip) { + add_sym = false; + } + } + } + if add_sym { let demangled_name = sym_reader.demangle(); let demangled_name = match &demangled_name { @@ -585,8 +609,52 @@ mod tests { } #[test] - fn lookup() { - let mappings = vec!( + fn add_matching_symbols_dedup_test() { + // Unions two symbol sources (e.g. ELF symtab then Go pclntab) into one + // mapping. Functions that appear in both (same start) must not be + // duplicated; the first source wins. Distinct functions are all kept. + // Dedup is transparent: calling add_matching_symbols a second time on a + // mapping that already has symbols de-duplicates automatically. + let start = 4096; + let end = start + 4096; + let file_offset = 1024; + + let mut mapping = ExportMapping::new(0, 0, start, end, file_offset, false, 0, UnwindType::Prolog); + let mut strings = InternedStrings::new(32); + + // VA = file - 1024 + 4096. IPs chosen to hit each symbol's start. + let mut unique_ips = vec![4096u64, 4222, 4372]; + + // Source A (e.g. symtab): two symbols. + let source_a = vec![ + (1024u64, 1100u64, "shared_func".to_string()), // VA 4096-4172 + (1150, 1250, "only_in_a".to_string()), // VA 4222-4322 + ]; + let mut reader_a = MockSymbolReader::new(source_a); + mapping.add_matching_symbols(&mut unique_ips, &mut reader_a, &mut strings); + assert_eq!(2, mapping.symbols().len(), "source A adds 2"); + + // Source B (e.g. pclntab): re-lists shared_func (same start) plus a new one. + let source_b = vec![ + (1024u64, 1100u64, "shared_func".to_string()), // duplicate start -> skipped + (1300, 1400, "only_in_b".to_string()), // VA 4372-4472 -> added + ]; + let mut reader_b = MockSymbolReader::new(source_b); + mapping.add_matching_symbols(&mut unique_ips, &mut reader_b, &mut strings); + + // Total should be 3 (shared_func once, only_in_a, only_in_b). + assert_eq!(3, mapping.symbols().len(), "duplicate start must be deduped"); + + let names: Vec<&str> = mapping.symbols().iter() + .map(|s| strings.from_id(s.name_id()).unwrap()) + .collect(); + assert!(names.contains(&"shared_func")); + assert!(names.contains(&"only_in_a")); + assert!(names.contains(&"only_in_b")); + } + + #[test] + fn lookup() { let mappings = vec!( new_map(0, 0, 1023, 1), new_map(0, 2048, 3071, 3), new_map(0, 1024, 2047, 2), diff --git a/one_collect/src/helpers/exporting/os/linux.rs b/one_collect/src/helpers/exporting/os/linux.rs index c0cadd46..4fa3e479 100644 --- a/one_collect/src/helpers/exporting/os/linux.rs +++ b/one_collect/src/helpers/exporting/os/linux.rs @@ -28,7 +28,7 @@ use crate::os::system_page_size; use crate::ruwind::elf::*; use crate::ruwind::{ModuleAccessor, UnwindType}; -use symbols::{ElfSymbolReader, R2RLoadedLayoutSymbolTransformer, R2RMapSymbolReader}; +use symbols::{ElfSymbolReader, GoPclnTabSymbolReader, R2RLoadedLayoutSymbolTransformer, R2RMapSymbolReader}; use self::symbols::PerfMapSymbolReader; /* OS Specific Session Type */ @@ -68,7 +68,7 @@ trait ExportProcessLinuxExt { bin_path: &str, metadata: &ElfModuleMetadata, sym_types_requested: u32, - strings: &InternedStrings) -> Vec; + strings: &InternedStrings) -> Vec<(File, u32)>; fn check_candidate_symbol_file( &self, @@ -173,27 +173,56 @@ impl ExportProcessLinuxExt for ExportProcess { let sym_files = self.find_symbol_files( filename, metadata, - SYMBOL_TYPE_ELF_SYMTAB | SYMBOL_TYPE_ELF_DYNSYM, + SYMBOL_TYPE_ELF_SYMTAB | SYMBOL_TYPE_ELF_DYNSYM | SYMBOL_TYPE_GO_PCLNTAB, strings); if sym_files.is_empty() { debug!("add_matching_elf_symbols: no symbol files found for file={}", filename); } - for sym_file in sym_files { + // De-duplication across the ELF and Go passes below is handled + // transparently by add_matching_symbols (a function already added + // from one source is not re-added from another). + for (sym_file, sym_types) in sym_files { // Page align the values from the load header. let p_offset = metadata.p_offset() & page_mask; let p_vaddr = metadata.p_vaddr() & page_mask; - let load_header = ElfLoadHeader::new(p_offset, p_vaddr); - let mut sym_reader = ElfSymbolReader::new(sym_file, load_header, page_size); + let has_elf = sym_types & (SYMBOL_TYPE_ELF_SYMTAB | SYMBOL_TYPE_ELF_DYNSYM) != 0; + let has_go = sym_types & SYMBOL_TYPE_GO_PCLNTAB != 0; + + // Give each reader its own independent file handle. Only clone + // when both passes will run; otherwise move the handle directly. + let (elf_file, go_file): (Option, Option) = match (has_elf, has_go) { + (true, true) => match sym_file.try_clone() { + Ok(clone) => (Some(sym_file), Some(clone)), + Err(_) => (Some(sym_file), None), + }, + (true, false) => (Some(sym_file), None), + (false, true) => (None, Some(sym_file)), + (false, false) => (None, None), + }; + let map_mut = self.mappings_mut().get_mut(map_index).unwrap(); - map_mut.add_matching_symbols( - frames, - &mut sym_reader, - strings); + // ELF symtab/dynsym pass. + if let Some(elf_file) = elf_file { + let load_header = ElfLoadHeader::new(p_offset, p_vaddr); + let mut sym_reader = ElfSymbolReader::new(elf_file, load_header, page_size); + map_mut.add_matching_symbols(frames, &mut sym_reader, strings); + } + + // Go pclntab pass. Unioned with the ELF symbols above; pclntab + // fills functions the ELF symbol table lacks (or is the only + // source when the symbol table has been stripped). Functions + // shared with the ELF pass are dropped automatically. + if let Some(go_file) = go_file { + let load_header = ElfLoadHeader::new(p_offset, p_vaddr); + if let Some(mut go_reader) = GoPclnTabSymbolReader::new(go_file, load_header, page_size) { + map_mut.add_matching_symbols(frames, &mut go_reader, strings); + } + } } } } @@ -204,10 +233,31 @@ impl ExportProcessLinuxExt for ExportProcess { bin_path: &str, metadata: &ElfModuleMetadata, sym_types_requested: u32, - strings: &InternedStrings) -> Vec { + strings: &InternedStrings) -> Vec<(File, u32)> { let mut symbol_files = Vec::new(); let mut sym_types_found = 0u32; + // ELF symbol tables (symtab/dynsym) can be split into separate debug + // files, so a still-missing ELF type justifies continuing to walk the + // .dbg/.debug/debug-link chain. The Go pclntab is only ever carried in + // the binary itself (the first candidate), so it never justifies + // continuing — this keeps non-Go binaries from incurring extra + // symbol-file lookups. + let searchable_requested = sym_types_requested + & (SYMBOL_TYPE_ELF_SYMTAB | SYMBOL_TYPE_ELF_DYNSYM); + + // The search is complete once everything requested is found, or once + // every *searchable* requested type is found (any remaining requested + // type can't appear in a later file). The `searchable_requested != 0` + // guard prevents a request with no searchable types (e.g. Go-only) from + // stopping after an arbitrary first hit — it then stops only when the + // requested type is actually found. + let search_complete = |found: u32| -> bool { + (found & sym_types_requested) == sym_types_requested + || (searchable_requested != 0 + && (found & searchable_requested) == searchable_requested) + }; + // Keep evaluating symbol files until we find a matching one with a symtab. let mut path_buf = PathBuf::new(); @@ -216,9 +266,9 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -230,10 +280,10 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -244,9 +294,9 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -260,9 +310,9 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -281,9 +331,9 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -297,9 +347,9 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -313,9 +363,9 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -341,9 +391,9 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -358,9 +408,9 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -374,9 +424,9 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -391,9 +441,9 @@ impl ExportProcessLinuxExt for ExportProcess { if let Some((sym_file, types_found)) = self.check_candidate_symbol_file( metadata.build_id(), &path_buf) { - symbol_files.push(sym_file); + symbol_files.push((sym_file, types_found)); sym_types_found |= types_found; - if sym_types_found == sym_types_requested { + if search_complete(sym_types_found) { return symbol_files } } @@ -467,6 +517,10 @@ impl ExportProcessLinuxExt for ExportProcess { sym_flags |= SYMBOL_TYPE_ELF_DYNSYM; } + if has_go_pclntab(&mut reader) || has_go_build_info(&mut reader) { + sym_flags |= SYMBOL_TYPE_GO_PCLNTAB; + } + if sym_flags != 0 { return Some((reader, sym_flags)); } diff --git a/one_collect/src/helpers/exporting/symbols.rs b/one_collect/src/helpers/exporting/symbols.rs index 386b333b..b43945f4 100644 --- a/one_collect/src/helpers/exporting/symbols.rs +++ b/one_collect/src/helpers/exporting/symbols.rs @@ -3,7 +3,8 @@ use std::{fs::File, io::{BufRead, BufReader, Seek, SeekFrom}}; use std::collections::HashSet; -use crate::ruwind::elf::{ElfLoadHeader, ElfSymbol, ElfSymbolIterator}; +use crate::ruwind::elf::{ElfLoadHeader, ElfSymbol, ElfSymbolIterator, symbol_rva}; +use crate::ruwind::go_pclntab::GoPclnTab; use tracing::{info, trace, warn}; use crate::helpers::exporting::ExportMachine; @@ -410,6 +411,114 @@ pub struct PerfMapSymbolReader { done: bool, } +/// Symbol reader backed by a Go `gopclntab` line table. +/// +/// Recovers Go function names from the `.gopclntab` section for binaries whose +/// ELF symbol table has been stripped (e.g. `go build -ldflags "-s"`, as used +/// by Kubernetes). Function virtual addresses from the table are translated to +/// file RVAs via [`symbol_rva`], identical to [`ElfSymbolReader`], so the two +/// can be layered (unioned) for the same mapping. +pub(crate) struct GoPclnTabSymbolReader { + tab: GoPclnTab, + load_header: ElfLoadHeader, + page_mask: u64, + index: usize, + current_start: u64, + current_end: u64, + current_name: String, + valid: bool, +} + +impl GoPclnTabSymbolReader { + /// Attempts to build a reader from `file`. Returns `None` if the file has no + /// usable `.gopclntab` section or the table cannot be parsed (e.g. an + /// unsupported/older Go layout), so callers fall back to ELF symbols. + pub(crate) fn new( + mut file: File, + load_header: ElfLoadHeader, + system_page_size: u64) -> Option { + // Prefer the .gopclntab section; fall back to scanning for a stripped + // (section-header-removed) line table. + let (data, text_start) = crate::ruwind::elf::read_go_pclntab(&mut file) + .or_else(|| crate::ruwind::elf::recover_go_pclntab(&mut file))?; + let tab = GoPclnTab::parse(data, text_start)?; + + Some(Self { + tab, + load_header, + page_mask: crate::page_size_to_mask(system_page_size), + index: 0, + current_start: 0, + current_end: 0, + current_name: String::with_capacity(256), + valid: false, + }) + } +} + +impl ExportSymbolReader for GoPclnTabSymbolReader { + fn reset(&mut self) { + self.index = 0; + self.valid = false; + } + + fn next(&mut self) -> bool { + let p_vaddr = self.load_header.p_vaddr(); + + while self.index < self.tab.len() { + let i = self.index; + self.index += 1; + + if let Some((start_va, end_va, name)) = self.tab.func(i) { + // Guard against entries below the loaded segment (would + // underflow the RVA translation). + if start_va < p_vaddr { + continue; + } + + let start = symbol_rva(start_va, &self.load_header, self.page_mask); + + // pclntab end is the next function's entry (exclusive); make it + // inclusive to match ElfSymbol semantics and avoid overlapping + // the following function's start. + let end_exclusive = if end_va > start_va { end_va } else { start_va + 1 }; + let end = symbol_rva(end_exclusive, &self.load_header, self.page_mask) + .saturating_sub(1) + .max(start); + + self.current_start = start; + self.current_end = end; + self.current_name.clear(); + self.current_name.push_str(name); + self.valid = true; + return true; + } + } + + self.valid = false; + false + } + + fn start(&self) -> u64 { + if self.valid { self.current_start } else { 0 } + } + + fn end(&self) -> u64 { + if self.valid { self.current_end } else { 0 } + } + + fn name(&self) -> &str { + if self.valid { &self.current_name } else { "" } + } + + fn demangle(&mut self) -> Option { + // Go pclntab names are already in final, human-readable form and must + // never be passed through a C++/Rust demangler. + None + } +} + + impl PerfMapSymbolReader { pub fn new(file: File) -> Self { Self { @@ -986,11 +1095,113 @@ mod tests { } } + /// Cross-checks the Go pclntab reader against the ELF symbol reader on a + /// binary that has both. For every function name present in both sources, + /// the start address must match exactly — proving coordinate consistency so + /// the two can be unioned. Ignored by default; run with: + /// GO_PCLNTAB_BIN=/path/to/go-binary \ + /// cargo test --lib go_pclntab_matches_elf -- --ignored --nocapture #[test] - fn symbol_page_map() { - let mut map = SymbolPageMap::new(256); + #[ignore] + #[cfg(target_os = "linux")] + fn go_pclntab_matches_elf() { + use std::collections::HashMap; + + let path = std::env::var("GO_PCLNTAB_BIN").expect("set GO_PCLNTAB_BIN"); + let page = system_page_size(); + + // Collect ELF symtab/dynsym function starts by name. + let mut elf_starts: HashMap = HashMap::new(); + { + let file = File::open(&path).expect("open binary"); + let mut reader = ElfSymbolReader::new(file, ElfLoadHeader::new(0, 0), page); + reader.reset(); + while reader.next() { + elf_starts.insert(reader.name().to_string(), reader.start()); + } + } + assert!(!elf_starts.is_empty(), "binary should have ELF symbols for this test"); + + // Walk the Go pclntab reader and compare overlapping names. + let file = File::open(&path).expect("open binary"); + let mut go = GoPclnTabSymbolReader::new(file, ElfLoadHeader::new(0, 0), page) + .expect("binary should have a .gopclntab"); + go.reset(); + + let mut compared = 0usize; + let mut matched = 0usize; + let mut mismatched = 0usize; + let mut go_count = 0usize; + let mut examples: Vec = Vec::new(); + while go.next() { + go_count += 1; + if let Some(&elf_start) = elf_starts.get(go.name()) { + compared += 1; + assert!(go.start() <= go.end()); + if elf_start == go.start() { + matched += 1; + } else { + mismatched += 1; + if examples.len() < 5 { + examples.push(format!( + "{}: elf={:#x} go={:#x}", go.name(), elf_start, go.start())); + } + } + } + } + + println!( + "{}: elf_funcs={} go_funcs={} overlap={} matched={} mismatched={}", + path, elf_starts.len(), go_count, compared, matched, mismatched); + for e in &examples { + println!(" mismatch (expected for ABI wrappers): {}", e); + } + + // The vast majority of shared names must agree exactly. A small number + // legitimately differ because the ELF symtab and pclntab record distinct + // addresses for ABI wrappers that share a name; the union dedups by start + // so those become separate, correct entries. A coordinate bug would make + // essentially everything mismatch, which this guards against. + assert!(compared > 1000, "expected a large overlap, got {}", compared); + assert!( + matched * 100 >= compared * 95, + "expected >=95% start agreement, got {}/{}", matched, compared); + } + + /// Exercises the full GoPclnTabSymbolReader over committed fixtures, for both + /// the section path and the recovery (renamed-section) path. No network. + #[test] + #[cfg(target_os = "linux")] + fn go_pclntab_symbol_reader_fixtures() { + let page = system_page_size(); + for name in ["pclntab_stripped_symbols", "pclntab_section_stripped"] { + let path = std::env::current_dir().unwrap() + .join("../test/assets/go").join(name); + let file = File::open(&path).unwrap_or_else(|_| panic!("open fixture {}", name)); + + let mut reader = GoPclnTabSymbolReader::new(file, ElfLoadHeader::new(0, 0), page) + .unwrap_or_else(|| panic!("reader should build for {}", name)); + reader.reset(); + + let mut count = 0usize; + let mut found_main = false; + while reader.next() { + count += 1; + assert!(reader.start() <= reader.end()); + assert!(!reader.name().is_empty()); + assert!(reader.demangle().is_none(), "Go names must not be demangled"); + if reader.name() == "main.main" { + found_main = true; + } + } + assert!(count > 100, "{}: expected many symbols, got {}", name, count); + assert!(found_main, "{}: main.main should resolve", name); + } + } - map.mark_ip(0); + #[test] + fn symbol_page_map() { + let mut map = SymbolPageMap::new(256); map.mark_ip(0); map.mark_ip(257); map.mark_ip(1024); diff --git a/one_collect/src/ruwind/elf.rs b/one_collect/src/ruwind/elf.rs index 37c07044..bfe2fcc7 100644 --- a/one_collect/src/ruwind/elf.rs +++ b/one_collect/src/ruwind/elf.rs @@ -14,14 +14,20 @@ pub const ELF_MAGIC: [u8; 4] = [0x7F, b'E', b'L', b'F']; pub const SHT_PROGBITS: ElfWord = 1; pub const SHT_SYMTAB: ElfWord = 2; +const SHT_RELA: ElfWord = 4; pub const SHT_NOTE: ElfWord = 7; pub const SHT_NOBITS: ElfWord = 8; pub const SHT_DYNSYM: ElfWord = 11; +// Relative relocation types (used to recover Go runtime.text on PIE binaries). +const R_X86_64_RELATIVE: u32 = 8; +const R_AARCH64_RELATIVE: u32 = 1027; + // Symbol type flags that can be or'd together to keep track of // which types of symbols are present in a binary. pub const SYMBOL_TYPE_ELF_SYMTAB: u32 = 1; pub const SYMBOL_TYPE_ELF_DYNSYM: u32 = 2; +pub(crate) const SYMBOL_TYPE_GO_PCLNTAB: u32 = 4; pub struct ElfSymbol { start: u64, @@ -146,17 +152,13 @@ impl<'a> ElfSymbolIterator<'a> { section_offsets: Vec::new(), section_str_offset: 0, load_header: load_header, - system_page_mask: Self::page_size_to_mask(system_page_size), + system_page_mask: crate::page_size_to_mask(system_page_size), entry_count: 0, entry_index: 0, reset: true, } } - const fn page_size_to_mask(page_size: u64) -> u64 { - !((page_size - 1) as u64) - } - pub fn reset(&mut self) { let clear = |iterator: &mut ElfSymbolIterator| { iterator.sections.clear(); @@ -306,7 +308,7 @@ fn get_symbols32( Ok(()) } -fn symbol_rva( +pub(crate) fn symbol_rva( value: u64, load_header: &ElfLoadHeader, system_page_mask: u64) -> u64 { @@ -715,6 +717,414 @@ pub fn read_debug_link<'a>( Ok(None) } +/// The ELF section name that holds the Go line table when not stripped. +const GO_PCLNTAB_SECTION: &str = ".gopclntab"; + +/// Returns true if the ELF file contains a `.gopclntab` section. +/// +/// Used as a cheap, one-time probe to decide whether a binary carries a Go +/// line table without reading the (potentially large) section contents. +pub(crate) fn has_go_pclntab( + reader: &mut (impl Read + Seek)) -> bool { + let mut sections = Vec::new(); + let mut section_offsets = Vec::new(); + + if enum_section_metadata(reader, None, None, &mut sections).is_err() { + return false; + } + if get_section_offsets(reader, None, &mut section_offsets).is_err() { + return false; + } + + find_section_by_name(reader, §ions, §ion_offsets, GO_PCLNTAB_SECTION).is_some() +} + +/// Finds the metadata for a section by name, if present. +fn find_section_by_name<'a>( + reader: &mut (impl Read + Seek), + sections: &'a [SectionMetadata], + section_offsets: &[u64], + name: &str) -> Option<&'a SectionMetadata> { + for section in sections { + let mut name_buf: [u8; 256] = [0; 256]; + if let Ok(sec_name) = read_section_name(reader, section, section_offsets, &mut name_buf) { + if sec_name == name { + return Some(section); + } + } + } + None +} + +/// Reads the `.gopclntab` section bytes and resolves the Go runtime text start. +/// +/// Returns `(pclntab_bytes, text_start)` when the `.gopclntab` section is +/// present, or `None` otherwise. `text_start` is the virtual address of +/// `runtime.text` (the base the line table's PC offsets are relative to), which +/// is **not** always the `.text` section address — for cgo / externally linked +/// binaries the first part of `.text` holds C startup stubs and `runtime.text` +/// is offset past them. It is resolved by priority: +/// +/// 1. the `runtime.text` symbol (authoritative; present unless stripped), +/// 2. the pcHeader `textStart` field in the table (set for internally linked +/// binaries; zero/relocated otherwise), +/// 3. the `.text` section address (best-effort fallback). +/// +/// The returned function virtual addresses are in the same space as ELF symbol +/// `st_value`, so the caller applies the usual load-header relocation. +pub(crate) fn read_go_pclntab( + reader: &mut (impl Read + Seek)) -> Option<(Vec, u64)> { + let mut sections = Vec::new(); + let mut section_offsets = Vec::new(); + + enum_section_metadata(reader, None, None, &mut sections).ok()?; + get_section_offsets(reader, None, &mut section_offsets).ok()?; + + let pclntab = find_section_by_name(reader, §ions, §ion_offsets, GO_PCLNTAB_SECTION)?; + let pclntab_offset = pclntab.offset; + let pclntab_size = pclntab.size as usize; + let pclntab_vaddr = pclntab.address; + + // Optional .text address for the fallback. + let text_vaddr = find_section_by_name(reader, §ions, §ion_offsets, ".text") + .map(|s| s.address); + + let mut data = vec![0u8; pclntab_size]; + reader.seek(SeekFrom::Start(pclntab_offset)).ok()?; + reader.read_exact(&mut data).ok()?; + + let text_start = resolve_go_text_start(reader, &data, Some(pclntab_vaddr), text_vaddr)?; + + debug!( + "read_go_pclntab: pclntab size={} text_start={:#x}", + pclntab_size, text_start); + + Some((data, text_start)) +} + +/// Resolves the Go `runtime.text` virtual address (the line table PC base). +/// +/// `pclntab_vaddr`, when known, is the virtual address at which the line table +/// is mapped; it is used to look up the PIE relocation that supplies +/// `runtime.text` when the in-table field is unrelocated (zero). +fn resolve_go_text_start( + reader: &mut (impl Read + Seek), + pclntab: &[u8], + pclntab_vaddr: Option, + text_vaddr: Option) -> Option { + // 1. The runtime.text symbol is authoritative when present. + if let Some(value) = find_symbol_value(reader, "runtime.text") { + if value != 0 { + return Some(value); + } + } + + let ptr_size = if pclntab.len() >= 8 { pclntab[7] as usize } else { 0 }; + + // 2. The pcHeader textStart field (offset 8 + 2*ptrSize). Non-zero for + // internally linked binaries; zero/relocated for externally linked ones. + if ptr_size == 4 || ptr_size == 8 { + let off = 8 + 2 * ptr_size; + let header_text_start = match ptr_size { + 8 if pclntab.len() >= off + 8 => Some(u64::from_le_bytes( + pclntab[off..off + 8].try_into().unwrap())), + 4 if pclntab.len() >= off + 4 => Some(u32::from_le_bytes( + pclntab[off..off + 4].try_into().unwrap()) as u64), + _ => None, + }; + if let Some(ts) = header_text_start { + if ts != 0 { + return Some(ts); + } + } + + // 3. For PIE binaries the field above is zero and relocated at load. The + // relocation that targets the field carries runtime.text as its addend. + if let Some(base) = pclntab_vaddr { + let field_vaddr = base + off as u64; + if let Some(addend) = find_relative_reloc_addend(reader, field_vaddr) { + if addend != 0 { + return Some(addend); + } + } + } + } + + // 4. Fall back to the .text section address. + text_vaddr +} + +/// Finds the addend of an `R_*_RELATIVE` relocation targeting `target_vaddr`. +/// +/// Used to recover `runtime.text` for position-independent Go binaries, where +/// the pcHeader `textStart` field is zero on disk and supplied by this +/// relocation at load time. +fn find_relative_reloc_addend( + reader: &mut (impl Read + Seek), + target_vaddr: u64) -> Option { + let mut sections = Vec::new(); + if get_section_metadata(reader, None, SHT_RELA, &mut sections).is_err() { + return None; + } + + // Elf64_Rela: r_offset (u64), r_info (u64), r_addend (i64) = 24 bytes. + const RELA_SIZE: usize = 24; + let mut buf: Vec = Vec::new(); + + for section in §ions { + // Only 64-bit RELA is handled (sufficient for the supported targets). + if section.class != ELFCLASS64 || section.entry_size as usize != RELA_SIZE { + continue; + } + if section.size == 0 { + continue; + } + + buf.resize(section.size as usize, 0); + if reader.seek(SeekFrom::Start(section.offset)).is_err() { + continue; + } + if reader.read_exact(&mut buf).is_err() { + continue; + } + + let count = buf.len() / RELA_SIZE; + for i in 0..count { + let base = i * RELA_SIZE; + let r_offset = u64::from_le_bytes(buf[base..base + 8].try_into().unwrap()); + if r_offset != target_vaddr { + continue; + } + let r_info = u64::from_le_bytes(buf[base + 8..base + 16].try_into().unwrap()); + let r_type = (r_info & 0xffff_ffff) as u32; + if r_type == R_X86_64_RELATIVE || r_type == R_AARCH64_RELATIVE { + let r_addend = i64::from_le_bytes(buf[base + 16..base + 24].try_into().unwrap()); + return Some(r_addend as u64); + } + } + } + + None +} + + +/// Finds the value (`st_value`) of a named ELF symbol in `.symtab` or `.dynsym`. +/// +/// Unlike the function-symbol iterator this matches symbols of any type (e.g. +/// `runtime.text`, which is not `STT_FUNC`). Returns the first match. +fn find_symbol_value( + reader: &mut (impl Read + Seek), + name: &str) -> Option { + let mut sections = Vec::new(); + let mut section_offsets = Vec::new(); + + get_section_offsets(reader, None, &mut section_offsets).ok()?; + get_section_metadata(reader, None, SHT_SYMTAB, &mut sections).ok()?; + get_section_metadata(reader, None, SHT_DYNSYM, &mut sections).ok()?; + + let target = name.as_bytes(); + let mut name_buf = vec![0u8; target.len() + 1]; + + for section in §ions { + if section.entry_size == 0 { + continue; + } + let strtab_off = match section_offsets.get(section.link as usize) { + Some(off) => *off, + None => continue, + }; + + let count = section.size / section.entry_size; + for i in 0..count { + let entry_pos = section.offset + i * section.entry_size; + if reader.seek(SeekFrom::Start(entry_pos)).is_err() { + break; + } + + // st_name is the first 4 bytes in both ELF32 and ELF64. + let mut name_off_buf = [0u8; 4]; + if reader.read_exact(&mut name_off_buf).is_err() { + break; + } + let st_name = u32::from_le_bytes(name_off_buf) as u64; + + // st_value: ELF64 at entry+8 (u64); ELF32 at entry+4 (u32). + let st_value = match section.class { + ELFCLASS64 => { + if reader.seek(SeekFrom::Start(entry_pos + 8)).is_err() { + break; + } + let mut v = [0u8; 8]; + if reader.read_exact(&mut v).is_err() { + break; + } + u64::from_le_bytes(v) + } + _ => { + if reader.seek(SeekFrom::Start(entry_pos + 4)).is_err() { + break; + } + let mut v = [0u8; 4]; + if reader.read_exact(&mut v).is_err() { + break; + } + u32::from_le_bytes(v) as u64 + } + }; + + // Compare the name (exact length, NUL-terminated). + if reader.seek(SeekFrom::Start(strtab_off + st_name)).is_err() { + continue; + } + if reader.read_exact(&mut name_buf).is_err() { + continue; + } + if name_buf[target.len()] == 0 && &name_buf[..target.len()] == target { + return Some(st_value); + } + } + } + + None +} + +/// Returns true if the ELF file carries a Go build-info marker section. +/// +/// `.go.buildinfo` (and `.note.go.buildid`) are allocated and survive `strip`, +/// so they remain even when the `.gopclntab` section header has been removed. +/// This is used as a cheap gate before attempting the (more expensive) scan to +/// recover a stripped line table: non-Go binaries lack these markers and never +/// pay for the scan. +pub(crate) fn has_go_build_info( + reader: &mut (impl Read + Seek)) -> bool { + let mut sections = Vec::new(); + let mut section_offsets = Vec::new(); + + if enum_section_metadata(reader, None, None, &mut sections).is_err() { + return false; + } + if get_section_offsets(reader, None, &mut section_offsets).is_err() { + return false; + } + + find_section_by_name(reader, §ions, §ion_offsets, ".go.buildinfo").is_some() + || find_section_by_name(reader, §ions, §ion_offsets, ".note.go.buildid").is_some() +} + +/// Magic prefix bytes shared by the supported pclntab versions: the low byte +/// (`0xf0`/`0xf1`) varies, the upper three bytes are `0xff`. +const PCLNTAB_MAGIC_GO118: [u8; 4] = [0xf0, 0xff, 0xff, 0xff]; +const PCLNTAB_MAGIC_GO120: [u8; 4] = [0xf1, 0xff, 0xff, 0xff]; + +/// Recovers a Go line table whose `.gopclntab` section header has been stripped. +/// +/// Scans the read-only `PT_LOAD` segments for a supported pclntab magic, +/// validates and parses each candidate, and returns the first table that parses +/// cleanly together with its resolved `runtime.text`. Only 64-bit ELF is +/// supported; returns `None` otherwise (callers fall back to ELF symbols). +/// +/// Callers should gate this behind [`has_go_build_info`] and the absence of a +/// `.gopclntab` section so non-Go binaries never incur the scan. +pub(crate) fn recover_go_pclntab( + reader: &mut (impl Read + Seek)) -> Option<(Vec, u64)> { + use crate::ruwind::go_pclntab::GoPclnTab; + + // Determine class; only 64-bit recovery is supported. + reader.seek(SeekFrom::Start(0)).ok()?; + let ident = get_ident(reader).ok()?; + if ident[..4] != ELF_MAGIC || ident[EI_CLASS] != ELFCLASS64 { + return None; + } + + // Read the ELF header for program-header table location. The ElfHeader64 + // struct begins after the 16-byte e_ident, so seek past it first. + reader.seek(SeekFrom::Start(16)).ok()?; + let mut header = ElfHeader64::default(); + unsafe { + reader.read_exact( + slice::from_raw_parts_mut( + &mut header as *mut _ as *mut u8, + size_of::())).ok()?; + } + + // Optional .text address (fallback for text_start resolution). + let text_vaddr = { + let mut sections = Vec::new(); + let mut section_offsets = Vec::new(); + enum_section_metadata(reader, None, None, &mut sections).ok(); + get_section_offsets(reader, None, &mut section_offsets).ok(); + find_section_by_name(reader, §ions, §ion_offsets, ".text").map(|s| s.address) + }; + + // Collect read-only PT_LOAD segments (where the line table lives). + let mut segments: Vec<(u64, u64, u64)> = Vec::new(); // (p_offset, p_filesz, p_vaddr) + let mut ph_offset = header.e_phoff; + for _ in 0..header.e_phnum { + reader.seek(SeekFrom::Start(ph_offset)).ok()?; + let mut ph = ElfProgramHeader64::default(); + if get_program_header64(reader, &mut ph).is_err() { + break; + } + ph_offset += header.e_phentsize as u64; + + // Loadable, non-executable segment (the line table is read-only data, + // but on PIE binaries it lives in a writable data-rel-ro segment, so we + // only exclude executable segments here). + if ph.p_type == PT_LOAD + && (ph.p_flags & PF_X) == 0 + && ph.p_filesz > 0 { + segments.push((ph.p_offset, ph.p_filesz, ph.p_vaddr)); + } + } + + for (seg_offset, seg_filesz, seg_vaddr) in segments { + let mut bytes = vec![0u8; seg_filesz as usize]; + if reader.seek(SeekFrom::Start(seg_offset)).is_err() { + continue; + } + if reader.read_exact(&mut bytes).is_err() { + continue; + } + + // Scan for a candidate magic. + let mut i = 0usize; + while i + 8 <= bytes.len() { + let b = bytes[i]; + if (b == 0xf0 || b == 0xf1) + && bytes[i..i + 4] == (if b == 0xf0 { PCLNTAB_MAGIC_GO118 } else { PCLNTAB_MAGIC_GO120 }) + // Header bytes 4,5 must be zero; minLC and ptrSize must be sane. + && bytes[i + 4] == 0 + && bytes[i + 5] == 0 + && matches!(bytes[i + 6], 1 | 2 | 4) + && matches!(bytes[i + 7], 4 | 8) { + + let candidate_vaddr = seg_vaddr + i as u64; + let slice = bytes[i..].to_vec(); + let text_start = resolve_go_text_start(reader, &slice, Some(candidate_vaddr), text_vaddr) + .unwrap_or(0); + + if let Some(tab) = GoPclnTab::parse(slice, text_start) { + // Sanity-check: a real table has many functions whose first + // and last entries decode to non-empty names. + if tab.len() > 16 + && tab.func(0).map(|(_, _, n)| !n.is_empty()).unwrap_or(false) + && tab.func(tab.len() - 1).map(|(_, _, n)| !n.is_empty()).unwrap_or(false) { + debug!( + "recover_go_pclntab: recovered {} funcs at vaddr={:#x} text_start={:#x}", + tab.len(), candidate_vaddr, text_start); + // Return the recovered table bytes; parse consumed the + // first copy, so produce a fresh one for the caller. + return Some((bytes[i..].to_vec(), text_start)); + } + } + } + i += 1; + } + } + + None +} + pub fn get_load_header( reader: &mut (impl Read + Seek)) -> Result { reader.seek(SeekFrom::Start(0))?; diff --git a/one_collect/src/ruwind/go_pclntab.rs b/one_collect/src/ruwind/go_pclntab.rs new file mode 100644 index 00000000..b378d597 --- /dev/null +++ b/one_collect/src/ruwind/go_pclntab.rs @@ -0,0 +1,587 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! OS-agnostic parser for the Go runtime line table (`gopclntab`). +//! +//! The `gopclntab` blob is a Go *runtime* structure and is identical across +//! operating systems and object formats (ELF/PE/Mach-O). This module therefore +//! deals only in raw bytes: the caller is responsible for locating the blob +//! (e.g. the ELF `.gopclntab` section, or the PE `runtime.pclntab` symbol +//! range) and for supplying the `text_start` virtual address. The returned +//! function entry/end values are virtual addresses in the same space as the +//! object file's symbol values, so callers can translate them exactly like an +//! ELF symbol's `st_value`. +//! +//! Only the modern table layouts are supported: Go 1.18/1.19 (`ver118`, +//! magic `0xfffffff0`) and Go 1.20+ (`ver120`, magic `0xfffffff1`). These share +//! an identical on-disk layout and differ only by magic; one parser serves +//! every Go release from 1.18 through current. Any other / older / unknown +//! magic causes [`GoPclnTab::parse`] to return `None` so the caller can fall +//! back to ELF/PE symbol tables — it never panics. +//! +//! The field math mirrors Go's own `debug/gosym` package. + +use tracing::debug; + +/// Magic for the Go 1.18/1.19 pclntab layout. +const MAGIC_GO118: u32 = 0xfffffff0; +/// Magic for the Go 1.20+ pclntab layout. +const MAGIC_GO120: u32 = 0xfffffff1; + +/// A parsed Go line table, retaining the raw blob for lazy name lookups. +pub(crate) struct GoPclnTab { + data: Vec, + little_endian: bool, + text_start: u64, + /// Number of functions in the table (the functab has `nfunc + 1` entries). + nfunc: usize, + /// Offset within `data` of the function-name string table. + funcname_off: usize, + /// Offset within `data` of the functab (PC, funcoff pairs) and func structs. + functab_off: usize, +} + +impl GoPclnTab { + /// Attempts to parse `data` as a supported (Go 1.18+) pclntab. + /// + /// `text_start` is the virtual address of the runtime text (the `.text` + /// section address). Returns `None` for any unsupported magic or malformed + /// header rather than panicking, so callers can fall back gracefully. + pub(crate) fn parse( + data: Vec, + text_start: u64) -> Option { + // Need at least the fixed 8-byte header prefix. + if data.len() < 8 { + return None; + } + + // Detect magic + endianness. + let le_magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]); + let be_magic = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); + + let little_endian = match (le_magic, be_magic) { + (MAGIC_GO118, _) | (MAGIC_GO120, _) => true, + (_, MAGIC_GO118) | (_, MAGIC_GO120) => false, + _ => { + debug!("gopclntab: unsupported magic le={:#x} be={:#x}", le_magic, be_magic); + return None; + } + }; + + // Bytes 4 and 5 must be zero in a valid header. + if data[4] != 0 || data[5] != 0 { + return None; + } + + let _min_lc = data[6]; + let ptr_size = data[7] as usize; + if ptr_size != 4 && ptr_size != 8 { + return None; + } + + // Header words are ptr_size-wide integers starting at offset 8. + // word 0 = nfunc, word 2 = textStart, word 3 = funcnameOffset, + // word 7 = pclnOffset (functab + func structs). + let read_word = |w: usize| -> Option { + let pos = 8 + w * ptr_size; + read_uint(&data, pos, ptr_size, little_endian) + }; + + let nfunc = read_word(0)? as usize; + let funcname_off = read_word(3)? as usize; + let functab_off = read_word(7)? as usize; + + // Sanity-check offsets are within the blob. + if funcname_off >= data.len() || functab_off >= data.len() { + return None; + } + + // The functab holds (nfunc + 1) pairs of 4-byte fields (Go 1.18+). + let functab_bytes = (nfunc + 1) + .checked_mul(2)? + .checked_mul(FUNCTAB_FIELD_SIZE)?; + if functab_off.checked_add(functab_bytes)? > data.len() { + return None; + } + + Some(GoPclnTab { + data, + little_endian, + text_start, + nfunc, + funcname_off, + functab_off, + }) + } + + /// Number of functions described by the table. + pub(crate) fn len(&self) -> usize { + self.nfunc + } + + /// Returns true if the table describes no functions. + pub(crate) fn is_empty(&self) -> bool { + self.nfunc == 0 + } + + /// Returns the `i`th function as `(start_va, end_va, name)`. + /// + /// `start_va`/`end_va` are virtual addresses (text_start relative). `end_va` + /// is the entry of the next function (the functab's trailing sentinel for + /// the final function), matching how Go derives function extents. Returns + /// `None` if `i` is out of range or the entry is malformed. + pub(crate) fn func(&self, i: usize) -> Option<(u64, u64, &str)> { + if i >= self.nfunc { + return None; + } + + let entry_off = self.functab_field(2 * i)?; + let next_off = self.functab_field(2 * (i + 1))?; + let func_off = self.functab_field(2 * i + 1)? as usize; + + let start = self.text_start.checked_add(entry_off)?; + let end = self.text_start.checked_add(next_off)?; + + // The _func struct lives at functab_off + func_off. For Go 1.18+ the + // first field is a uint32 entry offset, so nameoff (field 1) is the + // uint32 at _func + 4. + let func_struct = self.functab_off.checked_add(func_off)?; + let name_off = read_u32(&self.data, func_struct.checked_add(4)?, self.little_endian)? as usize; + + let name = self.name_at(self.funcname_off.checked_add(name_off)?)?; + + Some((start, end, name)) + } + + /// Reads functab field `idx` (a 4-byte value), adding `text_start` is left + /// to the caller. Returns the raw offset value. + fn functab_field(&self, idx: usize) -> Option { + let pos = self.functab_off.checked_add(idx.checked_mul(FUNCTAB_FIELD_SIZE)?)?; + read_u32(&self.data, pos, self.little_endian).map(|v| v as u64) + } + + /// Returns the NUL-terminated UTF-8 string starting at `off` within `data`. + fn name_at(&self, off: usize) -> Option<&str> { + let bytes = self.data.get(off..)?; + let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); + std::str::from_utf8(&bytes[..end]).ok() + } +} + +/// Functab field width for Go 1.18+ layouts (uint32 offsets). +const FUNCTAB_FIELD_SIZE: usize = 4; + +/// Reads a `size`-byte unsigned integer (4 or 8) at `pos`. +fn read_uint( + data: &[u8], + pos: usize, + size: usize, + little_endian: bool) -> Option { + match size { + 4 => read_u32(data, pos, little_endian).map(|v| v as u64), + 8 => read_u64(data, pos, little_endian), + _ => None, + } +} + +fn read_u32( + data: &[u8], + pos: usize, + little_endian: bool) -> Option { + let b = data.get(pos..pos + 4)?; + let arr = [b[0], b[1], b[2], b[3]]; + Some(if little_endian { + u32::from_le_bytes(arr) + } else { + u32::from_be_bytes(arr) + }) +} + +fn read_u64( + data: &[u8], + pos: usize, + little_endian: bool) -> Option { + let b = data.get(pos..pos + 8)?; + let arr = [b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]; + Some(if little_endian { + u64::from_le_bytes(arr) + } else { + u64::from_be_bytes(arr) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Builds a minimal but valid Go 1.18+ pclntab for the given functions. + /// Each function is `(entry_offset, name)`; the table is laid out as: + /// [header][funcname table][functab + func structs] + fn build_pclntab( + magic: u32, + ptr_size: usize, + little_endian: bool, + text_start: u64, + funcs: &[(u32, &str)], + sentinel_end: u32) -> Vec { + let put_uint = |buf: &mut Vec, v: u64| { + if ptr_size == 8 { + if little_endian { buf.extend_from_slice(&v.to_le_bytes()); } + else { buf.extend_from_slice(&v.to_be_bytes()); } + } else { + let v = v as u32; + if little_endian { buf.extend_from_slice(&v.to_le_bytes()); } + else { buf.extend_from_slice(&v.to_be_bytes()); } + } + }; + let put_u32 = |buf: &mut Vec, v: u32| { + if little_endian { buf.extend_from_slice(&v.to_le_bytes()); } + else { buf.extend_from_slice(&v.to_be_bytes()); } + }; + + let nfunc = funcs.len(); + + // Build the funcname string table; record each name's offset. + let mut funcname_tab: Vec = Vec::new(); + let mut name_offsets: Vec = Vec::new(); + for (_, name) in funcs { + name_offsets.push(funcname_tab.len() as u32); + funcname_tab.extend_from_slice(name.as_bytes()); + funcname_tab.push(0); + } + + // Layout offsets within the final blob. + let header_len = 8 + 8 * ptr_size; // 8 header words is plenty. + let funcname_off = header_len; + let functab_off = funcname_off + funcname_tab.len(); + + // functab: (nfunc + 1) pairs of (entryoff, funcoff). The func structs + // are placed after the functab; funcoff is relative to functab_off. + let functab_entries = (nfunc + 1) * 2; + let functab_len = functab_entries * FUNCTAB_FIELD_SIZE; + // Each func struct: uint32 entryoff (field 0) + uint32 nameoff (field 1). + let func_struct_size = 8; + + let mut functab: Vec = Vec::new(); + // First emit the func structs region after the functab, computing + // their offsets relative to functab_off. + let mut func_offs: Vec = Vec::new(); + for idx in 0..nfunc { + func_offs.push((functab_len + idx * func_struct_size) as u32); + } + + // Emit functab pairs. + for idx in 0..nfunc { + put_u32(&mut functab, funcs[idx].0); // entryoff + put_u32(&mut functab, func_offs[idx]); // funcoff + } + // Trailing sentinel pair: max pc, dummy funcoff. + put_u32(&mut functab, sentinel_end); + put_u32(&mut functab, 0); + + // Emit func structs. + for idx in 0..nfunc { + put_u32(&mut functab, funcs[idx].0); // field 0: entry offset + put_u32(&mut functab, name_offsets[idx]); // field 1: name offset + } + + // Assemble header. + let mut buf: Vec = Vec::new(); + if little_endian { buf.extend_from_slice(&magic.to_le_bytes()); } + else { buf.extend_from_slice(&magic.to_be_bytes()); } + buf.push(0); // pad1 + buf.push(0); // pad2 + buf.push(1); // minLC + buf.push(ptr_size as u8); + // header words 0..8 + put_uint(&mut buf, nfunc as u64); // word 0: nfunc + put_uint(&mut buf, 0); // word 1: nfiles + put_uint(&mut buf, text_start); // word 2: textStart + put_uint(&mut buf, funcname_off as u64); // word 3: funcnameOffset + put_uint(&mut buf, 0); // word 4: cuOffset + put_uint(&mut buf, 0); // word 5: filetabOffset + put_uint(&mut buf, 0); // word 6: pctabOffset + put_uint(&mut buf, functab_off as u64); // word 7: pclnOffset + + debug_assert_eq!(buf.len(), header_len); + + buf.extend_from_slice(&funcname_tab); + buf.extend_from_slice(&functab); + buf + } + + #[test] + fn parses_go120_le_64() { + let text = 0x400000u64; + let funcs = [(0x1000u32, "main.main"), (0x1100u32, "runtime.mcall")]; + let blob = build_pclntab(MAGIC_GO120, 8, true, text, &funcs, 0x1200); + + let tab = GoPclnTab::parse(blob, text).expect("should parse"); + assert_eq!(tab.len(), 2); + + let (s0, e0, n0) = tab.func(0).unwrap(); + assert_eq!(s0, text + 0x1000); + assert_eq!(e0, text + 0x1100); + assert_eq!(n0, "main.main"); + + let (s1, e1, n1) = tab.func(1).unwrap(); + assert_eq!(s1, text + 0x1100); + assert_eq!(e1, text + 0x1200); + assert_eq!(n1, "runtime.mcall"); + + assert!(tab.func(2).is_none()); + } + + #[test] + fn parses_go118_magic() { + let text = 0x10000u64; + let funcs = [(0u32, "foo.Bar")]; + let blob = build_pclntab(MAGIC_GO118, 8, true, text, &funcs, 0x40); + let tab = GoPclnTab::parse(blob, text).expect("should parse"); + let (s, e, n) = tab.func(0).unwrap(); + assert_eq!(s, text); + assert_eq!(e, text + 0x40); + assert_eq!(n, "foo.Bar"); + } + + #[test] + fn parses_big_endian() { + let text = 0x80000u64; + let funcs = [(0x10u32, "pkg.fn")]; + let blob = build_pclntab(MAGIC_GO120, 8, false, text, &funcs, 0x20); + let tab = GoPclnTab::parse(blob, text).expect("should parse BE"); + let (_, _, n) = tab.func(0).unwrap(); + assert_eq!(n, "pkg.fn"); + } + + #[test] + fn preserves_unicode_middle_dot() { + // Older-style anonymous names embed U+00B7 (·); must be preserved. + let text = 0x1000u64; + let name = "type:.eq.runtime\u{00b7}foo"; + let funcs = [(0u32, name)]; + let blob = build_pclntab(MAGIC_GO120, 8, true, text, &funcs, 0x10); + let tab = GoPclnTab::parse(blob, text).unwrap(); + let (_, _, n) = tab.func(0).unwrap(); + assert_eq!(n, name); + } + + #[test] + fn rejects_unsupported_magic() { + // go1.16 magic (0xfffffffa) is intentionally unsupported. + let blob = build_pclntab(0xfffffffa, 8, true, 0x1000, &[(0u32, "x")], 0x10); + assert!(GoPclnTab::parse(blob, 0x1000).is_none()); + } + + #[test] + fn rejects_short_and_malformed() { + assert!(GoPclnTab::parse(vec![0u8; 4], 0).is_none()); + // Valid magic but non-zero pad bytes. + let mut blob = MAGIC_GO120.to_le_bytes().to_vec(); + blob.extend_from_slice(&[1, 0, 1, 8]); + blob.extend_from_slice(&[0u8; 64]); + assert!(GoPclnTab::parse(blob, 0).is_none()); + } + + #[test] + fn rejects_bad_ptr_size() { + let mut blob = MAGIC_GO120.to_le_bytes().to_vec(); + blob.extend_from_slice(&[0, 0, 1, 7]); // ptr_size = 7 invalid + blob.extend_from_slice(&[0u8; 64]); + assert!(GoPclnTab::parse(blob, 0).is_none()); + } + + // ----- Committed-fixture integration tests (no network) ----- + + /// Path to a checked-in Go test fixture under test/assets/go/. + #[cfg(target_os = "linux")] + fn fixture_path(name: &str) -> std::path::PathBuf { + std::env::current_dir().unwrap().join("../test/assets/go").join(name) + } + + /// The `.gopclntab` section path: a Go binary built with `-ldflags "-s -w"` + /// (symbol table stripped, line table retained) — the kubelet-like case. + #[test] + #[cfg(target_os = "linux")] + fn fixture_section_path() { + use std::fs::File; + use std::io::BufReader; + + let path = fixture_path("pclntab_stripped_symbols"); + let file = File::open(&path).expect("open fixture"); + let mut reader = BufReader::new(file); + + let (data, text) = crate::ruwind::elf::read_go_pclntab(&mut reader) + .expect("fixture has a .gopclntab section"); + let tab = GoPclnTab::parse(data, text).expect("parse fixture pclntab"); + + assert!(tab.len() > 100, "expected many funcs, got {}", tab.len()); + + let mut found_main = false; + let mut found_mcall = false; + for i in 0..tab.len() { + let (s, e, n) = tab.func(i).unwrap(); + assert!(s <= e); + match n { + "main.main" => found_main = true, + "runtime.mcall" => found_mcall = true, + _ => {} + } + } + assert!(found_main, "main.main should resolve"); + assert!(found_mcall, "runtime.mcall should resolve"); + } + + /// The recovery path: the same binary with its `.gopclntab` section header + /// renamed so the section lookup fails (the data remains in the segment) — + /// the containerd-like stripped-section case. + #[test] + #[cfg(target_os = "linux")] + fn fixture_recovery_path() { + use std::fs::File; + use std::io::BufReader; + + let path = fixture_path("pclntab_section_stripped"); + let file = File::open(&path).expect("open fixture"); + let mut reader = BufReader::new(file); + + // The section lookup must fail (the header was renamed)... + assert!( + crate::ruwind::elf::read_go_pclntab(&mut reader).is_none(), + "section lookup should fail for the recovery fixture"); + + // ...but the build-info marker remains and recovery succeeds. + assert!(crate::ruwind::elf::has_go_build_info(&mut reader)); + + let (data, text) = crate::ruwind::elf::recover_go_pclntab(&mut reader) + .expect("recovery should find the stripped line table"); + let tab = GoPclnTab::parse(data, text).expect("parse recovered pclntab"); + + assert!(tab.len() > 100, "expected many funcs, got {}", tab.len()); + let mut found_main = false; + for i in 0..tab.len() { + let (_, _, n) = tab.func(i).unwrap(); + if n == "main.main" { + found_main = true; + break; + } + } + assert!(found_main, "main.main should resolve via recovery"); + } + + /// Integration check against a real Go binary. Ignored by default; run with: /// GO_PCLNTAB_BIN=/path/to/bin GO_PCLNTAB_MIN=69763 \ + /// cargo test --lib real_binary -- --ignored --nocapture + #[test] + #[ignore] + fn real_binary() { + use std::fs::File; + use std::io::BufReader; + + let path = std::env::var("GO_PCLNTAB_BIN") + .expect("set GO_PCLNTAB_BIN"); + let min = std::env::var("GO_PCLNTAB_MIN") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(1); + + let file = File::open(&path).expect("open binary"); + let mut reader = BufReader::new(file); + + let (data, text) = crate::ruwind::elf::read_go_pclntab(&mut reader) + .expect("should find .gopclntab"); + let tab = GoPclnTab::parse(data, text).expect("should parse pclntab"); + + println!("{}: funcs={} text_start={:#x}", path, tab.len(), text); + assert!(tab.len() >= min, "expected >= {} funcs, got {}", min, tab.len()); + + // Spot-check a few well-known names resolve and look sane. + let mut samples = 0; + for i in 0..tab.len() { + let (s, e, n) = tab.func(i).unwrap(); + assert!(s <= e, "func {} start {:#x} > end {:#x}", i, s, e); + if n.contains("runtime.mcall") || n.contains("runtime.systemstack") || n == "main.main" { + println!(" {:#x}-{:#x} {}", s, e, n); + samples += 1; + } + } + assert!(samples > 0, "expected to find some well-known runtime symbols"); + } + + /// Validates stripped-section recovery against a real binary whose + /// `.gopclntab` section header has been removed (e.g. containerd). Ignored; + /// run with: + /// GO_PCLNTAB_BIN=/path/to/stripped GO_PCLNTAB_MIN=51884 \ + /// cargo test --lib recover_real_binary -- --ignored --nocapture + #[test] + #[ignore] + fn recover_real_binary() { + use std::fs::File; + use std::io::BufReader; + + let path = std::env::var("GO_PCLNTAB_BIN").expect("set GO_PCLNTAB_BIN"); + let min = std::env::var("GO_PCLNTAB_MIN") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(1); + + let file = File::open(&path).expect("open binary"); + let mut reader = BufReader::new(file); + + assert!( + crate::ruwind::elf::has_go_build_info(&mut reader), + "binary should carry a Go build-info marker"); + + let (data, text) = crate::ruwind::elf::recover_go_pclntab(&mut reader) + .expect("should recover a stripped .gopclntab"); + let tab = GoPclnTab::parse(data, text).expect("recovered table should parse"); + + println!("{}: recovered funcs={} text_start={:#x}", path, tab.len(), text); + assert!(tab.len() >= min, "expected >= {} funcs, got {}", min, tab.len()); + + // Names must be sane. + let (_, _, n0) = tab.func(0).unwrap(); + assert!(!n0.is_empty()); + } + + /// Corpus-sweep dumper. Reads the binary at `GO_PCLNTAB_BIN`, resolves its + /// pclntab exactly as `GoPclnTabSymbolReader` does (section first, then + /// stripped-section recovery), and writes one `"\t"` line + /// per function to `GO_PCLNTAB_OUT` (or stdout). The emitted entry addresses + /// are absolute virtual addresses (text_start relative), directly comparable + /// to `debug/gosym`'s `Func.Entry`, so an external diff can measure parity + /// against the Go standard library oracle across a large binary corpus. + /// Ignored by default; run with: + /// GO_PCLNTAB_BIN=/path/to/bin GO_PCLNTAB_OUT=/path/to/out.tsv \ + /// cargo test --lib dump_pclntab -- --ignored --nocapture + #[test] + #[ignore] + fn dump_pclntab() { + use std::fs::File; + use std::io::{BufReader, BufWriter, Write}; + + let path = std::env::var("GO_PCLNTAB_BIN").expect("set GO_PCLNTAB_BIN"); + let file = File::open(&path).expect("open binary"); + let mut reader = BufReader::new(file); + + // Same resolution order as GoPclnTabSymbolReader::new. + let (data, text) = crate::ruwind::elf::read_go_pclntab(&mut reader) + .or_else(|| crate::ruwind::elf::recover_go_pclntab(&mut reader)) + .expect("should find or recover a .gopclntab"); + let tab = GoPclnTab::parse(data, text).expect("should parse pclntab"); + + let mut out: Box = match std::env::var("GO_PCLNTAB_OUT") { + Ok(p) => Box::new(BufWriter::new(File::create(p).expect("create out"))), + Err(_) => Box::new(BufWriter::new(std::io::stdout())), + }; + + let mut n = 0usize; + for i in 0..tab.len() { + if let Some((start, _end, name)) = tab.func(i) { + writeln!(out, "{:#x}\t{}", start, name).expect("write"); + n += 1; + } + } + out.flush().expect("flush"); + eprintln!("dump_pclntab: {} funcs, text_start={:#x}", n, text); + } +} diff --git a/one_collect/src/ruwind/mod.rs b/one_collect/src/ruwind/mod.rs index 7677c52d..56678abb 100644 --- a/one_collect/src/ruwind/mod.rs +++ b/one_collect/src/ruwind/mod.rs @@ -18,6 +18,7 @@ use std::hash::{Hash, Hasher}; pub mod elf; pub mod dwarf; +pub mod go_pclntab; mod module; mod process; diff --git a/test/assets/go/pclntab_section_stripped b/test/assets/go/pclntab_section_stripped new file mode 100755 index 00000000..28da0c8b Binary files /dev/null and b/test/assets/go/pclntab_section_stripped differ diff --git a/test/assets/go/pclntab_stripped_symbols b/test/assets/go/pclntab_stripped_symbols new file mode 100755 index 00000000..44f443b2 Binary files /dev/null and b/test/assets/go/pclntab_stripped_symbols differ