Skip to content

Commit e615aee

Browse files
committed
feat: resolve debug info from separate .debug files via .gnu_debuglink
When a binary has no embedded DWARF sections (e.g. stripped system libraries like libc.so.6), fall back to searching for a separate debug file using the .gnu_debuglink section. This follows the standard GDB search order: same directory, .debug/ subdirectory, and /usr/lib/debug/<path>/. The debug file is validated with a CRC32 check to avoid silently using stale or mismatched debug files after binary upgrades. This means installing debug symbol packages (e.g. apt install libc6-dbg) now actually works — the runner picks up the separate .debug file and resolves file/line info for libc frames in flamegraphs.
1 parent 7637bca commit e615aee

3 files changed

Lines changed: 83 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ shell-words = "1.1.0"
7373
rmp-serde = "1.3.0"
7474
uuid = { version = "1.21.0", features = ["v4"] }
7575
which = "8.0.2"
76+
crc32fast = "1.5.0"
7677

7778
[target.'cfg(target_os = "linux")'.dependencies]
7879
procfs = "0.17.0"

src/executor/wall_time/perf/debug_info.rs

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,41 @@ use std::path::PathBuf;
1111

1212
type EndianRcSlice = gimli::EndianRcSlice<gimli::RunTimeEndian>;
1313

14+
/// Search for a separate debug info file using `.gnu_debuglink`.
15+
///
16+
/// Follows the standard GDB search order:
17+
/// 1. `<binary_dir>/<debuglink>`
18+
/// 2. `<binary_dir>/.debug/<debuglink>`
19+
/// 3. `/usr/lib/debug/<binary_dir>/<debuglink>`
20+
fn find_debug_file(object: &object::File, binary_path: &Path) -> Option<PathBuf> {
21+
let (debuglink, expected_crc) = object.gnu_debuglink().ok()??;
22+
let debuglink = std::str::from_utf8(debuglink).ok()?;
23+
let dir = binary_path.parent()?;
24+
25+
let candidates = [
26+
dir.join(debuglink),
27+
dir.join(".debug").join(debuglink),
28+
Path::new("/usr/lib/debug")
29+
.join(dir.strip_prefix("/").unwrap_or(dir))
30+
.join(debuglink),
31+
];
32+
33+
candidates.into_iter().find(|p| {
34+
let Ok(content) = std::fs::read(p) else {
35+
return false;
36+
};
37+
let actual_crc = crc32fast::hash(&content);
38+
if actual_crc != expected_crc {
39+
trace!(
40+
"CRC mismatch for {}: expected {expected_crc:#x}, got {actual_crc:#x}",
41+
p.display()
42+
);
43+
return false;
44+
}
45+
true
46+
})
47+
}
48+
1449
pub trait ModuleDebugInfoExt {
1550
fn from_symbols<P: AsRef<Path>>(
1651
path: P,
@@ -43,7 +78,10 @@ pub trait ModuleDebugInfoExt {
4378
}
4479

4580
impl ModuleDebugInfoExt for ModuleDebugInfo {
46-
/// Create debug info from existing symbols by looking up file/line in DWARF
81+
/// Create debug info from existing symbols by looking up file/line in DWARF.
82+
///
83+
/// If the binary has no DWARF sections, tries to find a separate debug file
84+
/// via `.gnu_debuglink` (e.g. installed by `libc6-dbg`).
4785
fn from_symbols<P: AsRef<Path>>(
4886
path: P,
4987
symbols: &ModuleSymbols,
@@ -52,7 +90,25 @@ impl ModuleDebugInfoExt for ModuleDebugInfo {
5290
let content = std::fs::read(path.as_ref())?;
5391
let object = object::File::parse(&*content)?;
5492

55-
let ctx = Self::create_dwarf_context(&object).context("Failed to create DWARF context")?;
93+
// If the binary has no DWARF, try a separate debug file via .gnu_debuglink
94+
let ctx = if object.section_by_name(".debug_info").is_some() {
95+
Self::create_dwarf_context(&object).context("Failed to create DWARF context")?
96+
} else {
97+
let debug_path = find_debug_file(&object, path.as_ref()).with_context(|| {
98+
format!(
99+
"No DWARF in {:?} and no separate debug file found",
100+
path.as_ref()
101+
)
102+
})?;
103+
trace!(
104+
"Using separate debug file {debug_path:?} for {:?}",
105+
path.as_ref()
106+
);
107+
let debug_content = std::fs::read(&debug_path)?;
108+
let debug_object = object::File::parse(&*debug_content)?;
109+
Self::create_dwarf_context(&debug_object)
110+
.context("Failed to create DWARF context from debug file")?
111+
};
56112
let (mut min_addr, mut max_addr) = (None, None);
57113
let debug_infos = symbols
58114
.symbols()
@@ -213,6 +269,29 @@ mod tests {
213269
insta::assert_debug_snapshot!(module_debug_info.debug_infos);
214270
}
215271

272+
#[test]
273+
fn test_stripped_binary_with_debuglink_resolves_debug_info() {
274+
// Mimics the libc + libc6-dbg scenario:
275+
// - cpp_my_benchmark_stripped.bin has symbols but no DWARF,
276+
// with a .gnu_debuglink pointing to cpp_my_benchmark.debug
277+
// - cpp_my_benchmark.debug has the DWARF sections
278+
const MODULE_PATH: &str = "testdata/perf_map/cpp_my_benchmark_stripped.bin";
279+
280+
let module_symbols = ModuleSymbols::from_elf(MODULE_PATH).unwrap();
281+
assert!(
282+
!module_symbols.symbols().is_empty(),
283+
"symbols should load from .symtab"
284+
);
285+
286+
// Should succeed by following .gnu_debuglink to the .debug file
287+
let module_debug_info =
288+
ModuleDebugInfo::from_symbols(MODULE_PATH, &module_symbols, 0).unwrap();
289+
assert!(
290+
!module_debug_info.debug_infos.is_empty(),
291+
"should have resolved debug info from separate .debug file"
292+
);
293+
}
294+
216295
#[test]
217296
fn test_ruff_debug_info() {
218297
const MODULE_PATH: &str = "testdata/perf_map/ty_walltime";

0 commit comments

Comments
 (0)