diff --git a/Cargo.lock b/Cargo.lock index 45645f2f..d07a11f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,6 +690,7 @@ version = "0.1.0" dependencies = [ "anyhow", "crc32fast", + "dirs", "futures", "ghostscope-platform", "ghostscope-protocol", diff --git a/config-zh.toml b/config-zh.toml index 103ee841..a1ae8d57 100644 --- a/config-zh.toml +++ b/config-zh.toml @@ -33,8 +33,35 @@ enable_logging = true log_level = "trace" [dwarf] -# DWARF 调试信息搜索路径(用于未来的 --debug-file 自动发现功能) -# 目前未实现,为未来使用保留 +# DWARF 调试信息搜索路径(用于 .gnu_debuglink 文件) +# +# 当二进制文件使用 .gnu_debuglink 引用独立的调试文件时, +# GhostScope 会在这些路径中搜索调试文件。 +# +# 搜索顺序(优先级从高到低): +# 1. 绝对路径(如果 .gnu_debuglink 包含绝对路径 - 罕见) +# 2. 用户配置的 search_paths + basename(此处配置) +# 3. 二进制文件所在目录 + basename +# 4. 二进制文件所在目录的 .debug 子目录 + basename +# +# 对于每个用户配置的路径,会检查两种位置: +# - <路径>/debug_文件名 +# - <路径>/.debug/debug_文件名 +# +# 特性: +# - 主目录展开:"~/" 会被替换为你的主目录 +# - 自动去除重复路径以避免冗余检查 +# - 按顺序尝试路径,直到找到匹配的调试文件 +# +# 常用搜索路径: +# - 系统调试符号:"/usr/lib/debug"(用于已安装的调试包) +# - 本地调试符号:"/usr/local/lib/debug" +# - 用户特定: "~/.local/lib/debug" +# - 自定义构建输出:"/path/to/build/debug" +# +# 注意:.gnu_debuglink 通常使用 basename(相对路径),但也支持绝对路径。 +# 如需使用系统范围的调试目录(如 /usr/lib/debug),请添加到 search_paths。 +# # 默认值:["/usr/lib/debug", "/usr/local/lib/debug"] search_paths = [ "/usr/lib/debug", @@ -96,3 +123,86 @@ output_dir = "." # 自动生成的实时日志文件的默认文件名前缀 # 默认值:"ghostscope_session" filename_prefix = "ghostscope_session" + +[ebpf] +# RingBuf 映射大小(字节,必须是 2 的幂) +# 控制用于从内核向用户空间传输跟踪事件的环形缓冲区大小。 +# 较大的尺寸允许缓冲更多事件,但会消耗更多内核内存。 +# 有效范围:4096 (4KB) 到 16777216 (16MB) +# 推荐值: +# - 低频追踪:131072 (128KB) +# - 中频追踪:262144 (256KB) +# - 高频追踪:524288 (512KB) 或 1048576 (1MB) +# 默认值:262144 (256KB) +ringbuf_size = 262144 + +# ASLR 地址转换的最大 (pid, module) 偏移条目数 +# 此映射存储每个进程中每个已加载模块的运行时地址偏移。 +# 每个条目存储 text/rodata/data/bss 段的偏移量。 +# 有效范围:64 到 65536 +# 推荐值: +# - 单进程:1024 +# - 多进程:4096 +# - 系统范围追踪:8192 或 16384 +# 默认值:4096 +proc_module_offsets_max_entries = 4096 + +# PerfEventArray 页数(每个 CPU 的 perf 缓冲区页数) +# 仅在选择 PerfEventArray 时使用(内核 < 5.8 或 force_perf_event_array=true 时) +# 必须是 2 的幂。大多数系统上每页为 4KB。 +# 有效范围:1 到 512 页 +# 推荐值: +# - 低频追踪:8 页 (每 CPU 32KB) +# - 中频追踪:32 页 (每 CPU 128KB) +# - 高频追踪:64 页 (每 CPU 256KB) +# 默认值:32 (每 CPU 128KB) +perf_page_count = 32 + +# 强制使用 PerfEventArray 而不是 RingBuf(仅用于测试) +# 警告:仅用于测试目的。正常情况下系统会自动检测内核能力, +# 并使用 RingBuf(内核 >= 5.8)或回退到 PerfEventArray。 +# 设置为 true 可在支持 RingBuf 的内核上强制使用 PerfEventArray。 +# 默认值:false +force_perf_event_array = false + +# 源代码路径配置 +# 当 DWARF 调试信息中包含的编译时路径与运行时路径不同时, +# 使用这些设置帮助 ghostscope 定位实际的源文件。 + +[source] +# 路径替换规则(首先应用,优先级最高) +# 将编译时路径前缀替换为运行时路径前缀。 +# 适用于源代码在不同机器上编译或移动到新位置的情况。 +# +# 使用场景示例: +# - 在 CI 服务器上编译:/home/build/project -> /home/user/work/project +# - 内核源码移动:/usr/src/linux-5.15 -> /home/user/kernel/linux-5.15 +# - 交叉编译:/buildroot/arm/src -> /local/embedded/src +# +# 格式:数组,包含 { from = "编译路径前缀", to = "运行时路径前缀" } +substitutions = [ + # { from = "/home/build/myproject", to = "/home/user/work/myproject" }, + # { from = "/usr/src/linux", to = "/home/user/kernel/linux" }, +] + +# 附加搜索目录(替换失败时的回退方案) +# 当通过替换无法找到源文件时,ghostscope 将在这些目录中 +# 按文件名(basename 匹配)进行搜索。 +# 类似于 GDB 的 "directory" 命令。 +# +# 格式:目录路径数组 +search_dirs = [ + # "/home/user/sources", + # "/opt/local/src", +] + +# 运行时配置: +# 你也可以使用 'srcpath' 命令交互式配置源路径: +# srcpath - 显示当前配置 +# srcpath map - 添加路径替换规则 +# srcpath add - 添加搜索目录 +# srcpath remove - 移除规则 +# srcpath clear - 清除所有运行时规则 +# srcpath reset - 重置为配置文件规则 +# +# 运行时规则优先于配置文件规则,且不会持久化保存。 diff --git a/config.toml b/config.toml index 592c0ce9..13ae58dc 100644 --- a/config.toml +++ b/config.toml @@ -33,8 +33,36 @@ enable_logging = true log_level = "trace" [dwarf] -# DWARF debug information search paths (for future --debug-file auto-discovery) -# Currently not implemented, reserved for future use +# DWARF debug information search paths for .gnu_debuglink files +# +# When a binary uses .gnu_debuglink to reference separate debug files, +# GhostScope searches these paths to locate the debug file. +# +# Search order (highest priority first): +# 1. Absolute path (if .gnu_debuglink contains an absolute path - rare) +# 2. User-configured search_paths + basename (configured here) +# 3. Same directory as the binary + basename +# 4. .debug subdirectory next to the binary + basename +# +# For each user-configured path, both direct and .debug subdirectory are checked: +# - /debug_filename +# - /.debug/debug_filename +# +# Features: +# - Home directory expansion: "~/" is replaced with your home directory +# - Duplicate paths are automatically removed to avoid redundant checks +# - Paths are tried in order until a matching debug file is found +# +# Common search paths: +# - System debug symbols: "/usr/lib/debug" (for installed debug packages) +# - Local debug symbols: "/usr/local/lib/debug" +# - User-specific: "~/.local/lib/debug" +# - Custom build output: "/path/to/build/debug" +# +# Note: .gnu_debuglink typically uses basename (relative path), but absolute paths +# are also supported. If you need system-wide debug directories like /usr/lib/debug, +# add them to search_paths. +# # Default: ["/usr/lib/debug", "/usr/local/lib/debug"] search_paths = [ "/usr/lib/debug", diff --git a/docs/configuration.md b/docs/configuration.md index 531b6662..52083056 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -59,11 +59,18 @@ ghostscope --debug-file /path/to/binary.debug # Auto-detection searches in order: # 1. Binary itself (.debug_info sections) -# 2. .gnu_debuglink section +# 2. .gnu_debuglink section (see search paths below) # 3. .gnu_debugdata section (Android/compressed) -# 4. /usr/lib/debug, /usr/local/lib/debug -# 5. Build-ID based paths -# 6. binary.debug, binary.dbg +# 4. Build-ID based paths + +# .gnu_debuglink search paths (configurable in config.toml): +# 1. Absolute path (if .gnu_debuglink contains absolute path - rare) +# 2. User-configured search_paths + basename (highest priority) +# 3. Same directory as the binary + basename +# 4. .debug subdirectory next to the binary + basename +# +# Note: To use system-wide debug directories like /usr/lib/debug, +# add them to search_paths in config.toml ``` ### Logging Configuration @@ -176,10 +183,35 @@ enable_console_logging = false log_level = "warn" [dwarf] -# Debug information search paths +# Debug information search paths for .gnu_debuglink files +# When a binary uses .gnu_debuglink to reference separate debug files, +# GhostScope searches these paths to locate the debug file. +# +# Search order (highest priority first): +# 1. Absolute path (if .gnu_debuglink contains an absolute path - rare) +# 2. User-configured search_paths + basename (configured here) +# 3. Same directory as the binary + basename +# 4. .debug subdirectory next to the binary + basename +# +# For each user-configured path, both direct and .debug subdirectory are checked: +# - /debug_filename +# - /.debug/debug_filename +# +# Features: +# - Home directory expansion: "~/" is replaced with your home directory +# - Duplicate paths are automatically removed to avoid redundant checks +# - Paths are tried in order until a matching debug file is found +# +# Note: .gnu_debuglink typically uses basename (relative path), but absolute paths +# are also supported. If you need system-wide debug directories like /usr/lib/debug, +# add them to search_paths. +# +# Examples: search_paths = [ - "/usr/lib/debug", - "/usr/local/lib/debug" + "/usr/lib/debug", # System debug symbols (for installed packages) + "/usr/local/lib/debug", # Local debug symbols + "~/.local/lib/debug", # User debug symbols (~ expands to home) + "/opt/debug-symbols" # Custom debug symbol server ] [files] diff --git a/docs/install.md b/docs/install.md index d1984971..80b2b615 100644 --- a/docs/install.md +++ b/docs/install.md @@ -134,6 +134,8 @@ GhostScope automatically searches for debug files in the following locations: 2. `.debug` subdirectory: `/path/to/.debug/your_program.debug` 3. Global debug directory: `/usr/lib/debug/path/to/your_program.debug` +> **📝 Custom Search Paths**: You can configure additional search paths (including user-specific directories like `~/.local/lib/debug`) in the configuration file. See the [Configuration Reference - DWARF Debug Search Paths](configuration.md#dwarf) for detailed information. + **Installing system debug packages:** ```bash # Ubuntu/Debian - install debug symbols for libc diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 0d2bf626..c1e77d56 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -59,11 +59,18 @@ ghostscope --debug-file /path/to/binary.debug # 自动检测按以下顺序搜索: # 1. 二进制文件本身(.debug_info 节) -# 2. .gnu_debuglink 节 +# 2. .gnu_debuglink 节(参见下方搜索路径) # 3. .gnu_debugdata 节(Android/压缩格式) -# 4. /usr/lib/debug, /usr/local/lib/debug -# 5. 基于 Build-ID 的路径 -# 6. binary.debug, binary.dbg +# 4. 基于 Build-ID 的路径 + +# .gnu_debuglink 搜索路径(可在 config.toml 中配置): +# 1. 绝对路径(如果 .gnu_debuglink 包含绝对路径 - 罕见) +# 2. 用户配置的 search_paths + basename(最高优先级) +# 3. 二进制文件同目录 + basename +# 4. 二进制文件同目录的 .debug 子目录 + basename +# +# 注意:如需使用系统范围的调试目录(如 /usr/lib/debug), +# 请在 config.toml 的 search_paths 中添加 ``` ### 日志配置 @@ -176,10 +183,34 @@ enable_console_logging = false log_level = "warn" [dwarf] -# 调试信息搜索路径 +# DWARF 调试信息搜索路径(用于 .gnu_debuglink 文件) +# 当二进制文件使用 .gnu_debuglink 引用独立的调试文件时, +# GhostScope 会在这些路径中搜索调试文件。 +# +# 搜索顺序(优先级从高到低): +# 1. 绝对路径(如果 .gnu_debuglink 包含绝对路径 - 罕见) +# 2. 用户配置的 search_paths + basename(此处配置) +# 3. 二进制文件所在目录 + basename +# 4. 二进制文件所在目录的 .debug 子目录 + basename +# +# 对于每个用户配置的路径,会检查两种位置: +# - <路径>/debug_文件名 +# - <路径>/.debug/debug_文件名 +# +# 特性: +# - 主目录展开:"~/" 会被替换为你的主目录 +# - 自动去除重复路径以避免冗余检查 +# - 按顺序尝试路径,直到找到匹配的调试文件 +# +# 注意:.gnu_debuglink 通常使用 basename(相对路径),但也支持绝对路径。 +# 如需使用系统范围的调试目录(如 /usr/lib/debug),请添加到 search_paths。 +# +# 示例: search_paths = [ - "/usr/lib/debug", - "/usr/local/lib/debug" + "/usr/lib/debug", # 系统调试符号(用于已安装的软件包) + "/usr/local/lib/debug", # 本地调试符号 + "~/.local/lib/debug", # 用户调试符号(~ 会展开为主目录) + "/opt/debug-symbols" # 自定义调试符号服务器 ] [files] diff --git a/docs/zh/install.md b/docs/zh/install.md index e714e14b..6ef3271c 100644 --- a/docs/zh/install.md +++ b/docs/zh/install.md @@ -134,6 +134,8 @@ GhostScope 会自动在以下位置搜索调试文件: 2. `.debug` 子目录:`/path/to/.debug/your_program.debug` 3. 全局调试目录:`/usr/lib/debug/path/to/your_program.debug` +> **📝 自定义搜索路径**:你可以在配置文件中配置额外的搜索路径(包括用户特定目录如 `~/.local/lib/debug`)。详细信息请参阅 [配置参考 - DWARF 调试搜索路径](configuration.md#dwarf)。 + **安装系统调试包:** ```bash # Ubuntu/Debian - 安装 libc 的调试符号 diff --git a/ghostscope-dwarf/Cargo.toml b/ghostscope-dwarf/Cargo.toml index 814f7d0a..dd5e8ef5 100644 --- a/ghostscope-dwarf/Cargo.toml +++ b/ghostscope-dwarf/Cargo.toml @@ -27,3 +27,6 @@ libc = "0.2" # For .gnu_debuglink CRC validation crc32fast = "1.4" +# For home directory expansion in debug search paths +dirs = "5.0" + diff --git a/ghostscope-dwarf/src/analyzer.rs b/ghostscope-dwarf/src/analyzer.rs index f145ac30..88fc50d8 100644 --- a/ghostscope-dwarf/src/analyzer.rs +++ b/ghostscope-dwarf/src/analyzer.rs @@ -135,11 +135,23 @@ impl DwarfAnalyzer { /// Create DWARF analyzer from PID using parallel loading pub async fn from_pid_parallel(pid: u32) -> Result { - Self::from_pid_parallel_with_progress(pid, |_event| {}).await + Self::from_pid_parallel_with_config(pid, &[], |_event| {}).await } /// Create DWARF analyzer from PID using parallel loading with progress callback pub async fn from_pid_parallel_with_progress(pid: u32, progress_callback: F) -> Result + where + F: Fn(ModuleLoadingEvent) + Send + Sync + 'static, + { + Self::from_pid_parallel_with_config(pid, &[], progress_callback).await + } + + /// Create DWARF analyzer from PID using parallel loading with debug search paths and progress callback + pub async fn from_pid_parallel_with_config( + pid: u32, + debug_search_paths: &[String], + progress_callback: F, + ) -> Result where F: Fn(ModuleLoadingEvent) + Send + Sync + 'static, { @@ -164,8 +176,14 @@ impl DwarfAnalyzer { } // Load all modules in parallel with progress tracking - let modules = crate::loader::ModuleLoader::new(module_mappings) - .parallel() + let mut loader = crate::loader::ModuleLoader::new(module_mappings).parallel(); + + // Configure debug search paths if provided + if !debug_search_paths.is_empty() { + loader = loader.with_debug_search_paths(debug_search_paths.to_vec()); + } + + let modules = loader .with_progress_callback(progress_callback) .load() .await?; @@ -181,6 +199,14 @@ impl DwarfAnalyzer { /// Create DWARF analyzer from executable path (single module mode, now async parallel) pub async fn from_exec_path>(exec_path: P) -> Result { + Self::from_exec_path_with_config(exec_path, &[]).await + } + + /// Create DWARF analyzer from executable path with debug search paths + pub async fn from_exec_path_with_config>( + exec_path: P, + debug_search_paths: &[String], + ) -> Result { let exec_path = exec_path.as_ref().to_path_buf(); tracing::info!( "Creating DWARF analyzer for executable: {}", @@ -201,7 +227,7 @@ impl DwarfAnalyzer { }; // Load the single module using parallel loading - match ModuleData::load_parallel(module_mapping).await { + match ModuleData::load_parallel(module_mapping, debug_search_paths).await { Ok(module_data) => { analyzer.modules.insert(exec_path.clone(), module_data); tracing::info!( diff --git a/ghostscope-dwarf/src/debuglink.rs b/ghostscope-dwarf/src/debuglink.rs index d9f3d8eb..610d82c8 100644 --- a/ghostscope-dwarf/src/debuglink.rs +++ b/ghostscope-dwarf/src/debuglink.rs @@ -5,19 +5,35 @@ use crate::core::Result; use object::Object; +use std::collections::HashSet; 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 +/// 1. Absolute path (if .gnu_debuglink contains an absolute path) +/// 2. User-configured search paths + basename (from config file, highest priority) +/// 3. Same directory as binary + basename +/// 4. .debug subdirectory + basename +/// +/// Note: If .gnu_debuglink contains an absolute path (e.g., /usr/lib/debug/foo.debug), +/// the function will: +/// - First try the absolute path directly +/// - Then extract basename (foo.debug) and search in all configured paths +/// +/// This ensures maximum flexibility: +/// - Absolute paths are honored if they exist +/// - But custom search_paths can still provide alternatives via basename +/// +/// For system-wide debug directories (like /usr/lib/debug), configure them in search_paths. /// /// 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> { +pub fn find_debug_file>( + binary_path: P, + user_search_paths: &[String], +) -> Result> { let binary_path = binary_path.as_ref(); // Read binary and check for .gnu_debuglink section @@ -57,7 +73,7 @@ pub fn find_debug_file>(binary_path: P) -> Result ); // Build search paths following GDB's strategy - let search_paths = build_search_paths(binary_path, debug_filename); + let search_paths = build_search_paths(binary_path, debug_filename, user_search_paths); // Try each path and verify CRC + build ID for candidate_path in search_paths { @@ -97,31 +113,93 @@ pub fn find_debug_file>(binary_path: P) -> Result Ok(None) } +/// Expand home directory in path (e.g., ~/.local/debug -> /home/user/.local/debug) +fn expand_home_dir(path: &str) -> PathBuf { + if let Some(stripped) = path.strip_prefix("~/") { + if let Some(home) = dirs::home_dir() { + let expanded = home.join(stripped); + tracing::debug!( + "Expanded home directory: {} -> {}", + path, + expanded.display() + ); + return expanded; + } else { + tracing::warn!( + "Failed to expand home directory for path '{}', using as-is", + path + ); + } + } + PathBuf::from(path) +} + /// Build search paths for debug file following GDB conventions -fn build_search_paths(binary_path: &Path, debug_filename: &Path) -> Vec { +/// +/// Search order (highest priority first): +/// 1. Absolute path (if debug_filename is absolute) +/// 2. User-configured search paths (from config file) +/// 3. Same directory as binary +/// 4. .debug subdirectory +/// +/// Note: +/// - If debug_filename is an absolute path, it will be tried first, then basename extracted +/// - Paths are deduplicated to avoid redundant filesystem checks +/// - Global debug directories (like /usr/lib/debug) should be configured via search_paths +fn build_search_paths( + binary_path: &Path, + debug_filename: &Path, + user_search_paths: &[String], +) -> Vec { let mut paths = Vec::new(); + let mut seen = HashSet::new(); + + // Helper to add path only if not already seen + let mut add_path = |path: PathBuf| { + if seen.insert(path.clone()) { + paths.push(path); + } + }; + + // 1. If debug_filename is an absolute path, try it first + if debug_filename.is_absolute() { + add_path(debug_filename.to_path_buf()); + } + + // Extract basename for searching in configured paths + // This handles both absolute paths (e.g., /usr/lib/debug/foo.debug -> foo.debug) + // and relative paths (e.g., foo.debug -> foo.debug) + let basename = debug_filename + .file_name() + .map(Path::new) + .unwrap_or(debug_filename); + + // 2. User-configured search paths (highest priority) + // For each user path, try both: + // - user_path/basename + // - user_path/.debug/basename + for user_path in user_search_paths { + let expanded = expand_home_dir(user_path); + add_path(expanded.join(basename)); + add_path(expanded.join(".debug").join(basename)); + } // Get binary directory let binary_dir = binary_path.parent(); - // 1. Same directory as binary + // 3. Same directory as binary if let Some(dir) = binary_dir { - paths.push(dir.join(debug_filename)); + add_path(dir.join(basename)); } - // 2. .debug subdirectory + // 4. .debug subdirectory if let Some(dir) = binary_dir { - paths.push(dir.join(".debug").join(debug_filename)); + add_path(dir.join(".debug").join(basename)); } - // 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); - } + // Note: Global debug directories (like /usr/lib/debug) should be configured + // in user_search_paths if needed. This avoids generating nonsensical paths + // like /usr/lib/debug/mnt/500g/... for non-system binaries. paths } @@ -218,10 +296,11 @@ fn calculate_gnu_debuglink_crc(data: &[u8]) -> u32 { /// This is the main entry point for loading debug info pub fn try_load_debug_file>( binary_path: P, + user_search_paths: &[String], ) -> Result> { let binary_path = binary_path.as_ref(); - match find_debug_file(binary_path)? { + match find_debug_file(binary_path, user_search_paths)? { Some(debug_path) => { tracing::info!( "Loading debug info from separate file: {}", @@ -242,19 +321,107 @@ mod tests { use super::*; #[test] - fn test_build_search_paths() { + fn test_build_search_paths_no_user_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); + let paths = build_search_paths(binary_path, debug_filename, &[]); - assert_eq!(paths.len(), 3); + assert_eq!(paths.len(), 2); assert_eq!(paths[0], Path::new("/usr/bin/my_program.debug")); assert_eq!(paths[1], Path::new("/usr/bin/.debug/my_program.debug")); + } + + #[test] + fn test_build_search_paths_with_user_paths() { + let binary_path = Path::new("/usr/bin/my_program"); + let debug_filename = Path::new("my_program.debug"); + let user_paths = vec!["/opt/debug".to_string(), "/home/user/.debug".to_string()]; + + let paths = build_search_paths(binary_path, debug_filename, &user_paths); + + // Should have: 2 user paths * 2 (direct + .debug) + 2 standard paths = 6 total + assert_eq!(paths.len(), 6); + + // User paths come first (highest priority) + assert_eq!(paths[0], Path::new("/opt/debug/my_program.debug")); + assert_eq!(paths[1], Path::new("/opt/debug/.debug/my_program.debug")); + assert_eq!(paths[2], Path::new("/home/user/.debug/my_program.debug")); assert_eq!( - paths[2], - Path::new("/usr/lib/debug/usr/bin/my_program.debug") + paths[3], + Path::new("/home/user/.debug/.debug/my_program.debug") ); + + // Then standard paths + assert_eq!(paths[4], Path::new("/usr/bin/my_program.debug")); + assert_eq!(paths[5], Path::new("/usr/bin/.debug/my_program.debug")); + } + + #[test] + fn test_expand_home_dir() { + let expanded = expand_home_dir("~/test/path"); + + // Should replace ~ with home directory + if let Some(home) = dirs::home_dir() { + assert_eq!(expanded, home.join("test/path")); + } + + // Non-home paths should be unchanged + let regular_path = expand_home_dir("/usr/local/debug"); + assert_eq!(regular_path, Path::new("/usr/local/debug")); + } + + #[test] + fn test_path_deduplication() { + // Test that duplicate paths are removed + let binary_path = Path::new("/usr/bin/my_program"); + let debug_filename = Path::new("my_program.debug"); + // Configure /usr/bin which is the same as binary directory + let user_paths = vec!["/usr/bin".to_string()]; + + let paths = build_search_paths(binary_path, debug_filename, &user_paths); + + // Should deduplicate: + // User path: /usr/bin/my_program.debug (same as standard path #1) + // User path: /usr/bin/.debug/my_program.debug (same as standard path #2) + // Standard: /usr/bin/my_program.debug (duplicate, skipped) + // Standard: /usr/bin/.debug/my_program.debug (duplicate, skipped) + assert_eq!(paths.len(), 2); // Only 2 unique paths + + // Verify user paths come first (priority) + assert_eq!(paths[0], Path::new("/usr/bin/my_program.debug")); + assert_eq!(paths[1], Path::new("/usr/bin/.debug/my_program.debug")); + } + + #[test] + fn test_absolute_path_debug_filename() { + // Test handling of absolute path in debug_filename + let binary_path = Path::new("/usr/bin/my_program"); + let debug_filename = Path::new("/usr/lib/debug/my_program.debug"); + let user_paths = vec!["/opt/debug".to_string()]; + + let paths = build_search_paths(binary_path, debug_filename, &user_paths); + + // Should try: + // 1. Absolute path first: /usr/lib/debug/my_program.debug + // 2. Extract basename (my_program.debug) and search in user paths + // 3. Extract basename and search in standard locations + + // First path should be the absolute path + assert_eq!(paths[0], Path::new("/usr/lib/debug/my_program.debug")); + + // Then user-configured paths with basename + assert_eq!(paths[1], Path::new("/opt/debug/my_program.debug")); + assert_eq!(paths[2], Path::new("/opt/debug/.debug/my_program.debug")); + + // Then standard paths with basename + assert_eq!(paths[3], Path::new("/usr/bin/my_program.debug")); + assert_eq!(paths[4], Path::new("/usr/bin/.debug/my_program.debug")); + + // Verify basename was correctly extracted + assert!(paths + .iter() + .all(|p| p.file_name().unwrap() == "my_program.debug")); } #[test] diff --git a/ghostscope-dwarf/src/loader.rs b/ghostscope-dwarf/src/loader.rs index ece64265..955d3a58 100644 --- a/ghostscope-dwarf/src/loader.rs +++ b/ghostscope-dwarf/src/loader.rs @@ -14,12 +14,15 @@ use tokio::task; pub struct LoadConfig { /// Maximum number of concurrent module loads pub max_module_concurrency: usize, + /// Debug file search paths (for .gnu_debuglink) + pub debug_search_paths: Vec, } impl Default for LoadConfig { fn default() -> Self { Self { max_module_concurrency: num_cpus::get(), + debug_search_paths: Vec::new(), } } } @@ -29,6 +32,7 @@ impl LoadConfig { pub fn fast() -> Self { Self { max_module_concurrency: num_cpus::get(), + debug_search_paths: Vec::new(), } } } @@ -54,6 +58,12 @@ impl ModuleLoader { self } + /// Set debug search paths for .gnu_debuglink files + pub fn with_debug_search_paths(mut self, paths: Vec) -> Self { + self.config.debug_search_paths = paths; + self + } + /// Load with progress callback - always parallel pub async fn load_with_progress(self, progress_callback: F) -> Result> where @@ -89,6 +99,7 @@ impl ModuleLoader { let total_modules = self.mappings.len(); let progress_callback = Arc::new(progress_callback); + let debug_search_paths = Arc::new(self.config.debug_search_paths.clone()); let tasks: Vec<_> = self .mappings @@ -97,6 +108,7 @@ impl ModuleLoader { .map(|(index, mapping)| { let semaphore = semaphore.clone(); let progress_callback = progress_callback.clone(); + let debug_search_paths = debug_search_paths.clone(); task::spawn(async move { let _permit = semaphore.acquire().await.unwrap(); @@ -112,7 +124,7 @@ impl ModuleLoader { let start_time = std::time::Instant::now(); - let result = ModuleData::load_parallel(mapping).await; + let result = ModuleData::load_parallel(mapping, &debug_search_paths).await; let load_time_ms = start_time.elapsed().as_millis() as u64; diff --git a/ghostscope-dwarf/src/module/data.rs b/ghostscope-dwarf/src/module/data.rs index a7773937..d1cdb63b 100644 --- a/ghostscope-dwarf/src/module/data.rs +++ b/ghostscope-dwarf/src/module/data.rs @@ -66,9 +66,12 @@ pub(crate) struct ModuleData { impl ModuleData { /// Parallel loading: debug_info || debug_line || CFI simultaneously - pub(crate) async fn load_parallel(module_mapping: ModuleMapping) -> Result { + pub(crate) async fn load_parallel( + module_mapping: ModuleMapping, + debug_search_paths: &[String], + ) -> Result { tracing::info!("Parallel loading for: {}", module_mapping.path.display()); - Self::load_internal_parallel(module_mapping).await + Self::load_internal_parallel(module_mapping, debug_search_paths).await } /// Resolve a struct/class type by name using only indexes + shallow resolution (no scanning). @@ -189,7 +192,10 @@ impl ModuleData { } /// Parallel internal load implementation - true parallelism for debug_info || debug_line || CFI - async fn load_internal_parallel(module_mapping: ModuleMapping) -> Result { + async fn load_internal_parallel( + module_mapping: ModuleMapping, + debug_search_paths: &[String], + ) -> Result { tracing::debug!( "Loading module in parallel: {}", module_mapping.path.display() @@ -220,7 +226,10 @@ impl ModuleData { "No debug info in binary, searching for .gnu_debuglink: {}", module_mapping.path.display() ); - match crate::debuglink::try_load_debug_file(&module_mapping.path)? { + match crate::debuglink::try_load_debug_file( + &module_mapping.path, + debug_search_paths, + )? { Some((debug_path, debug_mmap)) => { tracing::info!( "Loading DWARF from separate debug file: {}", diff --git a/ghostscope/src/core/session.rs b/ghostscope/src/core/session.rs index 64bcfa00..07c23c5f 100644 --- a/ghostscope/src/core/session.rs +++ b/ghostscope/src/core/session.rs @@ -73,16 +73,29 @@ impl GhostSession { } } + /// Get debug search paths from configuration + fn get_debug_search_paths(&self) -> Vec { + self.config + .as_ref() + .map(|c| c.dwarf_search_paths.clone()) + .unwrap_or_default() + } + /// Load binary and perform DWARF analysis using parallel loading (TUI mode) pub async fn load_binary_parallel(&mut self) -> Result<()> { info!("Loading binary and performing DWARF analysis (parallel mode)"); + let debug_search_paths = self.get_debug_search_paths(); + let process_analyzer = if let Some(pid) = self.target_pid { info!("Loading binary from PID: {} (parallel)", pid); - Some(DwarfAnalyzer::from_pid_parallel(pid).await?) + Some( + DwarfAnalyzer::from_pid_parallel_with_config(pid, &debug_search_paths, |_| {}) + .await?, + ) } else if let Some(ref binary_path) = self.target_binary { info!("Loading binary from executable path: {}", binary_path); - Some(DwarfAnalyzer::from_exec_path(binary_path).await?) + Some(DwarfAnalyzer::from_exec_path_with_config(binary_path, &debug_search_paths).await?) } else { warn!("No PID or binary path specified - running without binary analysis"); None @@ -102,13 +115,21 @@ impl GhostSession { { info!("Loading binary and performing DWARF analysis (parallel mode with progress)"); + let debug_search_paths = self.get_debug_search_paths(); + let process_analyzer = if let Some(pid) = self.target_pid { info!("Loading binary from PID: {} (parallel with progress)", pid); - Some(DwarfAnalyzer::from_pid_parallel_with_progress(pid, progress_callback).await?) + Some( + DwarfAnalyzer::from_pid_parallel_with_config( + pid, + &debug_search_paths, + progress_callback, + ) + .await?, + ) } else if let Some(ref binary_path) = self.target_binary { info!("Loading binary from executable path: {}", binary_path); - // Note: from_exec_path doesn't support progress callbacks yet - Some(DwarfAnalyzer::from_exec_path(binary_path).await?) + Some(DwarfAnalyzer::from_exec_path_with_config(binary_path, &debug_search_paths).await?) } else { warn!("No PID or binary path specified - running without binary analysis"); None @@ -137,15 +158,15 @@ impl GhostSession { Ok(session) } - /// Create a new session with binary loading in parallel mode with progress callback - pub async fn new_with_binary_parallel_with_progress( - args: &ParsedArgs, + /// Create a new session with config and binary loading in parallel mode with progress callback + pub async fn new_with_config_and_progress( + config: &MergedConfig, progress_callback: F, ) -> Result where F: Fn(ghostscope_dwarf::ModuleLoadingEvent) + Send + Sync + 'static, { - let mut session = Self::new(args); + let mut session = Self::new_with_config(config); session .load_binary_parallel_with_progress(progress_callback) .await?; @@ -197,19 +218,6 @@ impl GhostSession { pub fn is_target_mode(&self) -> bool { self.target_pid.is_none() && self.target_binary.is_some() } - - /// Update source path resolver from merged config - /// This should be called after setting session.config to ensure resolver has correct config - pub fn update_source_resolver_from_config(&mut self) { - if let Some(ref config) = self.config { - self.source_path_resolver = SourcePathResolver::new(&config.source); - info!( - "Source path resolver updated from config: {} substitutions, {} search dirs", - config.source.substitutions.len(), - config.source.search_dirs.len() - ); - } - } } #[cfg(test)] @@ -218,8 +226,8 @@ mod tests { use crate::config::settings::{PathSubstitution, SourceConfig}; #[test] - fn test_update_source_resolver_from_config() { - // Create a session with default (empty) resolver + fn test_new_with_config_sets_source_resolver() { + // Create a merged config with source settings let args = ParsedArgs { binary_path: None, target_path: None, @@ -243,14 +251,6 @@ mod tests { force_perf_event_array: false, }; - let mut session = GhostSession::new(&args); - - // Initially resolver should have no rules - let initial_rules = session.source_path_resolver.get_all_rules(); - assert_eq!(initial_rules.config_substitution_count, 0); - assert_eq!(initial_rules.config_search_dir_count, 0); - - // Create a merged config with source settings let config = crate::config::Config { source: SourceConfig { substitutions: vec![ @@ -270,29 +270,26 @@ mod tests { let merged_config = MergedConfig::new(args, config); - // Set the config on the session - session.config = Some(merged_config); - - // Update the resolver from config - session.update_source_resolver_from_config(); + // Create session with config - should automatically set resolver + let session = GhostSession::new_with_config(&merged_config); - // Now resolver should have the config rules - let updated_rules = session.source_path_resolver.get_all_rules(); - assert_eq!(updated_rules.config_substitution_count, 2); - assert_eq!(updated_rules.config_search_dir_count, 1); + // Verify resolver was set correctly from config + let rules = session.source_path_resolver.get_all_rules(); + assert_eq!(rules.config_substitution_count, 2); + assert_eq!(rules.config_search_dir_count, 1); // Verify the substitutions are present - assert!(updated_rules + assert!(rules .substitutions .iter() .any(|s| s.from == "/build/path" && s.to == "/local/path")); - assert!(updated_rules + assert!(rules .substitutions .iter() .any(|s| s.from == "/usr/src" && s.to == "/home/src")); // Verify search dir is present - assert!(updated_rules + assert!(rules .search_dirs .contains(&"/home/user/sources".to_string())); } diff --git a/ghostscope/src/runtime/coordinator.rs b/ghostscope/src/runtime/coordinator.rs index 3b7dffa2..583fa122 100644 --- a/ghostscope/src/runtime/coordinator.rs +++ b/ghostscope/src/runtime/coordinator.rs @@ -81,16 +81,12 @@ async fn run_tui_coordinator_with_ui_config_and_merged_config( // Initialize DWARF information processing in background let dwarf_task = { - let parsed_args_clone = parsed_args.clone(); let status_sender = runtime_channels.create_status_sender(); let config_clone = merged_config.clone(); tokio::spawn(async move { - let mut session = - dwarf_loader::initialize_dwarf_processing(parsed_args_clone, status_sender).await?; - // Inject MergedConfig into session for eBPF map configuration - session.config = Some(config_clone); - // Update source path resolver with config after setting it - session.update_source_resolver_from_config(); + // Pass MergedConfig directly to ensure search_paths are available during DWARF loading + let session = + dwarf_loader::initialize_dwarf_processing(&config_clone, status_sender).await?; Ok::<_, anyhow::Error>(session) }) }; diff --git a/ghostscope/src/runtime/dwarf_loader.rs b/ghostscope/src/runtime/dwarf_loader.rs index a71ae1a5..34e6f576 100644 --- a/ghostscope/src/runtime/dwarf_loader.rs +++ b/ghostscope/src/runtime/dwarf_loader.rs @@ -1,4 +1,4 @@ -use crate::config::ParsedArgs; +use crate::config::MergedConfig; use crate::core::GhostSession; use anyhow::Result; use ghostscope_dwarf::ModuleLoadingEvent; @@ -60,15 +60,15 @@ fn convert_loading_event_to_runtime_status(event: ModuleLoadingEvent) -> Runtime /// Initialize DWARF processing in background pub async fn initialize_dwarf_processing( - parsed_args: ParsedArgs, + config: &MergedConfig, status_sender: tokio::sync::mpsc::UnboundedSender, ) -> Result { - initialize_dwarf_processing_with_progress(parsed_args, status_sender).await + initialize_dwarf_processing_with_progress(config, status_sender).await } /// Initialize DWARF processing in background with detailed progress reporting pub async fn initialize_dwarf_processing_with_progress( - parsed_args: ParsedArgs, + config: &MergedConfig, status_sender: tokio::sync::mpsc::UnboundedSender, ) -> Result { // Send status update: starting DWARF loading @@ -84,9 +84,8 @@ pub async fn initialize_dwarf_processing_with_progress( }; // Create debug session for DWARF processing with parallel loading and progress - match GhostSession::new_with_binary_parallel_with_progress(&parsed_args, progress_callback) - .await - { + // Use config to ensure search_paths are available during DWARF loading + match GhostSession::new_with_config_and_progress(config, progress_callback).await { Ok(session) => { // Validate that we have process analysis information match session.get_module_stats() { @@ -123,8 +122,8 @@ pub async fn initialize_dwarf_processing_with_progress( None => { let error_msg = format!( "Binary analysis failed! Cannot load DWARF information for PID {} or binary path {:?}", - parsed_args.pid.unwrap_or(0), - parsed_args.binary_path + config.pid.unwrap_or(0), + config.binary_path ); let _ = status_sender.send(RuntimeStatus::DwarfLoadingFailed(error_msg.clone()));