Skip to content

Commit 434383b

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 434383b

File tree

5 files changed

+144
-23
lines changed

5 files changed

+144
-23
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/src/logging.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,9 @@ pub fn initialize_logging_with_config(
3535
// Initialize LogTracer but ignore 'already set' errors to avoid noisy output
3636
let _ = tracing_log::LogTracer::init();
3737

38-
// Build EnvFilter from RUST_LOG if present; otherwise fall back to configured log_level
39-
// This enables module-level filtering like: RUST_LOG="info,ghostscope_loader=debug,ghostscope_protocol=debug"
40-
let env_filter =
41-
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level.to_string()));
38+
let make_env_filter = || {
39+
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level.to_string()))
40+
};
4241

4342
let event_format = tracing_subscriber::fmt::format()
4443
.with_target(true)
@@ -69,6 +68,7 @@ pub fn initialize_logging_with_config(
6968

7069
if enable_console_logging {
7170
// Console logging enabled: dual output to file and stdout with level filter
71+
let env_filter = make_env_filter();
7272
let stdout_layer = tracing_subscriber::fmt::layer()
7373
.event_format(event_format.clone())
7474
.with_writer(std::io::stdout)
@@ -82,6 +82,7 @@ pub fn initialize_logging_with_config(
8282
let _ = init_res;
8383
} else {
8484
// Console logging disabled: only log to file
85+
let env_filter = make_env_filter();
8586
let init_res = tracing_subscriber::registry()
8687
.with(env_filter)
8788
.with(file_layer)
@@ -92,6 +93,7 @@ pub fn initialize_logging_with_config(
9293
Err(_) => {
9394
// Fallback to stdout only if file creation fails and console logging is enabled
9495
if enable_console_logging {
96+
let env_filter = make_env_filter();
9597
let stdout_layer = tracing_subscriber::fmt::layer()
9698
.event_format(event_format)
9799
.with_writer(std::io::stdout);

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: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ 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) = run_ghostscope_with_script_for_pid(script, 9, pid).await?;
5454
let _ = prog.0.kill().await.is_ok();
55+
5556
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
5657

5758
assert!(
@@ -92,7 +93,7 @@ trace do_stuff {
9293
print "RC:{}", G_COUNTER;
9394
}
9495
"#;
95-
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 5, pid).await?;
96+
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 9, pid).await?;
9697
let _ = prog.0.kill().await.is_ok();
9798
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
9899

@@ -147,7 +148,7 @@ trace do_stuff {
147148
print "&RC:{}", &G_COUNTER;
148149
}
149150
"#;
150-
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
151+
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 9, pid).await?;
151152
let _ = prog.0.kill().await.is_ok();
152153
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
153154

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

0 commit comments

Comments
 (0)