Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions src/executor/helpers/apt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,6 @@ where
Ok(())
}

/// Returns whether a package is currently installed according to `dpkg`.
pub fn is_package_installed(package: &str) -> bool {
Command::new("dpkg")
.args(["-s", package])
.output()
.is_ok_and(|output| output.status.success())
}

pub fn install(system_info: &SystemInfo, packages: &[&str]) -> Result<()> {
if !is_system_compatible(system_info) {
bail!(
Expand Down
156 changes: 156 additions & 0 deletions src/executor/helpers/debug_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use crate::prelude::*;
use object::Object;
use std::path::{Path, PathBuf};

/// Search for a separate debug info file, in GDB's order (see [Separate Debug
/// Files]): build-id path first, then `.gnu_debuglink` with CRC32 validation.
/// Build-id wins because it hashes the binary contents, so a match cannot be a
/// false positive, while `.gnu_debuglink` only matches by filename.
///
/// The searched roots are where Debian/Ubuntu `*-dbg`/`*-dbgsym` packages and
/// NixOS `environment.enableDebugInfo` install debug files.
///
/// [Separate Debug Files]: https://sourceware.org/gdb/current/onlinedocs/gdb.html/Separate-Debug-Files.html
pub fn find_debug_file(object: &object::File, binary_path: &Path) -> Option<PathBuf> {
["/usr/lib/debug", "/run/current-system/sw/lib/debug"]
.iter()
.map(Path::new)
.filter(|dir| dir.exists())
.find_map(|dir| find_debug_file_in(object, binary_path, dir))
}

fn find_debug_file_in(
object: &object::File,
binary_path: &Path,
debug_dir: &Path,
) -> Option<PathBuf> {
if let Some(path) = find_debug_file_by_build_id(object, debug_dir) {
return Some(path);
}
find_debug_file_by_debuglink(object, binary_path, debug_dir)
}

/// Build-id `a05cfb6313fe06a13c9b4b5cb86c2069faa3951f` resolves to
/// `<debug_dir>/.build-id/a0/5cfb6313fe06a13c9b4b5cb86c2069faa3951f.debug`:
/// first byte as subdirectory, the rest as the filename.
fn find_debug_file_by_build_id(object: &object::File, debug_dir: &Path) -> Option<PathBuf> {
let build_id = object.build_id().ok()??;
if build_id.is_empty() {
return None;
}

let hex = build_id
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>();
let path = debug_dir
.join(".build-id")
.join(&hex[..2])
.join(format!("{}.debug", &hex[2..]));

if path.exists() {
return Some(path);
}

None
}

fn find_debug_file_by_debuglink(
object: &object::File,
binary_path: &Path,
debug_dir: &Path,
) -> Option<PathBuf> {
let (debuglink, expected_crc) = object.gnu_debuglink().ok()??;
let debuglink = std::str::from_utf8(debuglink).ok()?;
let dir = binary_path.parent()?;

let candidates = [
dir.join(debuglink),
dir.join(".debug").join(debuglink),
debug_dir
.join(dir.strip_prefix("/").unwrap_or(dir))
.join(debuglink),
];

candidates.into_iter().find(|p| {
let Ok(content) = std::fs::read(p) else {
return false;
};
let actual_crc = crc32fast::hash(&content);
if actual_crc != expected_crc {
trace!(
"CRC mismatch for {}: expected {expected_crc:#x}, got {actual_crc:#x}",
p.display()
);
return false;
}
true
})
}

/// Copy `binary` and `debug_file` in a fresh tempdir, renaming the debug file to
/// match the binary's `.gnu_debuglink` basename so `find_debug_file` resolves
/// the pair.
#[cfg(all(test, target_os = "linux"))]
pub(crate) fn setup_debuglink_tmpdir(
binary: &Path,
debug_file: &Path,
) -> (tempfile::TempDir, PathBuf, PathBuf) {
let src = std::fs::read(binary).unwrap();
let object = object::File::parse(&*src).unwrap();
let (debuglink, _crc) = object
.gnu_debuglink()
.unwrap()
.expect("binary has no .gnu_debuglink");
let debuglink = std::str::from_utf8(debuglink).unwrap();

let dir = tempfile::tempdir().unwrap();
let staged_binary = dir.path().join("binary");
let staged_debug = dir.path().join(debuglink);
std::fs::copy(binary, &staged_binary).unwrap();
std::fs::copy(debug_file, &staged_debug).unwrap();

(dir, staged_binary, staged_debug)
}

#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;

// Stripped libc plus its separate debug file, from Ubuntu 22.04's `libc6`
// and `libc6-dbg` packages.
const LIBC_PATH: &str = "testdata/perf_map/libc.so.6";
const LIBC_DEBUG_PATH: &str = "testdata/perf_map/libc.so.6.debug";

#[test]
fn test_find_debug_file_by_build_id() {
let binary_path = Path::new(LIBC_PATH);
let content = std::fs::read(binary_path).unwrap();
let object = object::File::parse(&*content).unwrap();

let build_id = object.build_id().unwrap().unwrap();
let hex: String = build_id.iter().map(|b| format!("{b:02x}")).collect();

let tmp = tempfile::tempdir().unwrap();
let debug_file_dir = tmp.path().join(".build-id").join(&hex[..2]);
std::fs::create_dir_all(&debug_file_dir).unwrap();

let debug_file_path = debug_file_dir.join(format!("{}.debug", &hex[2..]));
std::fs::copy(LIBC_DEBUG_PATH, &debug_file_path).unwrap();

let result = find_debug_file_in(&object, binary_path, tmp.path());
assert_eq!(result, Some(debug_file_path));
}

#[test]
fn test_find_debug_file_by_debuglink() {
let (_dir, binary, debug_file) =
setup_debuglink_tmpdir(Path::new(LIBC_PATH), Path::new(LIBC_DEBUG_PATH));
let content = std::fs::read(&binary).unwrap();
let object = object::File::parse(&*content).unwrap();

let empty_debug_dir = tempfile::tempdir().unwrap();
let result = find_debug_file_in(&object, &binary, empty_debug_dir.path());
assert_eq!(result, Some(debug_file));
}
}
1 change: 1 addition & 0 deletions src/executor/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod apt;
#[cfg(target_os = "linux")]
pub mod capabilities;
pub mod command;
pub mod debug_file;
pub mod detect_executable;
pub mod env;
pub mod get_bench_command;
Expand Down
66 changes: 58 additions & 8 deletions src/executor/valgrind/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ use crate::binary_pins::{
};
use crate::cli::run::helpers::download_pinned_file;
use crate::executor::helpers::apt;
use crate::executor::helpers::debug_file;
use crate::executor::{ToolInstallStatus, ToolStatus};
use crate::prelude::*;
use crate::system::{LinuxDistribution, SupportedOs, SystemInfo};
use semver::Version;
use std::{env, path::Path, process::Command};
use std::{
env,
path::{Path, PathBuf},
process::Command,
};

fn get_codspeed_valgrind_target(system_info: &SystemInfo) -> Result<ValgrindTarget> {
let SupportedOs::Linux(distro) = &system_info.os else {
Expand Down Expand Up @@ -173,6 +178,53 @@ fn classify_valgrind_version(version: String) -> ToolInstallStatus {
ToolInstallStatus::Installed { version }
}

/// Path of the system libc, following the Debian multiarch layout.
fn system_libc_path(system_info: &SystemInfo) -> Option<PathBuf> {
let triplet = match system_info.arch.as_str() {
"x86_64" => "x86_64-linux-gnu",
"aarch64" => "aarch64-linux-gnu",
arch => {
debug!("No known multiarch triplet for {arch}");
return None;
}
};
Some(PathBuf::from(format!("/lib/{triplet}/libc.so.6")))
}

/// Whether a separate debug file can be resolved for `binary`, through the same
/// build-id and `.gnu_debuglink` lookup that GDB and valgrind perform.
fn has_debug_symbols(binary: &Path) -> bool {
let data = match std::fs::read(binary) {
Ok(data) => data,
Err(e) => {
debug!("Failed to read {}: {e}", binary.display());
return false;
}
};
let object = match object::File::parse(data.as_slice()) {
Ok(object) => object,
Err(e) => {
debug!("Failed to parse {} as ELF: {e}", binary.display());
return false;
}
};

match debug_file::find_debug_file(&object, binary) {
Some(debug_path) => {
debug!(
"Resolved debug file for {}: {}",
binary.display(),
debug_path.display()
);
true
}
None => {
debug!("No debug file found for {}", binary.display());
false
}
}
}

fn is_valgrind_installed(system_info: &SystemInfo) -> bool {
if !matches!(
get_valgrind_status().status,
Expand All @@ -181,14 +233,12 @@ fn is_valgrind_installed(system_info: &SystemInfo) -> bool {
return false;
}

// `libc6-dbg` is only relevant on apt-based systems; on others (e.g. NixOS)
// `dpkg` is absent and would spuriously report it as missing.
if apt::is_system_compatible(system_info) {
apt::is_package_installed("libc6-dbg")
} else {
debug!("Skipping libc6-dbg check on non-apt-based system");
true
if !apt::is_system_compatible(system_info) {
debug!("Skipping libc debug symbol check on non-apt-based system");
return true;
}

system_libc_path(system_info).is_some_and(|libc| has_debug_symbols(&libc))
}

pub async fn install_valgrind(
Expand Down
11 changes: 6 additions & 5 deletions src/executor/wall_time/profiler/perf/debug_info.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::elf_helper::find_debug_file;
use super::loaded_module::LoadedModule;
use super::module_symbols::ModuleSymbols;
use crate::executor::helpers::debug_file::find_debug_file;
use crate::prelude::*;
use addr2line::{fallible_iterator::FallibleIterator, gimli};
use object::{Object, ObjectSection};
Expand Down Expand Up @@ -265,10 +265,11 @@ mod tests {
#[case] binary: &str,
#[case] debug_file: &str,
) {
let (_dir, binary, _debug_file) = super::super::elf_helper::setup_debuglink_tmpdir(
Path::new(binary),
Path::new(debug_file),
);
let (_dir, binary, _debug_file) =
crate::executor::helpers::debug_file::setup_debuglink_tmpdir(
Path::new(binary),
Path::new(debug_file),
);

let module_symbols = ModuleSymbols::from_elf(&binary).unwrap();
assert!(!module_symbols.symbols().is_empty());
Expand Down
Loading