Skip to content

Commit 2599548

Browse files
committed
fix: .BSS rebasing fix and coverage for globals
- derive a single ASLR bias per module and store that in proc_module_offsets so DW_OP_addr globals (especially .bss) map to the right runtime addresses - expand logging to show both module bias and reconstructed section addresses for easier verification - add Rust and C end-to-end tests that read .bss globals directly without pointer aliases to guard against regressions
1 parent b84acd2 commit 2599548

File tree

4 files changed

+149
-17
lines changed

4 files changed

+149
-17
lines changed

ghostscope-process/src/offsets.rs

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -385,18 +385,21 @@ impl ProcessManager {
385385
}
386386
}
387387
let mut offsets = SectionOffsets::default();
388-
if let Some(a0) = text_addr.and_then(find_bias_for) {
389-
offsets.text = a0;
390-
}
391-
if let Some(a1) = rodata_addr.and_then(find_bias_for) {
392-
offsets.rodata = a1;
393-
}
394-
if let Some(a2) = data_addr.and_then(find_bias_for) {
395-
offsets.data = a2;
396-
}
397-
if let Some(a3) = bss_addr.and_then(find_bias_for) {
398-
offsets.bss = a3;
399-
}
388+
// Each DW_OP_addr we encounter is an absolute link-time virtual address (e.g. 0x5798c for
389+
// G_COUNTER). To rebase it we only need the ASLR bias `module_base`, not per-section
390+
// runtime starts. Derive that bias from whichever segment we could match, then store it for
391+
// all four slots so the eBPF helper can simply do `link_addr + bias`.
392+
let module_base = text_addr
393+
.and_then(find_bias_for)
394+
.or_else(|| rodata_addr.and_then(find_bias_for))
395+
.or_else(|| data_addr.and_then(find_bias_for))
396+
.or_else(|| bss_addr.and_then(find_bias_for))
397+
.unwrap_or(0);
398+
399+
offsets.text = module_base;
400+
offsets.rodata = module_base;
401+
offsets.data = module_base;
402+
offsets.bss = module_base;
400403
let cookie = crate::cookie::from_path(module_path);
401404
let base = min_start.unwrap_or(0);
402405
let size = max_end.unwrap_or(base).saturating_sub(base);
@@ -419,17 +422,29 @@ impl ProcessManager {
419422
);
420423
}
421424
}
425+
let runtime_text = text_addr
426+
.map(|t| module_base.saturating_add(t))
427+
.unwrap_or(0);
428+
let runtime_ro = rodata_addr
429+
.map(|r| module_base.saturating_add(r))
430+
.unwrap_or(0);
431+
let runtime_data = data_addr
432+
.map(|d| module_base.saturating_add(d))
433+
.unwrap_or(0);
434+
let runtime_bss = bss_addr.map(|b| module_base.saturating_add(b)).unwrap_or(0);
435+
422436
tracing::debug!(
423-
"computed offsets: pid={} module='{}' cookie=0x{:016x} base=0x{:x} size=0x{:x} text=0x{:x} rodata=0x{:x} data=0x{:x} bss=0x{:x}",
437+
"computed offsets: pid={} module='{}' cookie=0x{:016x} base=0x{:x} size=0x{:x} module_bias=0x{:x} text=0x{:x} rodata=0x{:x} data=0x{:x} bss=0x{:x}",
424438
pid,
425439
module_path,
426440
cookie,
427441
base,
428442
size,
429443
offsets.text,
430-
offsets.rodata,
431-
offsets.data,
432-
offsets.bss
444+
runtime_text,
445+
runtime_ro,
446+
runtime_data,
447+
runtime_bss
433448
);
434449
Ok((cookie, offsets, base, size))
435450
}

ghostscope/src/cli/runtime.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::config::MergedConfig;
22
use crate::core::GhostSession;
33
use anyhow::Result;
4+
use std::io::{self, Write};
45
use tracing::{debug, error, info, warn};
56

67
/// Run GhostScope in command line mode with merged configuration
@@ -138,6 +139,11 @@ async fn run_cli_with_session(
138139
for line in formatted_output {
139140
println!(" {line}");
140141
}
142+
// When stdout is piped (as in tests), Rust switches to block buffering.
143+
// Flush explicitly so short event bursts appear before the process exits.
144+
if let Err(e) = io::stdout().flush() {
145+
warn!("Failed to flush event output: {e}");
146+
}
141147
}
142148

143149
// Also show raw debug info if needed (can be removed later)

ghostscope/tests/globals_execution.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2002,6 +2002,50 @@ trace globals_program.c:32 {
20022002
Ok(())
20032003
}
20042004

