diff --git a/Cargo.lock b/Cargo.lock index fed4bb10..9d10c2ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,10 +112,12 @@ dependencies = [ "chrono", "clap", "clap_complete", + "cpp_demangle", "csv", "csv-to-html", "ctor", "flate2", + "gimli", "include_directory", "indexmap", "infer", @@ -124,13 +126,18 @@ dependencies = [ "itoa", "lazy_static", "libc", + "linux-perf-data", + "linux-perf-event-reader", "log", "log4rs", + "lzma-rs", "nix", "numeric-sort", + "object", "perf-event2", "procfs", "regex", + "rustc-demangle", "rustix 0.38.28", "ryu", "serde", @@ -145,7 +152,7 @@ dependencies = [ "tar", "tdigest", "tempfile", - "thiserror", + "thiserror 1.0.69", "timerfd", "tokio", "vergen", @@ -693,6 +700,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -702,6 +718,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.3.2" @@ -1123,6 +1154,15 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "h2" version = "0.3.26" @@ -1395,6 +1435,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1422,6 +1471,12 @@ version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" + [[package]] name = "link-cplusplus" version = "1.0.9" @@ -1431,6 +1486,33 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-perf-data" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fab2acdab7737b0c69f86496747f251231321db9141827b53d964f25b150807d" +dependencies = [ + "byteorder", + "linear-map", + "linux-perf-event-reader", + "memchr", + "prost", + "prost-derive", + "thiserror 2.0.18", +] + +[[package]] +name = "linux-perf-event-reader" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8fc7e83909ea3b9e2784591655637d3401f2f16014f9d8d6e23ccd138e665f" +dependencies = [ + "bitflags 2.4.0", + "byteorder", + "memchr", + "thiserror 2.0.18", +] + [[package]] name = "linux-raw-sys" version = "0.3.6" @@ -1490,12 +1572,22 @@ dependencies = [ "serde-value", "serde_json", "serde_yaml", - "thiserror", + "thiserror 1.0.69", "thread-id", "typemap-ors", "winapi", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1658,6 +1750,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dcb6053ab98da45585315f79932c5c9821fab8efa4301c0d7b637c91630eb7" +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "flate2", + "memchr", + "ruzstd", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1804,6 +1907,28 @@ dependencies = [ "libc", ] +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "quick-xml" version = "0.26.0" @@ -1962,6 +2087,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc_version" version = "0.4.0" @@ -2047,6 +2178,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +[[package]] +name = "ruzstd" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f" +dependencies = [ + "twox-hash", +] + [[package]] name = "ryu" version = "1.0.11" @@ -2321,6 +2461,18 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "str_stack" version = "0.1.0" @@ -2389,7 +2541,7 @@ dependencies = [ "bitflags 1.3.2", "byteorder", "libc", - "thiserror", + "thiserror 1.0.69", "walkdir", ] @@ -2456,7 +2608,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -2470,6 +2631,17 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "thread-id" version = "4.2.2" @@ -2627,6 +2799,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + [[package]] name = "typemap-ors" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 2e8133fe..97869069 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,13 @@ sysctl = "*" perf-event2 = "0.7.2" procfs = "0.12.0" timerfd = "1.6.0" +linux-perf-data = "0.12" +linux-perf-event-reader = "0.10" +object = { version = "0.36", features = ["read"] } +cpp_demangle = "0.5" +rustc-demangle = "0.1" +gimli = { version = "0.33", default-features = false, features = ["read-core", "endian-reader"] } +lzma-rs = "0.3" [features] default = [] diff --git a/src/data/common/data_formats.rs b/src/data/common/data_formats.rs index c4d4cdc9..47ef70b5 100644 --- a/src/data/common/data_formats.rs +++ b/src/data/common/data_formats.rs @@ -201,10 +201,10 @@ impl Default for Profiler { } impl Profiler { - pub fn new(start_time_ms: i64, block_width_ms: u64) -> Self { + pub fn new(start_time_ms: i64) -> Self { Profiler { start_time_ms, - block_width_ms, + block_width_ms: BUCKET_WIDTH_MS, metadata: KeyValueData::default(), profiles: HashMap::new(), } diff --git a/src/data/java_profile.rs b/src/data/java_profile.rs index 023d57fe..f6bbc488 100644 --- a/src/data/java_profile.rs +++ b/src/data/java_profile.rs @@ -2,7 +2,7 @@ use crate::data::common::data_formats::{AperfData, Profiler, ProfilingData}; #[cfg(target_os = "linux")] use crate::data::common::utils::get_data_name_from_type; use crate::data::{Data, ProcessData}; -use crate::profiling::{jfr, Profile, ProfileGraph, BUCKET_WIDTH_MS}; +use crate::profiling::{jfr, Profile, ProfileGraph}; use crate::visualizer::ReportParams; use anyhow::Result; use log::error; @@ -343,7 +343,7 @@ impl CollectData for JavaProfileRaw { "{}-java-profile-{}-profiler-data.json", params.run_name, key )); - match jfr::jfr_to_profiler(&jfr_path, BUCKET_WIDTH_MS) { + match jfr::jfr_to_profiler(&jfr_path) { Ok(mut profiler) => { profiler.metadata = jfr::parse_jfr_metadata(&metadata_json); if let Ok(json) = serde_json::to_string(&profiler) { diff --git a/src/data/perf_profile.rs b/src/data/perf_profile.rs index 5fcdbf6b..b7a45459 100644 --- a/src/data/perf_profile.rs +++ b/src/data/perf_profile.rs @@ -1,24 +1,28 @@ -use crate::data::common::data_formats::{AperfData, TextData}; +use crate::data::common::data_formats::{AperfData, Profiler, TextData}; use crate::data::{Data, ProcessData}; use crate::visualizer::ReportParams; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::fs; -use std::{process::Child, sync::Mutex}; #[cfg(target_os = "linux")] use { crate::data::{CollectData, CollectorParams}, + crate::profiling::perf::parser::build_perf_profiler_data, crate::PDError, + chrono::Utc, log::{debug, error, warn}, nix::{sys::signal, unistd, unistd::Pid}, std::io::Write, std::process::{Command, Stdio}, + std::{process::Child, sync::Mutex}, }; pub const PERF_TOP_FUNCTIONS_FILE_NAME: &str = "top_functions"; +#[cfg(target_os = "linux")] lazy_static! { pub static ref PERF_CHILD: Mutex> = Mutex::new(None); + pub static ref PROFILE_START_TIME_MS: Mutex = Mutex::new(0); } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -72,6 +76,7 @@ impl CollectData for PerfProfileRaw { } } + *PROFILE_START_TIME_MS.lock().unwrap() = Utc::now().timestamp_millis(); match Command::new("perf") .stdout(Stdio::null()) .args([ @@ -130,6 +135,19 @@ impl CollectData for PerfProfileRaw { } Ok(_) => debug!("'perf record' executed successfully."), } + + // Parse raw Perf profile and build ProfilingData + let perf_profiler_data = build_perf_profiler_data( + ¶ms.data_file_path, + *PROFILE_START_TIME_MS.lock().unwrap(), + ); + let perf_profiler_data_path = params + .data_dir + .join(format!("{}-perf-profiler-data.json", params.run_name)); + if let Ok(json) = serde_json::to_string(&perf_profiler_data) { + fs::write(&perf_profiler_data_path, json)?; + } + let mut top_functions_file = fs::File::create(params.data_dir.join(PERF_TOP_FUNCTIONS_FILE_NAME))?; @@ -181,6 +199,14 @@ impl ProcessData for PerfProfile { params: ReportParams, _raw_data: Vec, ) -> Result { + // Deserialize the ProfilerData generated at the end of record. + // TODO: build ProfilingData from the ProfilerData and return it to replace top_function data. + let perf_profiler_data_path = params + .data_dir + .join(format!("{}-perf-profiler-data.json", params.run_name)); + let json_string = fs::read_to_string(perf_profiler_data_path)?; + let _perf_profiler_data = serde_json::from_str::(&json_string)?; + let mut text_data = TextData::default(); let file_loc = params.data_dir.join(PERF_TOP_FUNCTIONS_FILE_NAME); if file_loc.exists() { diff --git a/src/profiling/jfr/convert.rs b/src/profiling/jfr/convert.rs index a48c05ef..c86c7a93 100644 --- a/src/profiling/jfr/convert.rs +++ b/src/profiling/jfr/convert.rs @@ -281,7 +281,7 @@ fn parse_type(chars: &mut std::iter::Peekable) -> String { /// This function uses JfrReader to parse async-profiler generated JFR files into /// APerf Profiler. -pub fn jfr_to_profiler(path: &Path, block_width_ms: u64) -> Result { +pub fn jfr_to_profiler(path: &Path) -> Result { let mut reader = JfrReader::open(path.to_str().unwrap())?; let start_time_ms = reader.start_nanos / 1_000_000; @@ -301,7 +301,7 @@ pub fn jfr_to_profiler(path: &Path, block_width_ms: u64) -> Result { .cloned() .unwrap_or_default(); - let mut profiler = Profiler::new(start_time_ms, block_width_ms); + let mut profiler = Profiler::new(start_time_ms); // Cache: (method_id, frame_type) -> formatted frame string let mut frame_cache: HashMap<(i64, u8), String> = HashMap::new(); diff --git a/src/profiling/mod.rs b/src/profiling/mod.rs index 555bd22a..19ce8525 100644 --- a/src/profiling/mod.rs +++ b/src/profiling/mod.rs @@ -5,6 +5,8 @@ //! - [`jfr`] — JFR (Java Flight Recorder) binary format parser for async-profiler output. pub mod jfr; +pub mod perf; +pub mod symbols; pub const BUCKET_WIDTH_MS: u64 = 100; diff --git a/src/profiling/perf/mod.rs b/src/profiling/perf/mod.rs new file mode 100644 index 00000000..ea47ef02 --- /dev/null +++ b/src/profiling/perf/mod.rs @@ -0,0 +1,28 @@ +#![cfg(target_os = "linux")] + +pub mod parser; + +use crate::profiling::symbols::ResolvedSymbol; + +/// The information of a parsed Perf sample. +#[derive(Default, Debug)] +struct PerfSample { + /// The PID that this sample belongs to. + pid: i32, + /// The timestamp of the sample in nanoseconds from the EPOCH. + timestamp: u64, + /// The symbolicated call chain of the sample. + /// The order is from leaf to root. + call_chain: Vec>, +} + +// See below constants in https://github.com/torvalds/linux/blob/master/include/uapi/linux/perf_event.h + +/// It marks the case in a FORK event, where the child process has already exec'd and should +/// be treated as a regular process. +const PERF_RECORD_MISC_FORK_EXEC: u16 = 0x2000; +/// Sentinel inserted by the kernel into Perf sample callchain to mark that the following +/// are userspace addresses. +const PERF_CONTEXT_USER: u64 = 0xffff_ffff_ffff_fe00; +/// The bound of Perf Context. +const PERF_CONTEXT_MAX: u64 = 0xffff_ffff_ffff_f000; diff --git a/src/profiling/perf/parser.rs b/src/profiling/perf/parser.rs new file mode 100644 index 00000000..6812380e --- /dev/null +++ b/src/profiling/perf/parser.rs @@ -0,0 +1,308 @@ +use crate::data::common::data_formats::Profiler; +use crate::profiling::perf::{ + PerfSample, PERF_CONTEXT_MAX, PERF_CONTEXT_USER, PERF_RECORD_MISC_FORK_EXEC, +}; +use crate::profiling::symbols::symbol_resolver::SymbolResolver; +use crate::profiling::symbols::ResolvedSymbol; +use crate::profiling::ThreadState; +use anyhow::Result; +use linux_perf_data::{PerfFileReader, PerfFileRecord}; +use linux_perf_event_reader::{EventRecord, RawData, SampleRecord}; +use log::{debug, error}; +use std::env; +use std::fs::File; +use std::io::{BufReader, Write}; +use std::path::PathBuf; +use std::time::Instant; + +/// Parse the raw Perf profile and build the Profiler Data. +pub fn build_perf_profiler_data( + perf_data_path: &PathBuf, + profile_start_timestamp_ms: i64, +) -> Profiler { + debug!("Start parsing raw Perf profile..."); + + let mut profiler = Profiler::new(profile_start_timestamp_ms); + + let perf_parse_start_time = Instant::now(); + let perf_samples = match parse_perf_data(perf_data_path) { + Ok(perf_samples) => perf_samples, + Err(e) => { + error!("Error when parsing the raw Perf profile: {e}"); + return profiler; + } + }; + debug!( + "Raw Perf Data was parsed in {:?}", + perf_parse_start_time.elapsed() + ); + + // By default the timestamp of each sample is nanoseconds since the system booted, so we need to + // convert it into epoch + let system_boot_timestamp_ms = match procfs::boot_time() { + Ok(boot_time) => boot_time.timestamp_millis(), + Err(e) => { + error!("Failed to retrieve system boot timestamp: {e}"); + // In the rare case where the system boot timestamp cannot be retrieved, assume the + // first sample's epoch timestamp matches the profile start timestamp + perf_samples.get(0).map_or_else( + || 0, + |first_sample| { + profile_start_timestamp_ms - (first_sample.timestamp / 1_000_000) as i64 + }, + ) + } + }; + + // TODO: writing the stack output is for development only. Remove after verification step completes + let mut stack_output_file = perf_data_path + .parent() + .map(|parent_dir| { + File::create(PathBuf::from(parent_dir).join("parsed_perf_data.out")).unwrap() + }) + .unwrap(); + + let build_perf_profiler_data_start_time = Instant::now(); + let profile_type = "cpu"; + for perf_sample in perf_samples { + let mut frames: Vec = perf_sample + .call_chain + .iter() + .map(|resolved_symbol| { + resolved_symbol + .as_ref() + .map_or("[unknown]".to_string(), |s| s.name.to_string()) + }) + .collect(); + // Perf sample's call chain is from leaf to root, so reverse the frames + frames.reverse(); + + let sample_timestamp_ms = + system_boot_timestamp_ms + (perf_sample.timestamp / 1_000_000) as i64; + + // TODO: for development only. Remove after verification step completes. + writeln!( + stack_output_file, + "{}|{}|{}", + sample_timestamp_ms, + perf_sample.pid, + frames.join(";") + ); + + profiler.insert_stack( + profile_type, + sample_timestamp_ms, + ThreadState::None, + &frames, + 1, + ); + } + debug!( + "Perf Profiler Data was built in {:?}", + build_perf_profiler_data_start_time.elapsed() + ); + + profiler +} + +/// Parse every record in the raw Perf profile and collect all symbolicated samples. +fn parse_perf_data(perf_data_path: &PathBuf) -> Result> { + let perf_data_file = File::open(perf_data_path)?; + // Read an 1MB chunk at a time - the raw perf data typically has a size of several MB to ~500MB. + let buf_reader = BufReader::with_capacity(1 << 20, perf_data_file); + + let PerfFileReader { + mut perf_file, + mut record_iter, + } = PerfFileReader::parse_file(buf_reader)?; + + let arch = if let Ok(Some(arch)) = perf_file.arch() { + arch + } else { + env::consts::ARCH + }; + + let mut symbol_resolver = SymbolResolver::for_arch(arch); + + // Collect all ELF Build-IDs from the profile, which can be used to find the original + // build version of an ELF file. + match perf_file.build_ids() { + Ok(build_ids) => { + for dso_info in build_ids.values() { + let elf_file_path = String::from_utf8_lossy(&dso_info.path).to_string(); + let build_id = dso_info + .build_id + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); + symbol_resolver.add_build_id(&elf_file_path, build_id) + } + } + Err(e) => error!("Failed to read the Build-IDs from the Perf data: {e}"), + }; + + let mut perf_samples: Vec = Vec::new(); + + let mut num_record_parsing_errors: usize = 0; + while let Some(record) = record_iter.next_record(&mut perf_file)? { + match record { + PerfFileRecord::EventRecord { attr_index, record } => { + let parsed_record = match record.parse() { + Ok(parsed_record) => parsed_record, + Err(_) => { + num_record_parsing_errors += 1; + continue; + } + }; + + match parsed_record { + EventRecord::Mmap(mmap) => { + symbol_resolver.add_mmap( + mmap.pid, + mmap.address, + mmap.length, + mmap.page_offset, + rawdata_to_string(&mmap.path), + ); + } + EventRecord::Mmap2(mmap2) => { + symbol_resolver.add_mmap( + mmap2.pid, + mmap2.address, + mmap2.length, + mmap2.page_offset, + rawdata_to_string(&mmap2.path), + ); + } + EventRecord::Fork(fork) => { + if fork.ppid != fork.pid && (record.misc & PERF_RECORD_MISC_FORK_EXEC) == 0 + { + symbol_resolver.handle_forked_process_mmap(fork.ppid, fork.pid); + } + } + EventRecord::Sample(sample_record) => { + // For every PMU event that Perf record collects, it creates an entry in the + // attribute table. We only collect the cpu-clock event for now, so limit the + // scope to only handling one type of PMU event (the first one in the table). + if attr_index != 0 { + continue; + } + if let Some(perf_sample) = + handle_sample_event(&sample_record, &mut symbol_resolver) + { + perf_samples.push(perf_sample) + } + } + EventRecord::ContextSwitch(_) => { + // Not used for now, but it can be useful for off-CPU profiling in the future. + } + _ => {} + }; + } + _ => {} + } + } + + debug!("Number of Perf profile parsing errors: {num_record_parsing_errors}"); + + Ok(perf_samples) +} + +/// Handle a Perf sample event by symbolicating every frame in the call chain and performing +/// leaf frame recovery if needed to. +fn handle_sample_event( + sample_record: &SampleRecord, + symbol_resolver: &mut SymbolResolver, +) -> Option { + let pid = match sample_record.pid { + Some(pid) => pid, + None => return None, + }; + let timestamp = match sample_record.timestamp { + Some(timestamp) => timestamp, + None => return None, + }; + let call_chain = match sample_record.callchain { + Some(pid) => pid, + None => return None, + }; + + let mut leaf_frame_idx: Option = None; + let mut frame_addresses: Vec = Vec::new(); + let mut resolved_call_chain: Vec> = Vec::new(); + + // Refer to add_callchain_ip in + // https://github.com/torvalds/linux/blob/master/tools/perf/util/machine.c + for i in 0..call_chain.len() { + let frame_addr = match call_chain.get(i) { + Some(frame_addr) => frame_addr, + None => continue, + }; + // The userspace sentinel indicates that the following frames are all + // from the userspace, and the next frame is the leaf frame. + if frame_addr == PERF_CONTEXT_USER { + leaf_frame_idx = Some(frame_addresses.len()); + } + if frame_addr >= PERF_CONTEXT_MAX { + continue; + } + resolved_call_chain.push(symbol_resolver.resolve(pid, frame_addr)); + frame_addresses.push(frame_addr); + } + + let mut perf_sample = PerfSample { + pid, + timestamp, + call_chain: resolved_call_chain, + }; + + // On ARM, the caller of the leaf frame might not be in the call chain, due + // to the fact that the function invocation instruction (bl) saves the return + // address to the LR register, and it relies on the invoked function's prologue + // to save it to stack. Therefore, Perf, which relies on the stack to trace + // and create the call chain, might be missing the leaf frame's caller in the + // call chain. We need to check if the value of the leaf frame's LR register + // can be used to recover its caller frame. + if symbol_resolver.support_leaf_caller_recovery() { + let (lr, fp, sp) = match &sample_record.user_regs { + Some(user_regs) => (user_regs.get(30), user_regs.get(29), user_regs.get(31)), + None => return Some(perf_sample), + }; + // LR is crucial, while fp and sp are used as fallback. + if lr.is_none() { + return Some(perf_sample); + } + // When there are no userspace frames, there is nothing to recover. + if leaf_frame_idx.map_or(true, |idx| idx >= frame_addresses.len()) { + return Some(perf_sample); + } + let leaf_frame_idx = leaf_frame_idx.unwrap(); + let leaf_addr = frame_addresses[leaf_frame_idx]; + // If the leaf caller was successfully recovered, insert it right after + // the leaf frame in the call chain. + if let Some(leaf_caller_addr) = + symbol_resolver.recover_leaf_frame_caller(pid, leaf_addr, lr.unwrap(), fp, sp) + { + // Match perf's check: if (leaf_frame_caller && leaf_frame_caller != ip) + if leaf_caller_addr != 0 && leaf_caller_addr != leaf_addr { + perf_sample.call_chain.insert( + leaf_frame_idx + 1, + symbol_resolver.resolve(pid, leaf_caller_addr), + ); + } + } + } + + Some(perf_sample) +} + +fn rawdata_to_string(raw: &RawData) -> String { + let cow = raw.as_slice(); + let bytes: &[u8] = match &cow { + std::borrow::Cow::Borrowed(b) => b, + std::borrow::Cow::Owned(b) => b.as_slice(), + }; + // Strip trailing null bytes (perf data format null-terminates strings) + let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); + String::from_utf8_lossy(&bytes[..end]).to_string() +} diff --git a/src/profiling/symbols/elf_build_ids.rs b/src/profiling/symbols/elf_build_ids.rs new file mode 100644 index 00000000..720530d0 --- /dev/null +++ b/src/profiling/symbols/elf_build_ids.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; + +/// An ELF Build-ID is a unique identifier embedded in an ELF file by linkers, which is to +/// uniquely identify a specific build of a binary or shared library. Tools like Perf use +/// this ID to cache an ELF file, in case the original one being profiled has changed or +/// becomes inaccessible. +/// This struct caches an ELF file's Build-ID, and uses a Build-ID to look for the ELF file +/// in common places. +#[derive(Default)] +pub struct ElfBuildIds { + /// A map from an ELF file's path to its build id. + build_ids: HashMap, +} + +impl ElfBuildIds { + /// Store a pair of ELF file path and its Build-ID. + pub fn add_build_id(&mut self, elf_file_path: &str, build_id: String) { + self.build_ids.insert(elf_file_path.to_string(), build_id); + } + + /// Find the original build version of the ELF file using the ELF file's Build-ID. + pub fn find_original_elf_file(&self, elf_file_path: &str) -> Option { + let build_id = self.build_ids.get(elf_file_path)?; + + let home = std::env::var("HOME").ok()?; + + // The primary location where Perf writes the elf file to during capture: + // ~/.debug///elf + let mut original_elf_file_path = PathBuf::from(&home) + .join(".debug") + .join(elf_file_path.trim_start_matches('/')) + .join(build_id) + .join("elf"); + if let Ok(true) = fs::exists(&original_elf_file_path) { + return Some(original_elf_file_path); + } + + // Fallback location that Perf also populates: + // ~/.debug/.build-id///elf + if build_id.len() > 2 { + let (head, tail) = build_id.split_at(2); + original_elf_file_path = PathBuf::from(&home) + .join(".debug/.build-id") + .join(head) + .join(tail) + .join("elf"); + if let Ok(true) = fs::exists(&original_elf_file_path) { + return Some(original_elf_file_path); + } + // Some perf versions use the full hash as the filename rather than + // an 'elf' file inside a directory. + original_elf_file_path = PathBuf::from(&home) + .join(".debug/.build-id") + .join(head) + .join(tail); + if let Ok(true) = fs::exists(&original_elf_file_path) { + return Some(original_elf_file_path); + } + } + + None + } +} diff --git a/src/profiling/symbols/elf_symbols.rs b/src/profiling/symbols/elf_symbols.rs new file mode 100644 index 00000000..7acb38d6 --- /dev/null +++ b/src/profiling/symbols/elf_symbols.rs @@ -0,0 +1,663 @@ +use crate::profiling::symbols::demangle_symbol; +use crate::profiling::symbols::{ + prefer_second_symbol, resolve_symbol, RawSymbol, ResolvedSymbol, SymbolTableEntry, +}; +use anyhow::Result; +use object::{ + read::elf::SectionHeader, Architecture, Object, ObjectKind, ObjectSection, ObjectSegment, + ObjectSymbol, SymbolIterator, SymbolKind, SymbolScope, SymbolSection, +}; +use std::collections::HashMap; + +/// A single entry in the ELF file's program header table, indicating a segment +/// from [file_offset, file_offset + size) will be loaded at p_vaddr. +struct LoadSegment { + file_offset: u64, + size: u64, + p_vaddr: u64, +} + +/// Indicates what a CFI (Call Frame Information) in .eh_frame or .debug_frame sections says +/// about if the current value in the LR register is the correct return address, and can be +/// used for leaf frame recovery. +#[derive(Clone, Copy, PartialEq, Debug)] +enum CfiRule { + /// The current value in the LR register is the correct return address. + SameValue, + /// The current value in the LR register is not the correct return address, which + /// is instead saved on stack with some offsets. + Offset, + /// The current value in the LR register is not the correct return address for other + /// reasons that we do not handle. + Other, + /// There is no CFI rule (used for sentinel entries) + None, +} +impl CfiRule { + fn is_none(&self) -> bool { + *self == CfiRule::None + } +} + +/// An entry in the CFI rule table, which indicates that for any address that lies between +/// addr and the next entry's addr, the rule applies. +struct CfiRuleTableEntry { + addr: u64, + rule: CfiRule, +} + +/// Store all symbols retrieved from an ELF file to resolve a file offset. +#[derive(Default)] +pub struct ElfSymbols { + /// All symbols in the ELF file's .symtab and .dynsyms sections, sorted by p_vaddr for fast lookup. + symbol_table: Vec, + /// All load segments in the ELF file's program header, used to convert a file_offset into p_vaddr. + load_segments: Vec, + /// All CFI rules sorted by addr for fast look up during leaf frame recovery. + cfi_rule_table: Vec, + /// The name of the ELF file. + elf_filename: String, + /// True for ET_DYN (shared libraries and PIE executables). + is_dyn: bool, +} + +impl ElfSymbols { + /// Parse the contents of an ELF file and build the symbol table. + pub fn from_elf_data( + elf_data: &[u8], + elf_filename: String, + support_leaf_frame_recovery: bool, + ) -> Result { + let elf_obj = object::File::parse(elf_data)?; + let is_dyn = elf_obj.kind() == ObjectKind::Dynamic; + + // Populate and sort all load segments from the ELF file's program header + let mut load_segments: Vec = Vec::new(); + for segment in elf_obj.segments() { + load_segments.push(LoadSegment { + file_offset: segment.file_range().0, + size: segment.file_range().1, + p_vaddr: segment.address(), + }) + } + + let architecture = elf_obj.architecture(); + let is_x86 = matches!(architecture, Architecture::X86_64 | Architecture::I386); + + // On x86, check for .plt.sec — modern binaries compiled with -fcf-protection or + // linked with -z now emit the actual PLT entries in the .plt.sec section, + // while .plt becomes a pure resolver trampoline. + let (plt_section_name, lazy_plt) = + if is_x86 && elf_obj.section_by_name(".plt.sec").is_some() { + (".plt.sec", false) + } else { + (".plt", true) + }; + + // Retrieve all sizes of the PLT section. They will be used to filter out symbols when + // creating the symbol table from .symtab and .dynsym, and to parse the PLT section. + let (plt_section_header_size, plt_entry_size, plt_entries_offset, plt_section_size) = + get_plt_section_sizes(&elf_obj, plt_section_name, architecture) + .unwrap_or_else(|| (0, 0, 0, 0)); + let plt_address_range = (plt_entries_offset, plt_entries_offset + plt_section_size); + + // Collect all raw symbols from the .symtab and .dynsym section of the ELF file, and also + // create the index-symbol map for the two sections, to be used to create the PLT symbol table. + let mut raw_symbols: Vec = Vec::new(); + let mut symtab_index_name_map: HashMap = HashMap::new(); + let mut dynsym_index_name_map: HashMap = HashMap::new(); + collect_raw_symbols( + elf_obj.symbols(), + &mut raw_symbols, + &mut symtab_index_name_map, + plt_address_range, + ); + collect_raw_symbols( + elf_obj.dynamic_symbols(), + &mut raw_symbols, + &mut dynsym_index_name_map, + plt_address_range, + ); + // Fedora/RHEL/Ubuntu could strip .symtab in distro shared libs but create the .gnu_debugdata + // section, which is a LZMA-compressed embedded ELF file containing minimal debug symbols (mostly.symtab). + let mut gnu_debugdata_data: Option> = None; + if let Some(section) = elf_obj.section_by_name(".gnu_debugdata") { + if let Ok(compressed) = section.data() { + let mut decompressed = Vec::new(); + if lzma_rs::xz_decompress(&mut std::io::Cursor::new(compressed), &mut decompressed) + .is_ok() + { + gnu_debugdata_data = Some(decompressed); + } + } + } + let mini_elf_obj: Option = gnu_debugdata_data + .as_ref() + .and_then(|data| object::File::parse(data as &[u8]).ok()); + if let Some(ref mini_elf) = mini_elf_obj { + collect_raw_symbols( + mini_elf.symbols(), + &mut raw_symbols, + &mut symtab_index_name_map, + plt_address_range, + ); + } + let mut symbol_table = process_raw_symbols(raw_symbols); + + let plt_symbols = parse_plt( + &elf_obj, + &dynsym_index_name_map, + &symtab_index_name_map, + is_x86, + plt_section_header_size, + plt_entry_size, + plt_entries_offset, + plt_section_size, + lazy_plt, + ); + + // Create the final symbol tables + symbol_table.extend(plt_symbols); + symbol_table.sort_by_key(|symbol_table_entry| symbol_table_entry.addr); + + // If leaf frame recovery is enabled, build the CFI rule table + let cfi_rule_table = if support_leaf_frame_recovery { + create_cfi_rule_table(&elf_obj) + } else { + Vec::new() + }; + + Ok(ElfSymbols { + symbol_table, + load_segments, + elf_filename, + cfi_rule_table, + is_dyn, + }) + } + + /// Resolve a file_offset into a symbol in the symbol table. + pub fn resolve(&self, file_offset: u64) -> Option { + if self.symbol_table.is_empty() { + return None; + } + resolve_symbol( + self.file_offset_to_p_vaddr(file_offset)?, + &self.symbol_table, + &self.elf_filename, + ) + } + + /// Unwind the leaf caller frame by checking the corresponding CFI rule and deciding if the + /// value in the LR register is the correct address in the leaf caller frame. + pub fn recover_leaf_frame_caller( + &self, + leaf_file_offset: u64, + lr: u64, + fp: Option, + sp: Option, + ) -> Option { + if self.cfi_rule_table.is_empty() { + return None; + } + // Follows Perf's implementation in unwind-libdw.c, where if the ELF file is + // ET_DYN (shared libraries and PIE), it uses the file offset directly to query + // the CFI rule table. Otherwise, the p_vaddr should be used. + let cfi_table_lookup_key = if self.is_dyn { + leaf_file_offset + } else { + if let Some(p_vaddr) = self.file_offset_to_p_vaddr(leaf_file_offset) { + p_vaddr + } else { + leaf_file_offset + } + }; + + let matched_cfi_rule = match self + .cfi_rule_table + .binary_search_by_key(&cfi_table_lookup_key, |cfi_rule_table_entry| { + cfi_rule_table_entry.addr + }) { + Ok(i) => self.cfi_rule_table[i].rule, + Err(0) => CfiRule::None, + Err(i) => self.cfi_rule_table[i - 1].rule, + }; + match matched_cfi_rule { + // CFI rule says the value in LR is the correct return address of the leaf + CfiRule::SameValue => Some(lr), + // CFI rule says the value in LR is not the correct return address of the leaf + CfiRule::Offset | CfiRule::Other => None, + // No CFI rule coverage, so fall back to making decision using register values. + // It follows the same logics in the aarch64_unwind.c of elfutils, which is used + // by Perf. + CfiRule::None => { + // Missing fp/sp are treated as 0 — matching elfutils' aarch64_unwind, + // which sets `fp = 0; sp = 0` when getfunc fails. + let fp = fp.unwrap_or(0); + let sp = sp.unwrap_or(0); + if lr != 0 { + // fp = 0: the function did not set up a frame pointer, so naturally the + // LR value is not saved to the stack yet. + // fp + 16 > sp: the stack is moving toward the correct direction. + if fp == 0 || fp.wrapping_add(16) > sp { + return Some(lr); + } + } + None + } + } + } + + /// Use the load segment of the ELF file to convert a file_offset into p_vaddr. The number + /// of load segments is usually small (~4), so linear scan is faster. + fn file_offset_to_p_vaddr(&self, file_offset: u64) -> Option { + for load_segment in &self.load_segments { + if file_offset >= load_segment.file_offset + && file_offset < load_segment.file_offset + load_segment.size + { + return Some(file_offset - load_segment.file_offset + load_segment.p_vaddr); + } + } + None + } +} + +// ================================== .symtab and .dynsym section ================================== + +/// Iterate through all symbols retrieved from the ELF file (through symbol_iterator). Convert each of them +/// into RawSymbol and fill raw_symbols, which will be later processed to deduplicate symbols with the same +/// address and create the final symbol table. Also map every symbol name to index in symbol_index_name_map, +/// which is needed to populate the PLT symbol table. +fn collect_raw_symbols<'a>( + symbol_iterator: SymbolIterator<'a, 'a>, + raw_symbols: &mut Vec, + symbol_index_name_map: &mut HashMap, + plt_address_range: (u64, u64), +) { + for sym in symbol_iterator { + let symbol_name = match sym.name() { + Ok(symbol_name) => symbol_name, + Err(_) => continue, + }; + + symbol_index_name_map.insert(sym.index().0, symbol_name); + + // Mirrors perf's elf_sym__filter: + // accept if elf_sym__is_function() -- STT_FUNC / STT_GNU_IFUNC + // accept if elf_sym__is_object() -- STT_OBJECT + // accept if elf_sym__is_label() -- STT_NOTYPE with a proper name + // (not SHN_UNDEF / SHN_ABS) + // The `object` crate maps these to: + // SymbolKind::Text <- STT_FUNC / STT_GNU_IFUNC + // SymbolKind::Data <- STT_OBJECT / STT_COMMON + // SymbolKind::Unknown <- STT_NOTYPE + // Everything else (File, Section, Tls, ...) is dropped. + // See https://github.com/torvalds/linux/blob/master/tools/perf/util/symbol-elf.c + let is_function_symbol = sym.kind() == SymbolKind::Text; + let is_object_symbol = sym.kind() == SymbolKind::Data; + let is_label_symbol = + sym.kind() == SymbolKind::Unknown && sym.section() != SymbolSection::Absolute; + if symbol_name.is_empty() + || sym.address() == 0 + || sym.section() == SymbolSection::Undefined + || !(is_function_symbol || is_object_symbol || is_label_symbol) + { + continue; + } + // Skip dynamic symbols whose address falls in a PLT section. They will be handled + // later when parsing the PLT section. + if sym.address() >= plt_address_range.0 && sym.address() < plt_address_range.1 { + continue; + } + // Filter ARM/AARCH64 mapping symbols ($a, $d, $t, $x) + if symbol_name.starts_with('$') && symbol_name.len() >= 2 { + let second = symbol_name.as_bytes()[1]; + if second == b'a' || second == b'd' || second == b't' || second == b'x' { + continue; + } + } + raw_symbols.push(RawSymbol { + addr: sym.address(), + size: sym.size(), + name: symbol_name.to_string(), + is_weak: sym.is_weak(), + is_global: sym.scope() == SymbolScope::Dynamic || sym.scope() == SymbolScope::Linkage, + is_no_type: sym.kind() == SymbolKind::Unknown, + }); + } +} + +/// Create the final symbol table from the raw symbol tables, by sorting all raw symbols and +/// applying the deduplication logic. +fn process_raw_symbols(mut raw_symbols: Vec) -> Vec { + // Sort all raw symbols by address to apply deduplication logics. It also naturally makes the + // final symbol table sorted and enables its fast lookup. + raw_symbols.sort_by(|a, b| a.addr.cmp(&b.addr).then(a.name.cmp(&b.name))); + + let mut symbol_table: Vec = Vec::new(); + // For symbols with same address, apply a deduplication logic to choose the best one for this address. + let mut raw_symbols_iterator = raw_symbols.into_iter().peekable(); + while let Some(cur_raw_symbol) = raw_symbols_iterator.next() { + let cur_raw_symbol_addr = cur_raw_symbol.addr; + let mut chosen_raw_symbol = cur_raw_symbol; + while let Some(next_raw_symbol) = raw_symbols_iterator.peek() { + if next_raw_symbol.addr != cur_raw_symbol_addr { + break; + } + let next_raw_symbol = raw_symbols_iterator.next().unwrap(); + if prefer_second_symbol(&chosen_raw_symbol, &next_raw_symbol) { + chosen_raw_symbol = next_raw_symbol; + } + } + symbol_table.push(chosen_raw_symbol.to_symbol_table_entry()) + } + + symbol_table +} + +// =================================== .plt and .plt.sec section =================================== + +/// Parsing the PLT section of the ELF file into a symbol table. +/// The logics refer to dso__synthesize_plt_symbols in +/// https://github.com/torvalds/linux/blob/master/tools/perf/util/symbol-elf.c +fn parse_plt( + elf_obj: &object::File, + dynsym_index_name_map: &HashMap, + symtab_index_name_map: &HashMap, + is_x86: bool, + plt_section_header_size: u64, + plt_entry_size: u64, + mut plt_entries_offset: u64, + plt_section_size: u64, + lazy_plt: bool, +) -> Vec { + let mut entries = Vec::new(); + + // Find relocation section - prefer .rela.plt (SHT_RELA, 24-byte entries) but fall back to .rel.plt (SHT_REL, 16-byte entries). + let (relocation_section, use_rela, relocation_entry_size) = + if let Some(s) = elf_obj.section_by_name(".rela.plt") { + ( + s, + true, + get_elf_section_sh_entsize(elf_obj, ".rela.plt") as usize, + ) + } else if let Some(s) = elf_obj.section_by_name(".rel.plt") { + ( + s, + false, + get_elf_section_sh_entsize(elf_obj, ".rel.plt") as usize, + ) + } else { + return entries; + }; + let relocation_section_data = match relocation_section.data() { + Ok(d) => d, + Err(_) => return entries, + }; + let relocation_section_size = relocation_section_data.len(); + if relocation_entry_size == 0 || relocation_section_size % relocation_entry_size != 0 { + return entries; + } + + // Parse every relocation entry into (relocation_entry_offset, symbol_index). For every entry, + // relocation_entry_offset is used for sorting on X86 (see below), and symbol_index is to map + // it back to the symbol table. + let mut relocation_entries: Vec<(u64, u32)> = + Vec::with_capacity(relocation_section_size / relocation_entry_size); + let mut i = 0; + while i + relocation_entry_size <= relocation_section_size { + let relocation_entries_offset = + u64::from_le_bytes(relocation_section_data[i..i + 8].try_into().unwrap()); + let cur_relocation_entry = if use_rela { + u64::from_le_bytes(relocation_section_data[i + 8..i + 16].try_into().unwrap()) + } else { + u32::from_le_bytes(relocation_section_data[i + 8..i + 12].try_into().unwrap()) as u64 + }; + let symbol_index = if use_rela { + (cur_relocation_entry >> 32) as u32 + } else { + // 32-bit r_info layout: low 8 = type, high 24 = sym. + (cur_relocation_entry as u32) >> 8 + }; + relocation_entries.push((relocation_entries_offset, symbol_index)); + i += relocation_entry_size; + } + + if relocation_entries.is_empty() { + return entries; + } + + // According to Perf, X86 doesn't insert IFUNC relocations in the same order as PLT, so this is + // to get it back to order. Refer to the sort_rel function. + if is_x86 { + relocation_entries.sort_by_key(|&(off, _)| off); + } + + let num_relocation_entries = relocation_entries.len() as u64; + + // Matching Perf's implementation (search for lazy_plt), which assumes that if the number of relocation entries + // match exactly the number of PLT entries, the PLT section is not lazy and does not have a header. In this case, + // plt_entries_offset is already pointing at the first entry in the PLT section. Otherwise, move the offset over + // the header to point it to the first entry. + if lazy_plt && num_relocation_entries * plt_entry_size != plt_section_size { + plt_entries_offset += plt_section_header_size; + } + + // For every relocation entry, create a symbol entry in the symbol table. All the relocation entries should + // now be in the same order as PLT entries, so each entry can be mapped to the corresponding PLT entry's address. + for &(_, symbol_index) in relocation_entries.iter() { + let name: Option<&str> = if symbol_index != 0 { + dynsym_index_name_map + .get(&(symbol_index as usize)) + .or_else(|| symtab_index_name_map.get(&(symbol_index as usize))) + .copied() + } else { + None + }; + let symbol_name = match name { + Some(n) => { + let demangled_name = demangle_symbol(n); + let short = if let Some(p) = demangled_name.find('(') { + demangled_name[..p].to_string() + } else { + demangled_name + }; + format!("{}@plt", short) + } + None => format!("offset_{:#x}@plt", plt_entries_offset), + }; + entries.push(SymbolTableEntry { + addr: plt_entries_offset, + size: plt_entry_size, + name: symbol_name, + }); + plt_entries_offset += plt_entry_size; + } + + entries.sort_by_key(|entry| entry.addr); + entries +} + +/// Compute the sizes of components within the .plt or .plt.sec section: +/// - plt_header_size: the size of the header of the PLT section +/// - plt_entry_size: the size of an entry in the PLT section +/// - plt_entries_offset: the address of the first entry in the PLT section +/// - plt_section_size: the size of the PLT section +/// Refer to the get_plt_sizes function in +/// https://github.com/torvalds/linux/blob/master/tools/perf/util/symbol-elf.c +fn get_plt_section_sizes( + elf_obj: &object::File, + plt_section_name: &str, + architecture: Architecture, +) -> Option<(u64, u64, u64, u64)> { + let plt_section = elf_obj.section_by_name(plt_section_name)?; + + let plt_sh_entsize = get_elf_section_sh_entsize(elf_obj, plt_section_name); + + let (plt_header_size, plt_entry_size) = match architecture { + Architecture::Aarch64 => (32u64, 16u64), + Architecture::X86_64 | Architecture::I386 => { + let entry_size = if plt_sh_entsize == 8 || plt_sh_entsize == 16 { + plt_sh_entsize + } else if plt_section.align() == 8 { + 8 + } else { + 16 + }; + (entry_size, entry_size) + } + Architecture::Arm => (20u64, 12u64), + _ => (plt_sh_entsize, plt_sh_entsize), + }; + + Some(( + plt_header_size, + plt_entry_size, + plt_section.address(), + plt_section.size(), + )) +} + +/// Retrieve the sh_entsize field inside an ELF section, which describes the size of each entry +/// within the section's data. +fn get_elf_section_sh_entsize(elf_obj: &object::File, section_name: &str) -> u64 { + match elf_obj { + object::File::Elf64(elf_64_obj) => { + if let Some(elf_section) = elf_64_obj.section_by_name(section_name) { + elf_section + .elf_section_header() + .sh_entsize(elf_64_obj.endian()) + } else { + 0 + } + } + object::File::Elf32(elf_32_obj) => { + if let Some(elf_section) = elf_32_obj.section_by_name(section_name) { + elf_section + .elf_section_header() + .sh_entsize(elf_32_obj.endian()) as u64 + } else { + 0 + } + } + _ => 0, + } +} + +// ============================== .eh_frame and .debug_frame section =============================== + +/// Create the CFI rule table from the content of an ELF file, by parsing the .eh_frame +/// and .debug_frame sections. +fn create_cfi_rule_table(elf_obj: &object::File) -> Vec { + let mut cfi_rule_table: Vec = Vec::new(); + + let mut base_addresses = gimli::BaseAddresses::default(); + // When gimli parses an FDE entry in en_frame, it needs to parse encoded pointers sometimes + // requiring other ELF sections. Add the possibly required sections' base addresses so + // that it knows where to find them. + if let Some(text_section) = elf_obj.section_by_name(".text") { + base_addresses = base_addresses.set_text(text_section.address()) + }; + if let Some(got_section) = elf_obj.section_by_name(".got") { + base_addresses = base_addresses.set_got(got_section.address()) + }; + + // Collect CFI rules from the .eh_frame section (should present in all ELF files) + if let Some(eh_frame_section) = elf_obj.section_by_name(".eh_frame") { + base_addresses = base_addresses.set_eh_frame(eh_frame_section.address()); + if let Ok(eh_frame_section_data) = eh_frame_section.data() { + let mut eh_frame_unwind_section = + gimli::EhFrame::new(eh_frame_section_data, gimli::LittleEndian); + eh_frame_unwind_section.set_vendor(gimli::Vendor::AArch64); + collect_cfi_rules( + &eh_frame_unwind_section, + &base_addresses, + &mut cfi_rule_table, + ); + } + } + // Collect CFI rules from the .debug_frame section (will not be in stripped binaries) + if let Some(debug_frame_section) = elf_obj.section_by_name(".debug_frame") { + if let Ok(debug_frame_section_data) = debug_frame_section + .uncompressed_data() + .map(|d| d.into_owned()) + { + let mut debug_frame_unwind_section = + gimli::DebugFrame::new(&debug_frame_section_data, gimli::LittleEndian); + debug_frame_unwind_section.set_address_size(8); + debug_frame_unwind_section.set_vendor(gimli::Vendor::AArch64); + collect_cfi_rules( + &debug_frame_unwind_section, + &base_addresses, + &mut cfi_rule_table, + ); + } + } + + // Sort all CFI rules by address, and for each address only keep the first rule + // (the sentinel rules added will be overridden by real rules at the same address). + cfi_rule_table.sort_by(|a, b| { + a.addr + .cmp(&b.addr) + .then_with(|| a.rule.is_none().cmp(&b.rule.is_none())) + }); + cfi_rule_table.dedup_by_key(|cfi_rule_table_entry| cfi_rule_table_entry.addr); + + cfi_rule_table +} + +/// Parse all FDE (Frame Description Entry) entries in the section into CFI rules. +fn collect_cfi_rules<'a, S>( + section: &S, + base_addresses: &gimli::BaseAddresses, + cfi_rule_table: &mut Vec, +) where + S: gimli::UnwindSection>, + S::Offset: gimli::UnwindOffset, +{ + // All the FDE (Frame Description Entry) entries in .eh_frame and .debug_frame. Every FDE + // entry has CFI (Call Frame Information) that describes the rules. + let mut fde_entries = section.entries(base_addresses); + let mut unwind_context = gimli::UnwindContext::new(); + // The whole frame-unwinding logic is run on ARM only, so we only care + // about the LR (X30) register, which stores the return address. + let lr_register = gimli::Register(30); + + while let Ok(Some(fde_entry)) = fde_entries.next() { + if let gimli::CieOrFde::Fde(partial_fde) = fde_entry { + if let Ok(fde) = partial_fde.parse(S::cie_from_offset) { + let fde_start = fde.initial_address(); + let fde_end = fde_start + fde.len(); + + // Walk through the CFI rows in the FDE entry + if let Ok(mut cfi_rows_iter) = + fde.rows(section, base_addresses, &mut unwind_context) + { + while let Ok(Some(cfi_row)) = cfi_rows_iter.next_row() { + let addr = cfi_row.start_address(); + + let rule = match cfi_row.register(lr_register) { + Some(gimli::RegisterRule::SameValue) => CfiRule::SameValue, + Some(gimli::RegisterRule::Offset(_)) => CfiRule::Offset, + Some(_) => CfiRule::Other, + // When there are no explicit rules, use default CFI rules specified + // by the platform's ABI. It follows the definitions in aarch64_abi_cfi + // in aarch64_cfi.c of elfutils. + None => CfiRule::SameValue, + }; + + cfi_rule_table.push(CfiRuleTableEntry { addr, rule }); + } + } + + // Add sentinel entry to the table that marks the end of an FDE entry, so for all the + // address starting from the fde_end to the next valid CFI-covered address, there + // is no valid CFI rule. + cfi_rule_table.push(CfiRuleTableEntry { + addr: fde_end, + rule: CfiRule::None, + }) + } + } + } +} diff --git a/src/profiling/symbols/jit_symbols.rs b/src/profiling/symbols/jit_symbols.rs new file mode 100644 index 00000000..ad585e68 --- /dev/null +++ b/src/profiling/symbols/jit_symbols.rs @@ -0,0 +1,100 @@ +use crate::profiling::symbols::{resolve_symbol, ResolvedSymbol, SymbolTableEntry}; +use anyhow::Result; +use log::debug; +use std::{ + fs::File, + io::{BufRead, BufReader}, + path::PathBuf, + process::{Command, Stdio}, +}; + +/// Store all JIT symbols created by application runtimes to resolve an address. +#[derive(Default)] +pub struct JitSymbols { + /// All JIT symbols sorted by addr for fast lookup. + symbol_table: Vec, +} + +impl JitSymbols { + /// Parse the JIT symbols in /tmp/perf-.map file. Application runtimes are responsible + /// for writing their symbol mappings to the file. + /// * For HotSpot JVM, this is done through the JVM argument -XX:+DumpPerfMapAtExit. However, + /// this flag only creates the perf maps after the JVM exits. Therefore, we run jcmd as a + /// fallback to genarate the perf maps when the JVM is still running. + /// * For V8, --perf-basic-prof writes perf maps continuously. + /// * For .NET, setting DOTNET_PerfMapEnabled=1 writes perf maps continuously. + /// + /// Refer to the dso__load_perf_map function in + /// https://github.com/torvalds/linux/blob/master/tools/perf/util/symbol.c + pub fn from_perf_map(pid: i32, is_hotspot_jvm: bool) -> Result { + let perf_map_file_path = PathBuf::from(format!("/tmp/perf-{}.map", pid)); + if !perf_map_file_path.exists() { + if is_hotspot_jvm { + create_hotspot_jvm_perf_map(pid); + } else { + return Ok(Self::default()); + } + } + + let perf_map_file = File::open(&perf_map_file_path)?; + let buf_reader = BufReader::new(perf_map_file); + + let mut symbol_table: Vec = Vec::new(); + + // Format: + for line in buf_reader.lines() { + let line = match line { + Ok(l) => l, + Err(_) => continue, + }; + let parts: Vec<&str> = line.splitn(3, ' ').collect(); + if parts.len() < 3 { + continue; + } + let addr_str = parts[0] + .strip_prefix("0x") + .or_else(|| parts[0].strip_prefix("0X")) + .unwrap_or(parts[0]); + let size_str = parts[1] + .strip_prefix("0x") + .or_else(|| parts[1].strip_prefix("0X")) + .unwrap_or(parts[1]); + let addr = u64::from_str_radix(addr_str, 16).unwrap_or(0); + if addr == 0 { + continue; + } + let size = u64::from_str_radix(size_str, 16).unwrap_or(0); + let name = parts[2].to_string(); + symbol_table.push(SymbolTableEntry { addr, size, name }); + } + + symbol_table.sort_by_key(|symbol_table_entry| symbol_table_entry.addr); + + Ok(JitSymbols { symbol_table }) + } + + /// Resolve an address into a symbol in the symbol table. + pub fn resolve(&self, addr: u64) -> Option { + if self.symbol_table.is_empty() { + return None; + } + resolve_symbol(addr, &self.symbol_table, "jit") + } +} + +/// Run `jcmd Compiler.perfmap` to ask a HotSpot JVM to dump /tmp/perf-.map. +/// Best-effort: if jcmd is not on PATH, the JVM has shut down, or the dump fails for +/// any reason, we silently continue — the caller will fall back to leaving frames +/// unresolved. +fn create_hotspot_jvm_perf_map(pid: i32) { + let result = Command::new("jcmd") + .args([&pid.to_string(), "Compiler.perfmap"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + match result { + Ok(s) if s.success() => debug!("jcmd Compiler.perfmap succeeded for pid {pid}"), + Ok(s) => debug!("jcmd Compiler.perfmap exited with {s} for pid {pid}"), + Err(e) => debug!("jcmd Compiler.perfmap failed for pid {pid}: {e}"), + } +} diff --git a/src/profiling/symbols/kernel_symbols.rs b/src/profiling/symbols/kernel_symbols.rs new file mode 100644 index 00000000..bf020d79 --- /dev/null +++ b/src/profiling/symbols/kernel_symbols.rs @@ -0,0 +1,256 @@ +use crate::profiling::symbols::{ + prefer_second_symbol, resolve_symbol, tail_symbol_bound, RawSymbol, ResolvedSymbol, + SymbolTableEntry, +}; +use anyhow::Result; +use std::fs; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; + +/// The information of a symbol retrieved from /proc/kallsyms, which, in addition to +/// the common raw symbol information, can have a designated Kernel module that it belongs to. +struct RawKernelSymbol { + raw_symbol: RawSymbol, + module: Option, +} +impl RawKernelSymbol { + fn to_symbol_table_entry(self) -> SymbolTableEntry { + let mut symbol_table_entry = self.raw_symbol.to_symbol_table_entry(); + // A Kernel symbol does not actually have len - the len in raw_symbol is pseudo and + // only used for deduplication (see below). + symbol_table_entry.size = 0; + symbol_table_entry + } +} + +/// An entry in /proc/modules, indicating the Kernel module that an address range belongs to. +struct ProcModuleEntry { + base_addr: u64, + end_addr: u64, + module_name: String, +} + +/// Store all Kernel symbols retrieved from /proc/kallsyms to resolve an address. +#[derive(Default)] +pub struct KernelSymbols { + symbol_table: Vec, +} + +impl KernelSymbols { + /// Parse the content of /proc/kallsyms to build the Kernel symbol table. + pub fn from_kallsyms() -> Result { + Ok(KernelSymbols { + symbol_table: process_raw_kernel_symbols( + collect_raw_kernel_symbols(PathBuf::from("/proc/kallsyms"))?, + load_proc_modules(), + ), + }) + } + + /// Resolve a Kernel address into a symbol. + pub fn resolve(&self, addr: u64) -> Option { + if self.symbol_table.is_empty() { + return None; + } + resolve_symbol(addr, &self.symbol_table, "kallsyms") + } +} + +/// Accepts the symbol type letters that can correspond to runtime code +/// or data we might sample: T/t, W/w, D/d, B/b, plus the handful of +/// special lowercase types perf also accepts (u, l, N, 1). +/// Refer to symbol_type__filter in +/// https://github.com/torvalds/linux/blob/master/tools/perf/util/symbol.c +fn is_valid_type_letter(t: char) -> bool { + let upper = t.to_ascii_uppercase(); + upper == 'T' + || upper == 'W' + || upper == 'D' + || upper == 'B' + || t == 'u' + || t == 'l' + || t == 'N' + || t == '1' +} + +/// Load /proc/modules so we can attribute a symbol to its module when +/// kallsyms has duplicates at the same address from different modules. +fn load_proc_modules() -> Vec { + let mut proc_module_entries: Vec = Vec::new(); + + if let Ok(proc_module_content) = fs::read_to_string("/proc/modules") { + for line in proc_module_content.lines() { + // Format: name size refcount deps state base_addr + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 6 { + continue; + } + let size: u64 = parts[1].parse().unwrap_or(0); + let base = parts[5].trim_start_matches("0x"); + let base_addr = u64::from_str_radix(base, 16).unwrap_or(0); + if base_addr == 0 || size == 0 { + continue; + } + let end_addr = base_addr + size; + let module_name = parts[0].to_string(); + + proc_module_entries.push(ProcModuleEntry { + base_addr, + end_addr, + module_name, + }); + } + // Sort all entries by base_addr for fast lookup to see if a symbol has matched module. + proc_module_entries.sort_by_key(|proc_module_entry| proc_module_entry.base_addr); + } + + proc_module_entries +} + +/// Read the file (/proc/kallsyms) containing all kernel symbols and parse them into +/// a list of RawKernelSymbol sorted by the symbol address. +fn collect_raw_kernel_symbols(path: PathBuf) -> Result> { + let kallsyms_file = fs::File::open(path)?; + let buf_reader = BufReader::new(kallsyms_file); + + let mut raw_kernel_symbols: Vec = Vec::new(); + + for line_result in buf_reader.lines() { + let line = match line_result { + Ok(line) => line, + Err(_) => continue, + }; + + // Each line is in the format of
, where the + // raw name part contain the kernel symbol name and the module it belongs to (in "[]") + // e.g. ffffffffc0619010 t __nf_conntrack_hash_insert [nf_conntrack] + let parts: Vec<&str> = line.splitn(4, ' ').collect(); + if parts.len() < 3 { + continue; + } + let addr = u64::from_str_radix(parts[0], 16).unwrap_or(0); + if addr == 0 { + continue; + } + let type_letter = parts[1].chars().next().unwrap_or('?'); + if !is_valid_type_letter(type_letter) { + continue; + } + let raw_name = parts[2]; + // Ignore ARM mapping symbols and similar module-local labels, + // matching perf's map__process_kallsym_symbol (tools/perf/util/symbol.c): + // if (name[0] == '$') return 0; + if raw_name.starts_with('$') || raw_name.starts_with("__pi_$") { + continue; + } + // Retrieve symbol name and module it belongs to + let (name, module) = if let Some(tab_pos) = raw_name.find('\t') { + let module_part = &raw_name[tab_pos + 1..]; + let module = module_part + .trim_matches(|c| c == '[' || c == ']') + .to_string(); + (raw_name[..tab_pos].to_string(), Some(module)) + } else { + (raw_name.to_string(), None) + }; + + raw_kernel_symbols.push(RawKernelSymbol { + raw_symbol: RawSymbol { + addr, + // Give a dummy size first and compute it after all symbols are sorted. + size: 0, + name, + // In kallsyms, an upper case type letter denotes a global symbol. + is_global: type_letter.is_ascii_uppercase(), + // In kallsyms, weak symbols use type letters 'W' or 'w'. + is_weak: type_letter.to_ascii_lowercase() == 'w', + // All kallsyms symbols have a type. + is_no_type: false, + }, + module, + }); + } + + raw_kernel_symbols.sort_by_key(|raw_kernel_symbol| raw_kernel_symbol.raw_symbol.addr); + // Pre-compute a pseudo-size for each entry following Perf's symbols__fixup_end function, by + // computing the difference between two neighbor symbols. The pseudo-size will be then used + // to deduplicate overlapping symbols. + // This is to force that, for symbols with the same addr, the last symbol in the kallsyms file + // will get picked (Rust's sort_by_key is stable), which follows Perf's logic. + for i in 0..raw_kernel_symbols.len() { + let end_addr = if i + 1 < raw_kernel_symbols.len() { + raw_kernel_symbols[i + 1].raw_symbol.addr + } else { + tail_symbol_bound(raw_kernel_symbols[i].raw_symbol.addr) + }; + raw_kernel_symbols[i].raw_symbol.size = + end_addr.saturating_sub(raw_kernel_symbols[i].raw_symbol.addr) + } + + Ok(raw_kernel_symbols) +} + +/// Create the final Kernel symbol table by applying the deduplication logic on the raw symbols. +fn process_raw_kernel_symbols( + raw_kernel_symbols: Vec, + proc_module_entries: Vec, +) -> Vec { + let mut kernel_symbol_table: Vec = Vec::new(); + + // Deduplicate kernel symbols that share the same address. + let mut raw_kernel_symbol_iterator = raw_kernel_symbols.into_iter().peekable(); + while let Some(cur_raw_kernel_symbol) = raw_kernel_symbol_iterator.next() { + let cur_raw_kernel_symbol_addr = cur_raw_kernel_symbol.raw_symbol.addr; + // Attempt to retrieve the module that the address belongs to. + let matched_module = match proc_module_entries + .binary_search_by_key(&cur_raw_kernel_symbol_addr, |proc_module_entry| { + proc_module_entry.base_addr + }) { + Ok(i) => Some(&proc_module_entries[i].module_name), + Err(0) => None, + Err(i) => { + let matched_proc_module_entry = &proc_module_entries[i - 1]; + if cur_raw_kernel_symbol_addr < matched_proc_module_entry.end_addr { + Some(&matched_proc_module_entry.module_name) + } else { + None + } + } + }; + + let mut chosen_raw_kernel_symbol = cur_raw_kernel_symbol; + let mut chosen_with_matched_module = + matched_module.is_some() && chosen_raw_kernel_symbol.module.as_ref() == matched_module; + + while let Some(next_raw_kernel_symbol) = raw_kernel_symbol_iterator.peek() { + if next_raw_kernel_symbol.raw_symbol.addr != cur_raw_kernel_symbol_addr { + break; + } + + let next_raw_kernel_symbol = raw_kernel_symbol_iterator.next().unwrap(); + let next_raw_kernel_symbol_match_module = matched_module.is_some() + && next_raw_kernel_symbol.module.as_ref() == matched_module; + + // When choosing between two symbols with the same address, prefer the one whose module + // matches the /proc/modules. If none or both of them match, apply the choose_best_symbol + // logic in Perf. + let chose_next_raw_kernel_symbol = + if chosen_with_matched_module != next_raw_kernel_symbol_match_module { + next_raw_kernel_symbol_match_module + } else { + prefer_second_symbol( + &chosen_raw_kernel_symbol.raw_symbol, + &next_raw_kernel_symbol.raw_symbol, + ) + }; + if chose_next_raw_kernel_symbol { + chosen_raw_kernel_symbol = next_raw_kernel_symbol; + chosen_with_matched_module = next_raw_kernel_symbol_match_module; + } + } + + kernel_symbol_table.push(chosen_raw_kernel_symbol.to_symbol_table_entry()); + } + + kernel_symbol_table +} diff --git a/src/profiling/symbols/mmap_resolver.rs b/src/profiling/symbols/mmap_resolver.rs new file mode 100644 index 00000000..cc4e5eb3 --- /dev/null +++ b/src/profiling/symbols/mmap_resolver.rs @@ -0,0 +1,151 @@ +use std::collections::{BTreeMap, HashMap, HashSet}; + +/// This struct contains the information of an MMAP/MMAP2 event. When a process runs, an MMAP event maps +/// a section in file with file_path, located at page_offset and spanning len, to address start_addr +/// of the process's virtual address space. +#[derive(Clone)] +struct MmapEntry { + start_addr: u64, + len: u64, + page_offset: u64, + file_path: String, +} + +/// All MMAP entries of a process. +#[derive(Default)] +struct ProcessMmap { + /// Sorted map from start_address to MMAP entry, to quickly locate the corresponding MMAP entry + /// of a virtual address through binary search. + mmap_table: MmapTable, + /// If this process is just forked and the MMAP table was copied from its parent. + forked: bool, +} + +type MmapTable = BTreeMap; + +/// This struct contains MMAP events and related logics to resolve the virtual address in a process into the +/// file offset of an ELF file. The file offset and the ELF file can then be resolved into a symbol name. +#[derive(Default)] +pub struct MmapResolver { + /// Map from pid to the process's MMAP entries. + per_process_mmaps: HashMap, + /// Kernel MMAP table. + kernel_mmaps: MmapTable, + /// Cache the list of PIDs that have MMAP-ed a file path. It is used during the fallback of + /// reading the file from a process's root file system exposed by the Kernel. + file_path_pids_cache: HashMap>, + /// Used to identify whether a PID belongs to a HotSpot JVM, which might require additional + /// logics to build the JIT symbol table (see jit_symbols.rs). + hotspot_jvm_pids: HashSet, +} + +impl MmapResolver { + /// Store an MMAP entry for a process with PID. + pub fn add_mmap( + &mut self, + pid: i32, + start_addr: u64, + len: u64, + page_offset: u64, + file_path: String, + ) { + self.file_path_pids_cache + .entry(file_path.clone()) + .or_insert_with(|| HashSet::new()) + .insert(pid); + let process_map = self + .per_process_mmaps + .entry(pid) + .or_insert_with(|| ProcessMmap::default()); + + // When a forked process has an MMAP event, it has exec'd and the MMAP table + // copied from its parent is no longer valid. + if process_map.forked { + process_map.mmap_table.clear(); + process_map.forked = false; + } + + // HotSpot JVM candidate. + if file_path.contains("libjvm") { + self.hotspot_jvm_pids.insert(pid); + } + + process_map.mmap_table.insert( + start_addr, + MmapEntry { + start_addr, + len, + page_offset, + file_path, + }, + ); + } + + /// Store a Kernel MMAP entry. + pub fn add_kernel_mmap( + &mut self, + start_addr: u64, + len: u64, + page_offset: u64, + filename: String, + ) { + self.kernel_mmaps.insert( + start_addr, + MmapEntry { + start_addr, + len, + page_offset, + file_path: filename, + }, + ); + } + + /// Handle the case where a process is forked, by copying its parent's MMAP table. + pub fn fork_process(&mut self, ppid: i32, pid: i32) { + let cloned_parent_mmap_table = match self.per_process_mmaps.get(&ppid) { + Some(parent_process_mmaps) => parent_process_mmaps.mmap_table.clone(), + None => return, + }; + self.per_process_mmaps + .entry(pid) + .or_insert_with(|| ProcessMmap { + mmap_table: cloned_parent_mmap_table, + forked: true, + }); + } + + /// Resolve an instruction address of a process into the corresponding file offset and ELF file path. + pub fn resolve_addr(&self, pid: i32, addr: u64) -> Option<(u64, String)> { + let process_mmaps = self.per_process_mmaps.get(&pid)?; + resolve_mmap(&process_mmaps.mmap_table, addr) + } + + /// Resolve an instruction address in the Kernel space into the corresponding file offset and ELF file path. + pub fn resolve_kernel_addr(&self, kernel_addr: u64) -> Option<(u64, String)> { + resolve_mmap(&self.kernel_mmaps, kernel_addr) + } + + /// Retrieves the list of PIDs that have MMAP-ed the file path. + pub fn get_file_path_pids(&self, file_path: &str) -> Vec { + self.file_path_pids_cache + .get(file_path) + .map(|pids| pids.into_iter().map(|pid| *pid).collect()) + .unwrap_or(Vec::new()) + } + + /// Check if the pid belongs to a HotSpot JVM. + pub fn is_pid_hotspot_jvm(&self, pid: i32) -> bool { + self.hotspot_jvm_pids.contains(&pid) + } +} + +/// Resolve an address into the corresponding file offset and ELF file path in the MMAP table. +fn resolve_mmap(mmap_table: &MmapTable, addr: u64) -> Option<(u64, String)> { + let mmap_entry = mmap_table.range(..=addr).next_back()?.1; + if addr < mmap_entry.start_addr + mmap_entry.len { + let file_offset = addr - mmap_entry.start_addr + mmap_entry.page_offset; + Some((file_offset, mmap_entry.file_path.clone())) + } else { + None + } +} diff --git a/src/profiling/symbols/mod.rs b/src/profiling/symbols/mod.rs new file mode 100644 index 00000000..47f56e5e --- /dev/null +++ b/src/profiling/symbols/mod.rs @@ -0,0 +1,165 @@ +#![cfg(target_os = "linux")] + +mod elf_build_ids; +mod elf_symbols; +mod jit_symbols; +mod kernel_symbols; +mod mmap_resolver; +pub mod symbol_resolver; +mod vdso_symbols; + +/// A single entry in the symbol table, indicating that all addresses within +/// [addr, addr + size) should be resolved to name. +pub struct SymbolTableEntry { + addr: u64, + size: u64, + name: String, +} + +/// Information of a resolved symbol. +#[derive(Debug)] +pub struct ResolvedSymbol { + /// The human-readable name of the symbol. + pub name: String, + /// The offset from the starting address of the symbol. + pub offset: u64, + /// The source file or module that the symbol belongs to. + pub source: String, +} + +/// Attempt to resolve an address into a symbol in the symbol table. Entries in the symbol +/// table is sorted by every symbol's starting address to enable binary search. +pub fn resolve_symbol( + addr: u64, + symbol_table: &Vec, + source: &str, +) -> Option { + let idx = match symbol_table + .binary_search_by_key(&addr, |symbol_table_entry| symbol_table_entry.addr) + { + Ok(i) => i, + Err(0) => return None, + Err(i) => i - 1, + }; + let symbol_table_entry = &symbol_table[idx]; + let symbol_offset = addr - symbol_table_entry.addr; + if symbol_table_entry.size > 0 { + if symbol_offset >= symbol_table_entry.size { + return None; + } + } else { + // Handle symbols with zero-size, referring to function symbols__fixup_end in + // https://github.com/torvalds/linux/blob/master/tools/perf/util/symbol.c + if idx + 1 < symbol_table.len() { + let next_addr = symbol_table[idx + 1].addr; + if addr >= next_addr { + return None; + } + } else if addr >= tail_symbol_bound(symbol_table_entry.addr) { + return None; + } + } + + Some(ResolvedSymbol { + name: symbol_table_entry.name.clone(), + offset: symbol_offset, + source: source.to_string(), + }) +} + +/// The information of a symbol retrieved from an ELF file (for userspace symbols) or +/// kallsyms (for Kernel symbols), used to compare and deduplicate symbols with same addresses, +/// to choose the preferable one to create an entry in the final symbol table. +pub struct RawSymbol { + /// The starting address of the symbol. + addr: u64, + /// The size of the symbol in bytes, i.e. any address falling between [addr, addr + size) + /// should be resolved to this symbol. + size: u64, + /// The symbol name. + name: String, + /// A Local symbol is private to the translation unit that defines it and never exported + /// across object file, Equivalent to a C static function or a C++ anonymous-namespace helper. + /// A global symbol is externally visible - it is the default binding for non-static C/C++ + /// functions and exported kernel symbols. + is_global: bool, + /// A weak symbol is a hint saying "only use it if no strong symbol exists". They are + /// commonly default implementations or aliases. + is_weak: bool, + /// It indicates that the symbol exists, but the toolchain did not assert its type. Typical + /// sources are handwritten assemblies or linker generated symbols. + is_no_type: bool, +} +impl RawSymbol { + fn to_symbol_table_entry(self) -> SymbolTableEntry { + SymbolTableEntry { + addr: self.addr, + size: self.size, + name: demangle_symbol(&self.name), + } + } +} + +/// Refer to choose_best_symbol function in +/// https://github.com/torvalds/linux/blob/master/tools/perf/util/symbol.c +pub fn prefer_second_symbol(a: &RawSymbol, b: &RawSymbol) -> bool { + // Prefer non-zero size + let a_non_zero_size = a.size > 0; + let b_non_zero_size = b.size > 0; + if a_non_zero_size != b_non_zero_size { + return b_non_zero_size; + } + // Prefer non-NOTYPE + if a.is_no_type != b.is_no_type { + return a.is_no_type; + } + // Prefer non-weak + if a.is_weak != b.is_weak { + return a.is_weak; + } + // Prefer global + if a.is_global != b.is_global { + return b.is_global; + } + // Prefer fewer leading underscores. + let a_num_leading_underscores = a.name.bytes().take_while(|&c| c == b'_').count(); + let b_num_leading_underscores = b.name.bytes().take_while(|&c| c == b'_').count(); + if a_num_leading_underscores != b_num_leading_underscores { + return b_num_leading_underscores < a_num_leading_underscores; + } + // Prefer longer name + b.name.len() > a.name.len() +} + +/// Set the bound when dealing with last entry in a symbol table that has +/// no size (so we do not know the actual bound). This refers to the +/// symbols__fixup_end function (the "roundup(prev->end + 4096, 4096)" part) in +/// https://github.com/torvalds/linux/blob/master/tools/perf/util/symbol.c +pub fn tail_symbol_bound(symbol_address: u64) -> u64 { + ((symbol_address + 4095) & !4095u64) + 4096 +} + +/// Demangle a Rust or C++ symbol. +pub fn demangle_symbol(name: &str) -> String { + // Try Rust demangling first (v0 and legacy formats) + if name.starts_with("_R") || name.starts_with("_ZN") { + let demangled = rustc_demangle::demangle(name).to_string(); + if demangled != name { + return demangled; + } + } + // Try C++ demangling — use cpp_demangle with no_params + no_return_type + // for clean output that matches perf's format + if name.starts_with("_Z") || name.starts_with("___Z") { + if let Ok(sym) = cpp_demangle::Symbol::new(name) { + let options = cpp_demangle::DemangleOptions::default() + .no_return_type() + .no_params(); + if let Ok(demangled) = sym.demangle_with_options(&options) { + return demangled; + } + } + } + + name.to_string() +} diff --git a/src/profiling/symbols/symbol_resolver.rs b/src/profiling/symbols/symbol_resolver.rs new file mode 100644 index 00000000..855d3d04 --- /dev/null +++ b/src/profiling/symbols/symbol_resolver.rs @@ -0,0 +1,269 @@ +use crate::profiling::symbols::elf_build_ids::ElfBuildIds; +use crate::profiling::symbols::elf_symbols::ElfSymbols; +use crate::profiling::symbols::jit_symbols::JitSymbols; +use crate::profiling::symbols::kernel_symbols::KernelSymbols; +use crate::profiling::symbols::mmap_resolver::MmapResolver; +use crate::profiling::symbols::vdso_symbols::read_vdso_elf_data; +use crate::profiling::symbols::ResolvedSymbol; +use log::{debug, error, warn}; +use std::collections::HashMap; +use std::fs; + +#[derive(Default)] +pub struct SymbolResolver { + /// Store and resolve an instruction address into a file offset within an ELF file. + mmap_resolver: MmapResolver, + /// Store all ELF files' Build-IDs to help locate the original ELF file. + elf_build_ids: ElfBuildIds, + /// All ELF symbol tables mapped to ELF file paths. + elf_symbol_tables: HashMap, + /// All JIT symbol tables mapped to PIDs. + jit_symbol_tables: HashMap, + /// All Kernel symbols. + kernel_symbols: KernelSymbols, + /// The start of kernel space, used to distinguish Kernel instructions from userspace instructions. + kernel_space_start_address: u64, + /// Whether to create the frame unwinder to support leaf frame recovery. + support_leaf_caller_recovery: bool, +} + +impl SymbolResolver { + /// Initialize the Symbol Resolver for a specific architecture. + pub fn for_arch(arch: &str) -> Self { + let mut symbol_resolver = Self::default(); + + // Expect string values from uname -m. + let arch = arch.to_lowercase(); + if arch == "aarch64" || arch == "arm64" { + // https://www.kernel.org/doc/html/next/arm64/memory.html + symbol_resolver.kernel_space_start_address = 0xffff_0000_0000_0000; + symbol_resolver.support_leaf_caller_recovery = true; + } else if arch == "x86_64" { + // https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt + symbol_resolver.kernel_space_start_address = 0xffff_8000_0000_0000; + symbol_resolver.support_leaf_caller_recovery = false; + } else { + warn!("Unrecognized arch value when creating Symbol Resolver: {arch}"); + symbol_resolver.kernel_space_start_address = 0xffff_0000_0000_0000; + symbol_resolver.support_leaf_caller_recovery = false; + } + + // Parse /proc/kallsyms and create the Kernel Symbol table. + match KernelSymbols::from_kallsyms() { + Ok(kernel_symbols) => symbol_resolver.kernel_symbols = kernel_symbols, + Err(e) => error!( + "Failed to parse Kernel symbols from /proc/kallsyms: {:?}", + e + ), + } + + symbol_resolver + } + + /// Whether the resolver supports leaf frame recovery. + pub fn support_leaf_caller_recovery(&self) -> bool { + self.support_leaf_caller_recovery + } + + /// Store an MMAP event to be used for future symbol resolutions. + pub fn add_mmap( + &mut self, + pid: i32, + start_addr: u64, + len: u64, + page_offset: u64, + filename: String, + ) { + if pid == -1 { + self.mmap_resolver + .add_kernel_mmap(start_addr, len, page_offset, filename) + } else { + self.mmap_resolver + .add_mmap(pid, start_addr, len, page_offset, filename); + } + } + + /// Update the MMAP table when a process is forked - its parent's MMAP tables + /// should be inherited, until the process is exec'd and receives its own MMAP events. + pub fn handle_forked_process_mmap(&mut self, ppid: i32, pid: i32) { + self.mmap_resolver.fork_process(ppid, pid); + } + + /// Store a pair of ELF file path and its Build-ID, to help search for the original build + /// of the ELF file. + pub fn add_build_id(&mut self, elf_file_path: &str, build_id: String) { + self.elf_build_ids.add_build_id(elf_file_path, build_id) + } + + /// Resolve an instruction address of a PID into a symbol. + pub fn resolve(&mut self, pid: i32, addr: u64) -> Option { + if addr >= self.kernel_space_start_address { + self.resolve_kernel_addr(addr) + } else { + self.resolve_userspace_addr(pid, addr) + } + } + + /// Resolve a kernel instruction address through the following steps: + /// 1. Attempt to resolve the address directly using the Kernel symbol table (built from /proc/kallsyms). + /// 2. If the resolution failed, try to translate the address into file offset and ELF file + /// through Kernel MMAP entries, and then resolve it using ELF symbol table. + fn resolve_kernel_addr(&mut self, addr: u64) -> Option { + if let Some(resolved_symbol) = self.kernel_symbols.resolve(addr) { + return Some(resolved_symbol); + } + if let Some((file_offset, elf_file_path)) = self.mmap_resolver.resolve_kernel_addr(addr) { + self.resolve_by_elf_symbols(-1, file_offset, &elf_file_path) + } else { + None + } + } + + /// Resolve an userspace instruction address through the following steps: + /// 1. Translating the address into file offset and ELF file through MMAP entries. + /// 2. Resolve the file offset into symbol through the symbol table built from the ELF file. + /// 3. If the MMAP translation or ELF symbol resolution failed, resolve the address + /// directly using the JIT symbol table. + fn resolve_userspace_addr(&mut self, pid: i32, addr: u64) -> Option { + let (file_offset, elf_file_path) = match self.mmap_resolver.resolve_addr(pid, addr) { + Some(resolved_mmap) => resolved_mmap, + // No MMAP entry — try JIT symbols directly, since they may not have MMAP. + None => return self.resolve_by_jit_symbols(pid, addr), + }; + if let Some(resolved_symbol) = self.resolve_by_elf_symbols(pid, file_offset, &elf_file_path) + { + return Some(resolved_symbol); + } + self.resolve_by_jit_symbols(pid, addr) + } + + /// Resolve an instruction address from the JIT symbol table of the corresponding PID. If the + /// symbol table does not exist, build it from the /tmp/perf-.map file. + fn resolve_by_jit_symbols(&mut self, pid: i32, addr: u64) -> Option { + // Still insert a dummy symbol table in case it could not be built, so that + // the resolver does not attempt to keep rebuilding the symbol table. + self.jit_symbol_tables + .entry(pid) + .or_insert_with(|| { + JitSymbols::from_perf_map(pid, self.mmap_resolver.is_pid_hotspot_jvm(pid)) + .unwrap_or_default() + }) + .resolve(addr) + } + + /// Resolve a file offset from the corresponding ELF symbol table. If the symbol table does not + /// exist, build it from the ELF file with path elf_file_path. + fn resolve_by_elf_symbols( + &mut self, + pid: i32, + file_offset: u64, + elf_file_path: &str, + ) -> Option { + self.lazy_load_elf_file(pid, elf_file_path); + self.elf_symbol_tables + .get(elf_file_path) + .map_or(None, |elf_symbol_table| { + elf_symbol_table.resolve(file_offset) + }) + } + + /// Attempt to load the data of an ELF file and use it to build the ELF symbol table. If leaf + /// frame recovery is enabled, also build the Frame Unwinder from the same ELF file data. + fn lazy_load_elf_file(&mut self, pid: i32, elf_file_path: &str) { + if self.elf_symbol_tables.contains_key(elf_file_path) { + return; + } + + let load_elf_file_data = || -> Option<(Vec, String)> { + // First try to find the original build of the ELF file using the Build-ID - they ensure + // that the instruction address, MMAP entries, and symbol tables all refer to the same ELF file. + if let Some(original_elf_file_path) = + self.elf_build_ids.find_original_elf_file(elf_file_path) + { + if let Ok(data) = fs::read(&original_elf_file_path) { + return Some((data, original_elf_file_path.to_string_lossy().into_owned())); + } + } + // If dealing with VDSO symbols, load them from APerf's memory - all processes + // running on the same Kernel share the same VDSO symbol table. + if elf_file_path == "[vdso]" { + return read_vdso_elf_data().map(|vdso_data| (vdso_data, String::from("[vdso]"))); + } + // If failed to find the original ELF file using the Build-ID, attempt to read + // the ELF file path directly. + if let Ok(data) = fs::read(elf_file_path) { + return Some((data, elf_file_path.to_string())); + } + // Fall back to the process's FS mount exposed by the kernel /proc//root/ + // Caveats: the process needs to be owned by the same user who ran APerf, or sudo is + // required. Also, the path will not exist anymore if the process has exited. + if pid > 0 { + let process_fs_path = format!("/proc/{}/root{}", pid, elf_file_path); + if let Ok(data) = fs::read(&process_fs_path) { + return Some((data, process_fs_path)); + } + // In case the process has exited, retrieve the list of PIDs that have MMAP-ed + // this file path and access through their file system, in case one of them is + // still running. + for sibling_pid in self.mmap_resolver.get_file_path_pids(elf_file_path) { + if sibling_pid != pid { + let sibling_process_fs_path = + format!("/proc/{}/root{}", sibling_pid, elf_file_path); + if let Ok(data) = fs::read(&sibling_process_fs_path) { + return Some((data, sibling_process_fs_path)); + } + } + } + } + // Could not find or open the ELF file. + None + }; + + if let Some((elf_data, elf_data_source)) = load_elf_file_data() { + self.elf_symbol_tables.insert( + elf_file_path.to_string(), + ElfSymbols::from_elf_data( + &elf_data, + elf_data_source, + self.support_leaf_caller_recovery, + ) + .unwrap_or_else(|error| { + debug!( + "Failed to build ELF symbol table for file {elf_file_path}: {:?}", + error + ); + // Still insert a dummy symbol table in case it could not be built, so that + // the resolver does not attempt to keep rebuilding the symbol table. + return ElfSymbols::default(); + }), + ); + } else { + // Same reasoning as above - we only attempt to build the tables once. + self.elf_symbol_tables + .insert(elf_file_path.to_string(), ElfSymbols::default()); + } + } + + pub fn recover_leaf_frame_caller( + &mut self, + pid: i32, + leaf_addr: u64, + lr: u64, + fp: Option, + sp: Option, + ) -> Option { + let (leaf_file_offset, elf_file_path) = + match self.mmap_resolver.resolve_addr(pid, leaf_addr) { + Some(resolved_mmap) => resolved_mmap, + None => return None, + }; + + self.lazy_load_elf_file(pid, &elf_file_path); + + self.elf_symbol_tables + .get(&elf_file_path) + .map_or(None, |elf_symbol_table| { + elf_symbol_table.recover_leaf_frame_caller(leaf_file_offset, lr, fp, sp) + }) + } +} diff --git a/src/profiling/symbols/vdso_symbols.rs b/src/profiling/symbols/vdso_symbols.rs new file mode 100644 index 00000000..5843f5dd --- /dev/null +++ b/src/profiling/symbols/vdso_symbols.rs @@ -0,0 +1,75 @@ +use std::fs; +use std::io::{Read, Seek, SeekFrom}; + +/// Read the vDSO ELF data from the current process's memory. The vDSO is identical +/// for all processes on the same kernel. +pub fn read_vdso_elf_data() -> Option> { + // Find vDSO base address from /proc/self/auxv: + // Each entry is two native-sized words: a_type (a small integer tag defined in elf.h as AT_*) + // followed by a_un.a_val (an u64 whose meaning depends on the tag). The list ends with a_type == AT_NULL (0). + let auxv = fs::read("/proc/self/auxv").ok()?; + let mut vdso_base: u64 = 0; + let mut i = 0; + while i + 16 <= auxv.len() { + let tag = u64::from_ne_bytes(auxv[i..i + 8].try_into().ok()?); + let val = u64::from_ne_bytes(auxv[i + 8..i + 16].try_into().ok()?); + // Tag AT_SYSINFO_EHDR = 33 contains the base address of the vDSO ELF image in this process's memory + if tag == 33 { + vdso_base = val; + break; + } + if tag == 0 { + break; + } + i += 16; + } + if vdso_base == 0 { + return None; + } + + // Locate and read the vDSO ELF data from the process's own memory + let mut virtual_memory = fs::File::open("/proc/self/mem").ok()?; + virtual_memory.seek(SeekFrom::Start(vdso_base)).ok()?; + let mut elf_header = [0u8; 64]; + virtual_memory.read_exact(&mut elf_header).ok()?; + // Verify ELF magic + if &elf_header[0..4] != b"\x7fELF" { + return None; + } + + // Get program header and section header info to determine total size + let program_header_table_offset = u64::from_le_bytes(elf_header[32..40].try_into().ok()?); + let program_header_entry_size = u16::from_le_bytes(elf_header[54..56].try_into().ok()?) as u64; + let num_program_header_entries = u16::from_le_bytes(elf_header[56..58].try_into().ok()?) as u64; + let section_header_table_offset = u64::from_le_bytes(elf_header[40..48].try_into().ok()?); + let section_header_entry_size = u16::from_le_bytes(elf_header[58..60].try_into().ok()?) as u64; + let num_section_entries = u16::from_le_bytes(elf_header[60..62].try_into().ok()?) as u64; + + // Ensure that both of the program header and section header tables are included + let mut elf_data_end: u64 = + section_header_table_offset + num_section_entries * section_header_entry_size; + elf_data_end = elf_data_end + .max(program_header_table_offset + num_program_header_entries * program_header_entry_size); + // Check all LOAD segments to ensure that we read the complete ELF data. + virtual_memory + .seek(SeekFrom::Start(vdso_base + program_header_table_offset)) + .ok()?; + for _ in 0..num_program_header_entries { + let mut program_header_entry = vec![0u8; program_header_entry_size as usize]; + virtual_memory.read_exact(&mut program_header_entry).ok()?; + let p_type = u32::from_le_bytes(program_header_entry[0..4].try_into().ok()?); + if p_type == 1 { + // PT_LOAD + let p_offset = u64::from_le_bytes(program_header_entry[8..16].try_into().ok()?); + let p_file_size = u64::from_le_bytes(program_header_entry[32..40].try_into().ok()?); + elf_data_end = elf_data_end.max(p_offset + p_file_size) + } + } + + // Read the entire vDSO + virtual_memory.seek(SeekFrom::Start(vdso_base)).ok()?; + let mut elf_data = vec![0u8; elf_data_end as usize]; + virtual_memory.read_exact(&mut elf_data).ok()?; + + Some(elf_data) +} diff --git a/tests/analytics/test_profile_stack_frame_threshold_rule.rs b/tests/analytics/test_profile_stack_frame_threshold_rule.rs index 2ef2ea22..75a273cc 100644 --- a/tests/analytics/test_profile_stack_frame_threshold_rule.rs +++ b/tests/analytics/test_profile_stack_frame_threshold_rule.rs @@ -13,7 +13,7 @@ use super::test_helpers::{create_processed_data, DataFindingsExt}; /// frame4;frame5;frame6 75 /// frame1;frame7 90 fn create_profiler_instance(group_name: &str) -> Profiler { - let mut pd = Profiler::new(0, 100); + let mut pd = Profiler::new(0); let ts = ThreadState::from_str("STATE_DEFAULT"); pd.insert_stack( group_name,