Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion e2e-tests/tests/fixtures/static_scope_program/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ CC ?= gcc
CFLAGS ?= -Wall -Wextra -g -O0
BINARY ?= static_scope_program
OBJ ?= $(BINARY).o
EXTRA_OBJ = $(OBJ:.o=_other.o)

all: $(BINARY)

$(BINARY): $(OBJ)
$(BINARY): $(OBJ) $(EXTRA_OBJ)
$(CC) $(CFLAGS) -o $@ $^

$(OBJ): static_scope_program.c
$(CC) $(CFLAGS) -c -o $@ $<

$(EXTRA_OBJ): other.c
$(CC) $(CFLAGS) -c -o $@ $<

clean:
rm -f *.o static_scope_program static_scope_program_clang_dwarf5

Expand Down
6 changes: 6 additions & 0 deletions e2e-tests/tests/fixtures/static_scope_program/other.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
static struct { int other; int common; } cfg = {99, 2};
int binding_state = 11;

__attribute__((noinline)) int binding_scope_two(void) {
return cfg.other + cfg.common;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,32 @@ static int bump_counters(int seed) {
return function_scope_static_counter + file_scope_static_counter + regular_local;
}

static int binding_scope_one(void);
static int binding_scope_unrelated(void);
extern int binding_scope_two(void);
extern int binding_state;

int main(void) {
while (1) {
int snapshot = bump_counters(2);
snapshot += binding_scope_one() + binding_scope_two();
snapshot += binding_scope_unrelated();
if (snapshot == -1) {
return 1;
}
usleep(10000);
}
return 0;
}

static struct { int own; int common; } cfg = {11, 1};

__attribute__((noinline)) static int binding_scope_one(void) {
return cfg.own + cfg.common + binding_state;
}

__attribute__((noinline)) static int binding_scope_unrelated(void) {
static int binding_state = 999;
static struct { int own; int common; } cfg = {777, 778};
return binding_state + cfg.own + cfg.common;
}
56 changes: 56 additions & 0 deletions e2e-tests/tests/static_scope_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,62 @@ use common::{fixture_compiler_available, init, FixtureCompiler, FIXTURES};
use std::path::Path;
use std::time::Duration;

#[tokio::test]
async fn test_static_global_binding_uses_the_current_compilation_unit() -> anyhow::Result<()> {
init();
let binary = FIXTURES.get_test_binary("static_scope_program")?;
let target = spawn_static_scope_program(&binary).await?;
let script = r#"
trace binding_scope_one { print "FIRST_CU:{}:{}:{}", cfg.own, cfg.common, binding_state; }
trace binding_scope_two { print "SECOND_CU:{}:{}", cfg.other, cfg.common; }
trace binding_scope_unrelated { print "LOCAL_SCOPE:{}:{}:{}", cfg.own, cfg.common, binding_state; }
"#;
let (code, stdout, stderr) = run_ghostscope_with_script_for_target(script, 3, &target).await?;
target.terminate().await?;
assert_eq!(code, 0, "stderr={stderr} stdout={stdout}");
assert!(stdout.contains("FIRST_CU:11:1:11"), "{stdout}");
assert!(stdout.contains("SECOND_CU:99:2"), "{stdout}");
assert!(stdout.contains("LOCAL_SCOPE:777:778:999"), "{stdout}");
Ok(())
}

#[tokio::test]
async fn test_global_binding_rejects_static_local_outside_its_scope() -> anyhow::Result<()> {
init();
let binary = FIXTURES.get_test_binary("static_scope_program")?;
let target = spawn_static_scope_program(&binary).await?;
let (code, stdout, stderr) = run_ghostscope_with_script_for_target(
r#"trace binding_scope_one { print "WRONG_LOCAL:{}", function_scope_static_counter; }"#,
3,
&target,
)
.await?;
target.terminate().await?;
assert_ne!(code, 0, "stderr={stderr} stdout={stdout}");
assert!(!stdout.contains("WRONG_LOCAL:"), "{stdout}");
assert!(stderr.contains("function_scope_static_counter"), "{stderr}");
Ok(())
}

#[tokio::test]
async fn test_invalid_static_field_cannot_bind_a_different_compilation_unit() -> anyhow::Result<()>
{
init();
let binary = FIXTURES.get_test_binary("static_scope_program")?;
let target = spawn_static_scope_program(&binary).await?;
let (code, stdout, stderr) = run_ghostscope_with_script_for_target(
r#"trace binding_scope_one { print "WRONG_SCOPE:{}", cfg.other; }"#,
3,
&target,
)
.await?;
target.terminate().await?;
assert_ne!(code, 0, "stderr={stderr} stdout={stdout}");
assert!(!stdout.contains("WRONG_SCOPE:"), "{stdout}");
assert!(stderr.contains("other"), "{stderr}");
Ok(())
}

async fn run_ghostscope_with_script_for_target(
script_content: &str,
timeout_secs: u64,
Expand Down
8 changes: 6 additions & 2 deletions ghostscope-compiler/src/ebpf/dwarf_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1522,7 +1522,11 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
}

if let Some((_global_module, plan)) = analyzer
.plan_global_access_read_plan(&prefer_module, var_name, &VariableAccessPath::default())
.plan_global_access_read_plan_at_address(
&module_address,
var_name,
&VariableAccessPath::default(),
)
.map_err(|err| CodeGenError::DwarfError(err.to_string()))?
{
debug!("Found DWARF global '{}' via variable read plan", var_name);
Expand Down Expand Up @@ -1608,7 +1612,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
}

if let Some((_module_path, plan)) = analyzer
.plan_global_access_read_plan(&prefer_module, base_name, access_path)
.plan_global_access_read_plan_at_address(&module_address, base_name, access_path)
.map_err(|err| CodeGenError::DwarfError(err.to_string()))?
{
debug!("Found DWARF global access '{path_text}' via variable read plan");
Expand Down
152 changes: 82 additions & 70 deletions ghostscope-dwarf/src/analyzer/plan_global.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
use super::DwarfAnalyzer;
use crate::{
core::{GlobalVariableInfo, Provenance, Result},
semantics::{VariableAccessPath, VariableReadPlan},
semantics::{PcContext, VariableAccessPath, VariableReadPlan},
};
use std::path::{Path, PathBuf};

impl DwarfAnalyzer {
pub(super) fn select_unambiguous_global_plan(
pub(super) fn select_unambiguous_global_binding(
base: &str,
mut candidates: Vec<(PathBuf, VariableReadPlan)>,
) -> Result<Option<(PathBuf, VariableReadPlan)>> {
mut candidates: Vec<(PathBuf, GlobalVariableInfo)>,
) -> Result<Option<(PathBuf, GlobalVariableInfo)>> {
match candidates.len() {
0 => Ok(None),
1 => Ok(candidates.pop()),
count => {
let details = candidates
.iter()
.map(|(module_path, plan)| {
let declaration = plan
.declaration
.map(|die| format!(" cu={} die=0x{:x}", die.cu.0, die.offset))
.unwrap_or_default();
format!("{}{}", module_path.display(), declaration)
.map(|(module_path, info)| {
format!(
"{} cu={} die=0x{:x}",
module_path.display(),
info.unit_offset.0,
info.die_offset.0
)
})
.collect::<Vec<_>>()
.join(", ");
Expand All @@ -32,19 +33,19 @@ impl DwarfAnalyzer {
}
}

pub(super) fn select_global_plan_with_preferred_module(
pub(super) fn select_global_binding_with_preferred_module(
base: &str,
prefer_module: &Path,
candidates: Vec<(PathBuf, VariableReadPlan)>,
) -> Result<Option<(PathBuf, VariableReadPlan)>> {
candidates: Vec<(PathBuf, GlobalVariableInfo)>,
) -> Result<Option<(PathBuf, GlobalVariableInfo)>> {
let (preferred, fallback): (Vec<_>, Vec<_>) = candidates
.into_iter()
.partition(|(module_path, _)| module_path == prefer_module);
if !preferred.is_empty() {
return Self::select_unambiguous_global_plan(base, preferred);
return Self::select_unambiguous_global_binding(base, preferred);
}

Self::select_unambiguous_global_plan(base, fallback)
Self::select_unambiguous_global_binding(base, fallback)
}

/// Find global/static variables by name across all loaded modules
Expand Down Expand Up @@ -74,71 +75,82 @@ impl DwarfAnalyzer {
results
}

/// Plan a global/static source-level access path as a neutral read plan.
/// Bind a global/static declaration before projecting its access path.
/// Without a PC, duplicate declarations in the preferred module are ambiguous.
pub fn plan_global_access_read_plan(
&self,
prefer_module: &PathBuf,
prefer_module: &Path,
base: &str,
path: &VariableAccessPath,
) -> Result<Option<(PathBuf, VariableReadPlan)>> {
let matches = self.find_global_variables_by_name(base);
if matches.is_empty() {
return Ok(None);
}
self.plan_global_access_read_plan_in_scope(prefer_module, None, base, path)
}

let mut ordered: Vec<(PathBuf, GlobalVariableInfo)> = Vec::new();
for (module_path, info) in matches.iter() {
if *module_path == *prefer_module {
ordered.push((module_path.clone(), info.clone()));
}
}
for (module_path, info) in matches.into_iter() {
if module_path != *prefer_module {
ordered.push((module_path, info));
}
}
/// Resolve global names in the compilation unit of the traced instruction.
/// An invalid field on that declaration must not select another CU's variable.
pub fn plan_global_access_read_plan_at_address(
&self,
address: &crate::ModuleAddress,
base: &str,
path: &VariableAccessPath,
) -> Result<Option<(PathBuf, VariableReadPlan)>> {
let context = self.resolve_pc(address)?;
self.plan_global_access_read_plan_in_scope(&address.module_path, Some(&context), base, path)
}

let mut direct_matches = Vec::new();
let mut last_error = None;
for (module_path, info) in ordered {
let base_plan = match self.resolve_variable_read_plan_by_offsets_in_module(
&module_path,
info.unit_offset,
info.die_offset,
Provenance::Synthesized {
detail: "global access".to_string(),
},
) {
Ok(plan) => plan,
Err(err) => {
last_error = Some(err);
continue;
}
fn plan_global_access_read_plan_in_scope(
&self,
prefer_module: &Path,
context: Option<&PcContext>,
base: &str,
path: &VariableAccessPath,
) -> Result<Option<(PathBuf, VariableReadPlan)>> {
let mut matches = self.find_global_variables_by_name(base);
let mut has_unknown_scope = false;
if let Some(context) = context {
// The global index also lists static locals. A CU preference must not
// promote a declaration belonging to another function or lexical block.
matches.retain(|(module_path, info)| {
let visibility = self.module_id_for_path(module_path).and_then(|module| {
self.modules
.get(module_path)?
.global_variable_visibility(info, module, context)
});
has_unknown_scope |= visibility.is_none();
visibility != Some(false)
});
}
let prefer_cu = context.filter(|_| !has_unknown_scope).and_then(|context| {
context
.inline_chain
.last()
.map(|frame| frame.abstract_origin.unwrap_or(frame.concrete_die).cu)
.or(context.cu)
});
if let Some(cu) = prefer_cu {
let in_scope = |(module_path, info): &(PathBuf, GlobalVariableInfo)| {
Self::module_paths_equivalent(module_path, prefer_module)
&& info.unit_offset.0 as u64 == u64::from(cu.0)
};

match self.plan_access_path_with_type_completion(&module_path, base_plan, path) {
Ok(plan) => direct_matches.push((module_path, plan)),
Err(primary_error) => {
if Self::is_value_backed_aggregate_access_error(&primary_error) {
return Err(primary_error);
}
last_error = Some(primary_error);
}
if matches.iter().any(in_scope) {
matches.retain(in_scope);
}
}

if !direct_matches.is_empty() {
return Self::select_global_plan_with_preferred_module(
base,
prefer_module,
direct_matches,
);
}

if let Some(err) = last_error {
return Err(err);
}
Ok(None)
let Some((module_path, info)) =
Self::select_global_binding_with_preferred_module(base, prefer_module, matches)?
else {
return Ok(None);
};
let base_plan = self.resolve_variable_read_plan_by_offsets_in_module(
&module_path,
info.unit_offset,
info.die_offset,
Provenance::Synthesized {
detail: "global access".to_string(),
},
)?;
let plan = self.plan_access_path_with_type_completion(&module_path, base_plan, path)?;
Ok(Some((module_path, plan)))
}

fn resolve_variable_read_plan_by_offsets_in_module<P: AsRef<Path>>(
Expand Down
10 changes: 2 additions & 8 deletions ghostscope-dwarf/src/analyzer/plan_pc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ use super::DwarfAnalyzer;
use crate::{
core::{demangle::RustSymbolHashDisplay, ModuleAddress, Provenance, Result},
semantics::{
AddressSpaceInfo, FunctionParameter, PcContext, PcLineInfo, PcRange, PlanError,
VariableAccessPath, VariableAccessSegment, VariableReadPlan, VisibleVariable,
VisibleVariablesResult,
AddressSpaceInfo, FunctionParameter, PcContext, PcLineInfo, PcRange, VariableAccessPath,
VariableAccessSegment, VariableReadPlan, VisibleVariable, VisibleVariablesResult,
},
};
use std::path::Path;
Expand Down Expand Up @@ -193,11 +192,6 @@ impl DwarfAnalyzer {
))
}

pub(super) fn is_value_backed_aggregate_access_error(err: &anyhow::Error) -> bool {
err.downcast_ref::<PlanError>()
.is_some_and(PlanError::is_value_backed_aggregate_access)
}

pub(super) fn read_plan_from_variable(
variable: crate::parser::ParsedVariable,
provenance: Provenance,
Expand Down
Loading
Loading