diff --git a/Cargo.lock b/Cargo.lock index fcfd8115..45645f2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -689,6 +689,7 @@ name = "ghostscope-dwarf" version = "0.1.0" dependencies = [ "anyhow", + "crc32fast", "futures", "ghostscope-platform", "ghostscope-protocol", diff --git a/docs/install.md b/docs/install.md index f098c7be..d1984971 100644 --- a/docs/install.md +++ b/docs/install.md @@ -97,6 +97,66 @@ If no `.debug_*` sections are found, the binary must be recompiled with debug sy **Note**: Without debug symbols, GhostScope cannot resolve function names, variables, or source line information. +#### Separate Debug Files (GNU debuglink) + +GhostScope also supports loading debug information from separate debug files using the `.gnu_debuglink` mechanism. This is useful when working with stripped binaries in production environments. + +**Check for debuglink section:** +```bash +# Check if binary has .gnu_debuglink pointing to a separate debug file +readelf -x .gnu_debuglink your_program + +# Example output: +# Hex dump of section '.gnu_debuglink': +# 0x00000000 6d795f70 726f6772 616d2e64 65627567 my_program.debug +# 0x00000010 00000000 12345678 ....4Vx +``` + +**Create separate debug file for a stripped binary:** +```bash +# 1. Extract debug information to a separate file +objcopy --only-keep-debug your_program your_program.debug + +# 2. Strip debug information from the binary +objcopy --strip-debug your_program + +# 3. Add a link from the binary to the debug file +objcopy --add-gnu-debuglink=your_program.debug your_program + +# Verify the debuglink was added +readelf -x .gnu_debuglink your_program +``` + +**Debug file search paths (following GDB conventions):** + +GhostScope automatically searches for debug files in the following locations: +1. Same directory as the binary: `/path/to/your_program.debug` +2. `.debug` subdirectory: `/path/to/.debug/your_program.debug` +3. Global debug directory: `/usr/lib/debug/path/to/your_program.debug` + +**Installing system debug packages:** +```bash +# Ubuntu/Debian - install debug symbols for libc +sudo apt install libc6-dbg + +# Fedora/RHEL - install debug symbols +sudo dnf debuginfo-install glibc + +# The debug files are typically installed in /usr/lib/debug/ +``` + +**Verification:** + +GhostScope will automatically detect and use separate debug files. You can verify this in the logs: +```bash +# Run with debug logging to see debuglink resolution +RUST_LOG=debug sudo ghostscope -p $(pidof your_program) + +# Look for messages like: +# "Looking for debug file 'your_program.debug' for binary '/path/to/your_program'" +# "Found matching debug file: /path/to/your_program.debug (CRC: 0x12345678)" +``` + ## Troubleshooting ### Permission Denied Errors diff --git a/docs/zh/install.md b/docs/zh/install.md index 5b9c9f7a..e714e14b 100644 --- a/docs/zh/install.md +++ b/docs/zh/install.md @@ -97,6 +97,66 @@ readelf -S your_program | grep debug **注意**:没有调试符号,GhostScope 无法解析函数名、变量或源代码行信息。 +#### 独立调试文件(GNU debuglink) + +GhostScope 支持使用 `.gnu_debuglink` 机制从独立的调试文件加载调试信息。这在生产环境中处理 stripped 二进制文件时非常有用。 + +**检查 debuglink 段:** +```bash +# 检查二进制文件是否有指向独立调试文件的 .gnu_debuglink +readelf -x .gnu_debuglink your_program + +# 示例输出: +# Hex dump of section '.gnu_debuglink': +# 0x00000000 6d795f70 726f6772 616d2e64 65627567 my_program.debug +# 0x00000010 00000000 12345678 ....4Vx +``` + +**为 stripped 二进制创建独立调试文件:** +```bash +# 1. 提取调试信息到独立文件 +objcopy --only-keep-debug your_program your_program.debug + +# 2. 从二进制文件中删除调试信息 +objcopy --strip-debug your_program + +# 3. 在二进制文件中添加指向调试文件的链接 +objcopy --add-gnu-debuglink=your_program.debug your_program + +# 验证 debuglink 已添加 +readelf -x .gnu_debuglink your_program +``` + +**调试文件搜索路径(遵循 GDB 约定):** + +GhostScope 会自动在以下位置搜索调试文件: +1. 二进制文件同目录:`/path/to/your_program.debug` +2. `.debug` 子目录:`/path/to/.debug/your_program.debug` +3. 全局调试目录:`/usr/lib/debug/path/to/your_program.debug` + +**安装系统调试包:** +```bash +# Ubuntu/Debian - 安装 libc 的调试符号 +sudo apt install libc6-dbg + +# Fedora/RHEL - 安装调试符号 +sudo dnf debuginfo-install glibc + +# 调试文件通常安装在 /usr/lib/debug/ 目录下 +``` + +**验证:** + +GhostScope 会自动检测并使用独立调试文件。你可以通过日志验证: +```bash +# 启用调试日志以查看 debuglink 解析过程 +RUST_LOG=debug sudo ghostscope -p $(pidof your_program) + +# 查找类似以下的消息: +# "Looking for debug file 'your_program.debug' for binary '/path/to/your_program'" +# "Found matching debug file: /path/to/your_program.debug (CRC: 0x12345678)" +``` + ## 故障排除 ### 权限被拒绝错误 diff --git a/ghostscope-dwarf/Cargo.toml b/ghostscope-dwarf/Cargo.toml index 7f3260f1..814f7d0a 100644 --- a/ghostscope-dwarf/Cargo.toml +++ b/ghostscope-dwarf/Cargo.toml @@ -24,3 +24,6 @@ num_cpus = "1.0" # For proc mapping parsing (temporary, will extract from ghostscope-binary) libc = "0.2" +# For .gnu_debuglink CRC validation +crc32fast = "1.4" + diff --git a/ghostscope-dwarf/src/debuglink.rs b/ghostscope-dwarf/src/debuglink.rs new file mode 100644 index 00000000..d9f3d8eb --- /dev/null +++ b/ghostscope-dwarf/src/debuglink.rs @@ -0,0 +1,269 @@ +//! Support for .gnu_debuglink section - find separate debug info files +//! +//! This module implements the standard GNU debuglink mechanism for locating +//! debug information in separate files, following GDB's search strategy. + +use crate::core::Result; +use object::Object; +use std::fs::File; +use std::path::{Path, PathBuf}; + +/// Find separate debug file using .gnu_debuglink section +/// +/// Search order (following GDB conventions): +/// 1. Same directory as binary: /path/to/binary.debug +/// 2. .debug subdirectory: /path/to/.debug/binary.debug +/// 3. Global debug directory: /usr/lib/debug/path/to/binary.debug +/// +/// Returns the path to the debug file if found and CRC matches +/// Also verifies build ID if present in both files +pub fn find_debug_file>(binary_path: P) -> Result> { + let binary_path = binary_path.as_ref(); + + // Read binary and check for .gnu_debuglink section + let binary_data = std::fs::read(binary_path)?; + let binary_obj = object::File::parse(&*binary_data)?; + + // Extract build ID from binary for later verification + let binary_build_id = binary_obj.build_id().ok().flatten(); + + // Check if .gnu_debuglink section exists + let (debug_filename, expected_crc) = match binary_obj.gnu_debuglink() { + Ok(Some((filename, crc))) => (filename, crc), + Ok(None) => { + // No .gnu_debuglink section - binary contains debug info + tracing::debug!("No .gnu_debuglink section in {}", binary_path.display()); + return Ok(None); + } + Err(e) => { + tracing::warn!( + "Failed to read .gnu_debuglink from {}: {}", + binary_path.display(), + e + ); + return Ok(None); + } + }; + + // Convert filename bytes to PathBuf (Linux-only, as GhostScope is an eBPF project) + use std::os::unix::ffi::OsStrExt; + let os_str = std::ffi::OsStr::from_bytes(debug_filename); + let debug_filename = Path::new(os_str); + + tracing::info!( + "Looking for debug file '{}' for binary '{}'", + debug_filename.display(), + binary_path.display() + ); + + // Build search paths following GDB's strategy + let search_paths = build_search_paths(binary_path, debug_filename); + + // Try each path and verify CRC + build ID + for candidate_path in search_paths { + tracing::debug!("Checking debug file path: {}", candidate_path.display()); + + if candidate_path.exists() { + match verify_debug_file(&candidate_path, expected_crc, binary_build_id) { + Ok(true) => { + tracing::info!( + "Found matching debug file: {} (CRC: 0x{:08x})", + candidate_path.display(), + expected_crc + ); + return Ok(Some(candidate_path)); + } + Ok(false) => { + tracing::warn!( + "Debug file {} exists but verification failed (CRC or build ID mismatch)", + candidate_path.display() + ); + } + Err(e) => { + tracing::debug!( + "Failed to verify debug file {}: {}", + candidate_path.display(), + e + ); + } + } + } + } + + tracing::warn!( + "Debug file '{}' not found in any standard location", + debug_filename.display() + ); + Ok(None) +} + +/// Build search paths for debug file following GDB conventions +fn build_search_paths(binary_path: &Path, debug_filename: &Path) -> Vec { + let mut paths = Vec::new(); + + // Get binary directory + let binary_dir = binary_path.parent(); + + // 1. Same directory as binary + if let Some(dir) = binary_dir { + paths.push(dir.join(debug_filename)); + } + + // 2. .debug subdirectory + if let Some(dir) = binary_dir { + paths.push(dir.join(".debug").join(debug_filename)); + } + + // 3. Global debug directory with full path structure + // For /usr/bin/foo -> /usr/lib/debug/usr/bin/foo.debug + if binary_path.is_absolute() { + let global_debug_path = PathBuf::from("/usr/lib/debug") + .join(binary_path.strip_prefix("/").unwrap_or(binary_path)) + .with_file_name(debug_filename); + paths.push(global_debug_path); + } + + paths +} + +/// Verify debug file matches binary (CRC + build ID) +/// +/// Checks: +/// 1. CRC-32 matches (required by .gnu_debuglink) +/// 2. Build ID matches if present in both files (warning if mismatch) +fn verify_debug_file( + debug_file_path: &Path, + expected_crc: u32, + binary_build_id: Option<&[u8]>, +) -> Result { + let file_data = std::fs::read(debug_file_path)?; + + // 1. Verify CRC-32 + let actual_crc = calculate_gnu_debuglink_crc(&file_data); + + tracing::debug!( + "CRC check for {}: expected=0x{:08x}, actual=0x{:08x}", + debug_file_path.display(), + expected_crc, + actual_crc + ); + + if actual_crc != expected_crc { + tracing::warn!( + "CRC mismatch for {}: expected=0x{:08x}, actual=0x{:08x}", + debug_file_path.display(), + expected_crc, + actual_crc + ); + return Ok(false); + } + + // 2. Verify build ID if present + let debug_obj = object::File::parse(&*file_data)?; + let debug_build_id = debug_obj.build_id().ok().flatten(); + + match (binary_build_id, debug_build_id) { + (Some(binary_id), Some(debug_id)) => { + if binary_id != debug_id { + tracing::warn!( + "Build ID mismatch for {}: binary={:02x?}, debug={:02x?}", + debug_file_path.display(), + binary_id, + debug_id + ); + // According to GDB behavior: CRC takes priority, build ID mismatch is just a warning + // We still return true if CRC matches + tracing::warn!( + "CRC matches but build IDs differ - using debug file anyway (following GDB behavior)" + ); + } else { + tracing::debug!( + "Build ID verification passed for {}: {:02x?}", + debug_file_path.display(), + binary_id + ); + } + } + (Some(binary_id), None) => { + tracing::debug!( + "Binary has build ID {:02x?} but debug file has none", + binary_id + ); + } + (None, Some(debug_id)) => { + tracing::debug!( + "Debug file has build ID {:02x?} but binary has none", + debug_id + ); + } + (None, None) => { + tracing::debug!("Neither binary nor debug file has build ID"); + } + } + + Ok(true) +} + +/// Calculate CRC-32 using GNU debuglink algorithm +/// +/// This uses the IEEE 802.3 polynomial (same as standard CRC-32) +/// Note: GNU debuglink uses specific CRC-32 variant +fn calculate_gnu_debuglink_crc(data: &[u8]) -> u32 { + // Use crc32fast crate for standard CRC-32 (IEEE polynomial) + crc32fast::hash(data) +} + +/// Try to load debug file if available, otherwise return None +/// +/// This is the main entry point for loading debug info +pub fn try_load_debug_file>( + binary_path: P, +) -> Result> { + let binary_path = binary_path.as_ref(); + + match find_debug_file(binary_path)? { + Some(debug_path) => { + tracing::info!( + "Loading debug info from separate file: {}", + debug_path.display() + ); + + let file = File::open(&debug_path)?; + let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? }; + + Ok(Some((debug_path, mmap))) + } + None => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_search_paths() { + let binary_path = Path::new("/usr/bin/my_program"); + let debug_filename = Path::new("my_program.debug"); + + let paths = build_search_paths(binary_path, debug_filename); + + assert_eq!(paths.len(), 3); + assert_eq!(paths[0], Path::new("/usr/bin/my_program.debug")); + assert_eq!(paths[1], Path::new("/usr/bin/.debug/my_program.debug")); + assert_eq!( + paths[2], + Path::new("/usr/lib/debug/usr/bin/my_program.debug") + ); + } + + #[test] + fn test_crc_calculation() { + // Test with known data + let data = b"hello world"; + let crc = calculate_gnu_debuglink_crc(data); + + // CRC-32 (IEEE) for "hello world" is 0x0d4a1185 + assert_eq!(crc, 0x0d4a1185); + } +} diff --git a/ghostscope-dwarf/src/lib.rs b/ghostscope-dwarf/src/lib.rs index 9baa416a..977cc5bc 100644 --- a/ghostscope-dwarf/src/lib.rs +++ b/ghostscope-dwarf/src/lib.rs @@ -8,6 +8,7 @@ pub mod core; // Internal implementation modules pub(crate) mod data; +pub(crate) mod debuglink; pub(crate) mod loader; pub(crate) mod module; pub(crate) mod parser; diff --git a/ghostscope-dwarf/src/module/data.rs b/ghostscope-dwarf/src/module/data.rs index 8ffde243..978682ee 100644 --- a/ghostscope-dwarf/src/module/data.rs +++ b/ghostscope-dwarf/src/module/data.rs @@ -54,8 +54,10 @@ pub(crate) struct ModuleData { cfi_index: Option, /// On-demand resolver (for detailed parsing) resolver: OnDemandResolver, - /// Memory mapped file (keep alive) - _mapped_file: MappedFile, + /// Memory mapped file for DWARF data (may be debug file via .gnu_debuglink) + _dwarf_mapped_file: std::sync::Arc, + /// Memory mapped file for binary (used for vaddr to file offset calculation) + _binary_mapped_file: std::sync::Arc, /// Per-function block/variable index (blockvector-like) block_index: crate::data::BlockIndex, /// Type name index for cross-CU completion @@ -193,11 +195,70 @@ impl ModuleData { module_mapping.path.display() ); - // Memory map the file once - let mapped_file = std::sync::Arc::new(Self::map_file(&module_mapping.path)?); + // Memory map the binary file + let binary_mapped = std::sync::Arc::new(Self::map_file(&module_mapping.path)?); - // Load DWARF sections (now returns Dwarf) - let dwarf = std::sync::Arc::new(Self::load_dwarf_sections(&mapped_file)?); + // Try to load DWARF sections from the binary file first + let dwarf_result = Self::load_dwarf_sections(&binary_mapped); + + // Check if we need to search for separate debug file + let (dwarf, mapped_file_for_dwarf) = match dwarf_result { + Ok(dwarf_data) => { + // Check if we actually have debug info sections + if Self::has_debug_info(&dwarf_data) { + tracing::debug!( + "Found debug info in binary: {}", + module_mapping.path.display() + ); + ( + std::sync::Arc::new(dwarf_data), + std::sync::Arc::clone(&binary_mapped), + ) + } else { + // No debug info, try to find separate debug file + tracing::info!( + "No debug info in binary, searching for .gnu_debuglink: {}", + module_mapping.path.display() + ); + match crate::debuglink::try_load_debug_file(&module_mapping.path)? { + Some((debug_path, debug_mmap)) => { + tracing::info!( + "Loading DWARF from separate debug file: {}", + debug_path.display() + ); + let debug_mapped = std::sync::Arc::new(MappedFile { + data: debug_mmap, + path: debug_path.clone(), + }); + let debug_dwarf = Self::load_dwarf_sections(&debug_mapped)?; + (std::sync::Arc::new(debug_dwarf), debug_mapped) + } + None => { + // No debug file found, use original (possibly empty) dwarf + tracing::warn!( + "No separate debug file found for: {}", + module_mapping.path.display() + ); + ( + std::sync::Arc::new(dwarf_data), + std::sync::Arc::clone(&binary_mapped), + ) + } + } + } + } + Err(e) => { + tracing::error!( + "Failed to parse DWARF from {}: {}", + module_mapping.path.display(), + e + ); + return Err(e); + } + }; + + // Use mapped_file_for_dwarf which is either binary or debug file + let mapped_file = mapped_file_for_dwarf; tracing::debug!( "Starting parallel DWARF parsing with true debug_line || debug_info parallelism..." @@ -223,13 +284,13 @@ impl ModuleData { parser.parse_debug_info(&module_path) } }), - // Parse CFI independently (now using Arc-based data, no unsafe!) + // Parse CFI independently from binary file (not debug file) tokio::task::spawn_blocking({ - let mapped_file = std::sync::Arc::clone(&mapped_file); + let binary_for_cfi = std::sync::Arc::clone(&binary_mapped); let module_path = module_mapping.path.clone(); move || -> Result> { // Convert MappedFile data to Arc<[u8]> - let file_data_arc: std::sync::Arc<[u8]> = mapped_file.data[..].into(); + let file_data_arc: std::sync::Arc<[u8]> = binary_for_cfi.data[..].into(); match crate::data::CfiIndex::from_arc_data(file_data_arc) { Ok(cfi) => { tracing::info!( @@ -330,8 +391,8 @@ impl ModuleData { resolver, block_index: crate::data::BlockIndex::new(), type_name_index, - _mapped_file: std::sync::Arc::try_unwrap(mapped_file) - .map_err(|_| anyhow::anyhow!("Failed to unwrap MappedFile Arc"))?, + _dwarf_mapped_file: mapped_file, + _binary_mapped_file: binary_mapped, }) } @@ -347,6 +408,17 @@ impl ModuleData { }) } + /// Check if DWARF data contains debug information + /// + /// Returns true if .debug_info section has at least one compilation unit + fn has_debug_info(dwarf: &gimli::Dwarf>) -> bool { + // Try to get the first unit header - need to check if it actually exists + match dwarf.units().next() { + Ok(Some(_)) => true, // Has at least one unit + _ => false, // No units or error + } + } + /// Load DWARF sections using gimli with Arc-based data fn load_dwarf_sections( file_data: &std::sync::Arc, @@ -385,11 +457,11 @@ impl ModuleData { /// Convert a virtual address (DWARF PC) to an ELF file offset using PT_LOAD segments /// Returns None if no containing segment is found pub(crate) fn vaddr_to_file_offset(&self, vaddr: u64) -> Option { - // Re-parse the object file on-demand from the mapped file - if self._mapped_file.data.is_empty() { + // Use binary file (not debug file) for segment calculation + if self._binary_mapped_file.data.is_empty() { return None; } - let data: &[u8] = &self._mapped_file.data; + let data: &[u8] = &self._binary_mapped_file.data; let obj = match object::File::parse(data) { Ok(f) => f, Err(_) => return None, @@ -1374,7 +1446,7 @@ impl ModuleData { let entries = self.lightweight_index.find_variables_by_name(name); // Parse object file once for section classification - let obj = match object::File::parse(&self._mapped_file.data[..]) { + let obj = match object::File::parse(&self._binary_mapped_file.data[..]) { Ok(f) => f, Err(_) => { // Cannot classify sections, but still return entries with link_address @@ -1443,7 +1515,7 @@ impl ModuleData { /// Public helper: classify a virtual address to a section type by parsing the module object pub(crate) fn classify_section_for_vaddr(&self, addr: u64) -> Option { - match object::File::parse(&self._mapped_file.data[..]) { + match object::File::parse(&self._binary_mapped_file.data[..]) { Ok(obj) => self.classify_section(&obj, addr), Err(_) => None, } @@ -1453,7 +1525,7 @@ impl ModuleData { pub(crate) fn list_all_global_variables(&self) -> Vec { let mut out = Vec::new(); // Parse object once for section classification - let _obj = match object::File::parse(&self._mapped_file.data[..]) { + let _obj = match object::File::parse(&self._binary_mapped_file.data[..]) { Ok(f) => f, Err(_) => { return out; diff --git a/ghostscope/src/script/compiler.rs b/ghostscope/src/script/compiler.rs index 3d0bed86..31ee138d 100644 --- a/ghostscope/src/script/compiler.rs +++ b/ghostscope/src/script/compiler.rs @@ -408,8 +408,40 @@ pub async fn compile_and_load_script_for_cli( } if uprobe_configs.is_empty() { + // Check if we have debug info - this is checked during module loading + let available_functions = session.list_functions(); + + if available_functions.is_empty() { + return Err(anyhow::anyhow!( + "No debug information found in any module!\n\ + \n\ + The target binary and its libraries are stripped or compiled without debug symbols.\n\ + GhostScope requires debug information (DWARF) to:\n\ + - Locate functions by name\n\ + - Analyze variable types and locations\n\ + - Map source lines to addresses\n\ + \n\ + Solutions:\n\ + 1. Recompile your target with -g flag: gcc -g your_program.c -o your_program\n\ + 2. Install debug symbol packages (e.g., libc6-dbg on Debian/Ubuntu)\n\ + 3. For stripped binaries, use objcopy to create separate debug files:\n\ + objcopy --only-keep-debug binary binary.debug\n\ + objcopy --add-gnu-debuglink=binary.debug binary" + )); + } + return Err(anyhow::anyhow!( - "No uprobe configurations created - nothing to attach" + "No uprobe configurations created - the functions referenced in your script were not found.\n\ + \n\ + Possible reasons:\n\ + - Function names are misspelled (check available functions below)\n\ + - Functions don't exist in the target binary\n\ + - Functions are from libraries that aren't loaded yet\n\ + \n\ + Available functions (first 10):\n{}\n\ + \n\ + Tip: Run GhostScope in TUI mode to browse all available functions", + available_functions.iter().take(10).map(|f| format!(" - {}", f)).collect::>().join("\n") )); } diff --git a/ghostscope/tests/common/mod.rs b/ghostscope/tests/common/mod.rs index 98707a12..b10faa4a 100644 --- a/ghostscope/tests/common/mod.rs +++ b/ghostscope/tests/common/mod.rs @@ -17,6 +17,7 @@ static REGISTER_CLEANUP: Once = Once::new(); lazy_static! { static ref COMPILE_DEBUG_RESULT: Mutex>> = Mutex::new(None); static ref COMPILE_OPT_RESULT: Mutex>> = Mutex::new(None); + static ref COMPILE_STRIPPED_RESULT: Mutex>> = Mutex::new(None); static ref COMPILE_COMPLEX_DEBUG_RESULT: Mutex>> = Mutex::new(None); static ref COMPILE_COMPLEX_OPT_RESULT: Mutex>> = Mutex::new(None); static ref COMPILE_COMPLEX_NOPIE_RESULT: Mutex>> = Mutex::new(None); @@ -63,6 +64,7 @@ pub fn init() { } static COMPILE_OPTIMIZED: Once = Once::new(); +static COMPILE_STRIPPED: Once = Once::new(); /// Optimization level for test program compilation #[derive(Debug, Clone, Copy, PartialEq)] @@ -73,6 +75,8 @@ pub enum OptimizationLevel { O2, // -O2 #[allow(dead_code)] O3, // -O3 + #[allow(dead_code)] + Stripped, // -O0 with separate debug file (.gnu_debuglink) } impl OptimizationLevel { @@ -82,6 +86,7 @@ impl OptimizationLevel { OptimizationLevel::O1 => "sample_program_o1", OptimizationLevel::O2 => "sample_program_o2", OptimizationLevel::O3 => "sample_program_o3", + OptimizationLevel::Stripped => "sample_program_stripped", } } @@ -91,6 +96,7 @@ impl OptimizationLevel { OptimizationLevel::O1 => "sample_program_o1", OptimizationLevel::O2 => "sample_program_o2", OptimizationLevel::O3 => "sample_program_o3", + OptimizationLevel::Stripped => "sample_program_stripped", } } @@ -100,6 +106,7 @@ impl OptimizationLevel { OptimizationLevel::O1 => "Optimized (O1)", OptimizationLevel::O2 => "Optimized (O2)", OptimizationLevel::O3 => "Highly Optimized (O3)", + OptimizationLevel::Stripped => "Stripped with .gnu_debuglink", } } } @@ -123,6 +130,17 @@ pub fn ensure_test_program_compiled_with_opt(opt_level: OptimizationLevel) -> an None => panic!("Compilation result should be set after call_once"), } } + OptimizationLevel::Stripped => { + COMPILE_STRIPPED.call_once(|| { + let compile_result = compile_sample_program(opt_level); + *COMPILE_STRIPPED_RESULT.lock().unwrap() = Some(compile_result); + }); + match COMPILE_STRIPPED_RESULT.lock().unwrap().as_ref() { + Some(Ok(())) => Ok(()), + Some(Err(e)) => Err(anyhow::anyhow!("{}", e)), + None => panic!("Compilation result should be set after call_once"), + } + } _ => { COMPILE_OPTIMIZED.call_once(|| { let compile_result = compile_sample_program(opt_level); @@ -222,6 +240,9 @@ fn compile_complex_program(opt_level: OptimizationLevel) -> anyhow::Result<()> { OptimizationLevel::O1 => "complex_types_program_o1", OptimizationLevel::O2 => "complex_types_program_o2", OptimizationLevel::O3 => "complex_types_program_o3", + OptimizationLevel::Stripped => { + anyhow::bail!("Stripped optimization level not supported for complex_types_program") + } }; let output = Command::new("make") @@ -285,6 +306,11 @@ impl TestFixtures { OptimizationLevel::O1 => "complex_types_program_o1", OptimizationLevel::O2 => "complex_types_program_o2", OptimizationLevel::O3 => "complex_types_program_o3", + OptimizationLevel::Stripped => { + anyhow::bail!( + "Stripped optimization level not supported for complex_types_program" + ) + } }; self.base_path.join("complex_types_program").join(bin_name) } else if name == "globals_program" { diff --git a/ghostscope/tests/dwarf_parsing.rs b/ghostscope/tests/dwarf_parsing.rs index 8c7de1a4..a15fcb9c 100644 --- a/ghostscope/tests/dwarf_parsing.rs +++ b/ghostscope/tests/dwarf_parsing.rs @@ -552,3 +552,48 @@ async fn test_dwarf_tool_text_output_format() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn test_stripped_binary_with_debuglink() -> anyhow::Result<()> { + init(); + + // Compile stripped binary with separate debug file + common::ensure_test_program_compiled_with_opt(common::OptimizationLevel::Stripped)?; + + let binary_path = + FIXTURES.get_test_binary_with_opt("sample_program", common::OptimizationLevel::Stripped)?; + + println!( + "Testing stripped binary with .gnu_debuglink: {}", + binary_path.display() + ); + + // Verify debug file exists + let debug_file = binary_path.with_file_name("sample_program_stripped.debug"); + assert!( + debug_file.exists(), + "Debug file should exist: {}", + debug_file.display() + ); + + // Test that we can still read function info from stripped binary via .gnu_debuglink + // Test main function + let main_info = run_dwarf_tool_json(&binary_path, "function", &["main"]).await?; + assert!( + main_info.is_array() || main_info.is_object(), + "Should get function info for main" + ); + + // Test add_numbers function + let add_numbers_info = run_dwarf_tool_json(&binary_path, "function", &["add_numbers"]).await?; + assert!( + add_numbers_info.is_array() || add_numbers_info.is_object(), + "Should get function info for add_numbers" + ); + + println!("✓ Successfully loaded debug info from .gnu_debuglink"); + println!(" Found main function"); + println!(" Found add_numbers function"); + + Ok(()) +} diff --git a/ghostscope/tests/fixtures/sample_program/Makefile b/ghostscope/tests/fixtures/sample_program/Makefile index d5043551..d6238a02 100644 --- a/ghostscope/tests/fixtures/sample_program/Makefile +++ b/ghostscope/tests/fixtures/sample_program/Makefile @@ -15,6 +15,16 @@ sample_program.o: sample_program.c sample_lib.h sample_lib.o: sample_lib.c sample_lib.h $(CC) $(CFLAGS) -c -o $@ sample_lib.c +# Build stripped binary with separate debug file (.gnu_debuglink) +sample_program_stripped: sample_program.o sample_lib.o + $(CC) $(CFLAGS) -o sample_program_stripped sample_program.o sample_lib.o + objcopy --only-keep-debug sample_program_stripped sample_program_stripped.debug + objcopy --strip-debug sample_program_stripped + objcopy --add-gnu-debuglink=sample_program_stripped.debug sample_program_stripped + @echo "Created stripped binary: sample_program_stripped" + @echo "Debug info in: sample_program_stripped.debug" + @readelf -x .gnu_debuglink sample_program_stripped 2>/dev/null || true + # Optimized builds with different levels sample_program_o1: sample_program_o1.o sample_lib_o1.o $(CC) $(BASE_CFLAGS) -O1 -o $@ $^ @@ -44,10 +54,10 @@ sample_lib_o2.o: sample_lib.c sample_lib.h sample_lib_o3.o: sample_lib.c sample_lib.h $(CC) $(BASE_CFLAGS) -O3 -DNDEBUG -c -o $@ sample_lib.c -# Build all optimization levels -all: sample_program sample_program_o1 sample_program_o2 sample_program_o3 +# Build all optimization levels (including stripped version for .gnu_debuglink testing) +all: sample_program sample_program_o1 sample_program_o2 sample_program_o3 sample_program_stripped clean: - rm -f *.o sample_program sample_program_o* + rm -f *.o sample_program sample_program_o* sample_program_stripped sample_program_stripped.debug -.PHONY: clean all sample_program_o1 sample_program_o2 sample_program_o3 \ No newline at end of file +.PHONY: clean all sample_program_o1 sample_program_o2 sample_program_o3 sample_program_stripped \ No newline at end of file diff --git a/ghostscope/tests/script_execution.rs b/ghostscope/tests/script_execution.rs index a71b0a75..6c496fd9 100644 --- a/ghostscope/tests/script_execution.rs +++ b/ghostscope/tests/script_execution.rs @@ -1346,3 +1346,114 @@ trace calculate_something { Ok(()) } + +#[tokio::test] +#[serial_test::serial] +async fn test_stripped_binary_with_debuglink() -> anyhow::Result<()> { + init(); + ensure_global_cleanup_registered(); + + // Compile stripped binary with separate debug file + common::ensure_test_program_compiled_with_opt(OptimizationLevel::Stripped)?; + + let script_content = r#" +trace add_numbers { + print "STRIPPED_BINARY: add_numbers called with a={} b={}", a, b; +} +"#; + + println!("=== Stripped Binary with .gnu_debuglink Test ==="); + + // Start stripped binary + let binary_path = + FIXTURES.get_test_binary_with_opt("sample_program", OptimizationLevel::Stripped)?; + + println!("Binary path: {}", binary_path.display()); + + // Verify debug file exists + let debug_file = binary_path.with_file_name("sample_program_stripped.debug"); + assert!( + debug_file.exists(), + "Debug file should exist: {}", + debug_file.display() + ); + println!("Debug file found: {}", debug_file.display()); + + // Verify binary is actually stripped + let output = std::process::Command::new("readelf") + .args(["-S", binary_path.to_str().unwrap()]) + .output()?; + let sections_output = String::from_utf8_lossy(&output.stdout); + + if sections_output.contains(".debug_info") { + println!("⚠️ Warning: Binary still contains .debug_info section"); + } else { + println!("✓ Binary is stripped (no .debug_info section)"); + } + + if sections_output.contains(".gnu_debuglink") { + println!("✓ Binary has .gnu_debuglink section"); + } else { + println!("⚠️ Warning: Binary missing .gnu_debuglink section"); + } + + // Start the stripped binary + let mut child = Command::new(&binary_path) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + + let pid = child.id().expect("Failed to get PID"); + println!("Started stripped binary with PID: {}", pid); + + // Give it time to start + tokio::time::sleep(Duration::from_millis(100)).await; + + // Run ghostscope with the stripped binary + let (exit_code, stdout, stderr) = + run_ghostscope_with_specific_pid(script_content, pid, 3).await?; + + println!("Exit code: {}", exit_code); + println!("STDOUT: {}", stdout); + println!("STDERR: {}", stderr); + println!("==============================================="); + + // Clean up + let _ = child.kill().await; + + // Verify results + if exit_code == 0 { + let traced_outputs = stdout + .lines() + .filter(|line| line.contains("STRIPPED_BINARY:")) + .count(); + + if traced_outputs > 0 { + println!( + "✓ Successfully traced {} function calls from stripped binary", + traced_outputs + ); + println!("✓ .gnu_debuglink mechanism working correctly"); + println!("✓ Uprobe offset calculation correct for stripped binary"); + } else { + println!("⚠️ No function calls captured, but debuglink loading succeeded"); + } + + // Verify that debug info was actually loaded from debuglink + if stderr.contains("Looking for debug file") + || stderr.contains("Loading DWARF from separate debug file") + { + println!("✓ Confirmed: Debug info loaded from .gnu_debuglink"); + } + } else { + // Check for specific error messages + if stderr.contains("No debug information found") { + println!("✗ Failed: Could not load debug information from .gnu_debuglink"); + anyhow::bail!("Debug information not found - .gnu_debuglink not working"); + } else { + println!("⚠️ Unexpected exit code: {}. STDERR: {}", exit_code, stderr); + } + } + + Ok(()) +}