2005+
#[tokio::test]
2006+
async fn test_direct_bss_global_no_alias() -> anyhow::Result<()> {
2007+
// Focused regression: read the executable's .bss counter directly (without going through
2008+
// pointer aliases) to ensure rebasing logic works for zero-initialized globals.
2009+
init();
2010+
2011+
let binary_path = FIXTURES.get_test_binary("globals_program")?;
2012+
let bin_dir = binary_path.parent().unwrap();
2013+
let mut prog = Command::new(&binary_path)
2014+
.current_dir(bin_dir)
2015+
.stdout(Stdio::null())
2016+
.stderr(Stdio::null())
2017+
.spawn()?;
2018+
let pid = prog
2019+
.id()
2020+
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
2021+
tokio::time::sleep(Duration::from_millis(500)).await;
2022+
2023+
let script = r#"
2024+
trace globals_program.c:32 {
2025+
print "SBSS_ONLY:{}", s_bss_counter;
2026+
}
2027+
"#;
2028+
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
2029+
let _ = prog.kill().await.is_ok();
2030+
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
2031+
2032+
let re = Regex::new(r"SBSS_ONLY:(-?\d+)").unwrap();
2033+
let mut vals = Vec::new();
2034+
for line in stdout.lines() {
2035+
if let Some(c) = re.captures(line) {
2036+
vals.push(c[1].parse::<i64>().unwrap_or(0));
2037+
}
2038+
}
2039+
assert!(
2040+
vals.len() >= 2,
2041+
"Insufficient SBSS_ONLY events. STDOUT: {stdout}"
2042+
);
2043+
for pair in vals.windows(2) {
2044+
assert_eq!(pair[1] - pair[0], 3, "s_bss_counter should +3 per tick");
2045+
}
2046+
Ok(())
2047+
}
2048+
20052049
#[tokio::test]
20062050
async fn test_direct_global_cross_module() -> anyhow::Result<()> {
20072051
init();

ghostscope/tests/rust_script_execution.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,22 @@ trace do_stuff {
5050
}
5151
"#;
5252

53-
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
53+
let (exit_code, stdout, stderr) = common::runner::GhostscopeRunner::new()
54+
.with_script(script)
55+
.with_pid(pid)
56+
.timeout_secs(4)
57+
.enable_file_logging(true)
58+
.enable_sysmon_shared_lib(false)
59+
.run()
60+
.await?;
5461
let _ = prog.0.kill().await.is_ok();
62+
63+
let log_dump = tokio::fs::read_to_string("ghostscope.log")
64+
.await
65+
.unwrap_or_else(|e| format!("<ghostscope.log unavailable: {e}>"));
66+
println!("===== ghostscope.log =====\n{log_dump}");
67+
let _ = tokio::fs::remove_file("ghostscope.log").await;
68+
5569
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
5670

5771
assert!(
@@ -161,3 +175,56 @@ trace do_stuff {
161175
);
162176
Ok(())
163177
}
178+
179+
#[tokio::test]
180+
async fn test_rust_script_bss_counter_direct() -> anyhow::Result<()> {
181+
// Regression coverage: ensure we can read a pure .bss global (G_COUNTER) directly, without
182+
// relying on DWARF locals or pointer aliases.
183+
init();
184+
185+
let binary_path = FIXTURES.get_test_binary("rust_global_program")?;
186+
let bin_dir = binary_path.parent().unwrap();
187+
struct KillOnDrop(tokio::process::Child);
188+
impl Drop for KillOnDrop {
189+
fn drop(&mut self) {
190+
let _ = self.0.start_kill().is_ok();
191+
}
192+
}
193+
let mut cmd = Command::new(&binary_path);
194+
cmd.current_dir(bin_dir)
195+
.stdout(Stdio::null())
196+
.stderr(Stdio::null());
197+
let child = cmd.spawn()?;
198+
let pid = child.id().ok_or_else(|| anyhow::anyhow!("no pid"))?;
199+
let mut prog = KillOnDrop(child);
200+
tokio::time::sleep(Duration::from_millis(1500)).await;
201+
202+
let script = r#"
203+
trace touch_globals {
204+
print "BSSCNT:{}", G_COUNTER;
205+
}
206+
"#;
207+
208+
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 5, pid).await?;
209+
let _ = prog.0.kill().await.is_ok();
210+
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
211+
212+
let mut vals = Vec::new();
213+
for line in stdout.lines() {
214+
if let Some(pos) = line.find("BSSCNT:") {
215+
if let Some(num_str) = line[pos + "BSSCNT:".len()..].split_whitespace().next() {
216+
if let Ok(v) = num_str.parse::<i64>() {
217+
vals.push(v);
218+
}
219+
}
220+
}
221+
}
222+
assert!(
223+
vals.len() >= 2,
224+
"Insufficient BSSCNT events. STDOUT: {stdout}"
225+
);
226+
for pair in vals.windows(2) {
227+
assert_eq!(pair[1] - pair[0], 1, "G_COUNTER should +1 per tick");
228+
}
229+
Ok(())
230+
}

0 commit comments

Comments
 (0)