use {
super::{
cargo_deps::{discover_dep_src_roots, discover_path_dep_roots},
highlight::highlight_asm,
model::{DebugNode, DebugSession, DebugStep, DebugTx, ProgramDisasm, StaticInsn},
source::{discover_platform_tools_stdlib_roots, SourceResolver, CI_PLATFORM_TOOLS_PREFIX},
},
crate::flamegraph::trace::{
discover_invocations, find_unstripped_binary, load_function_map, stream_trace,
InvocationFiles, INSN_ENTRY_SIZE, KNOWN_SYSCALLS, REGS_ENTRY_SIZE,
},
anyhow::{anyhow, bail, Context, Result},
solana_compute_budget::compute_budget::ComputeBudget,
solana_sbpf::{ebpf, static_analysis::Analysis},
std::{
collections::BTreeMap,
fs,
path::{Path, PathBuf},
sync::Arc,
},
};
pub fn build_session(
profile_dir: &Path,
programs: &BTreeMap<String, PathBuf>,
manifest_dir: Option<&Path>,
crate_dir: Option<&Path>,
test_filter: Option<&str>,
) -> Result<DebugSession> {
let mut txs: Vec<DebugTx> = Vec::new();
let mut src_roots: Vec<PathBuf> = Vec::new();
if let Some(c) = crate_dir {
src_roots.extend(discover_path_dep_roots(c));
}
if let Some(p) = manifest_dir {
src_roots.push(p.to_path_buf());
src_roots.extend(discover_dep_src_roots(p));
}
let path_rewrites: Vec<(PathBuf, PathBuf)> = discover_platform_tools_stdlib_roots()
.into_iter()
.map(|replacement| (PathBuf::from(CI_PLATFORM_TOOLS_PREFIX), replacement))
.collect();
let cwd = crate_dir.map(|p| p.to_path_buf());
if !profile_dir.exists() {
return Ok(DebugSession {
txs,
src_roots,
path_rewrites,
cwd,
programs: BTreeMap::new(),
});
}
let mut program_ctx: BTreeMap<String, ProgramCtx> = BTreeMap::new();
for entry in fs::read_dir(profile_dir)
.with_context(|| format!("read profile dir {}", profile_dir.display()))?
{
let entry = entry?;
let dir = entry.path();
if !dir.is_dir() {
continue;
}
let Some(test_name) = dir.file_name().and_then(|s| s.to_str()).map(str::to_owned) else {
continue;
};
if let Some(filter) = test_filter {
if !test_name.contains(filter) {
continue;
}
}
let invocations = discover_invocations(&dir)
.with_context(|| format!("discover invocations in {}", dir.display()))?;
if invocations.is_empty() {
continue;
}
let mut tx_groups: BTreeMap<u32, Vec<&InvocationFiles>> = BTreeMap::new();
for inv in &invocations {
tx_groups.entry(inv.tx_seq).or_default().push(inv);
}
for (tx_seq, mut invs) in tx_groups {
invs.reverse();
let mut nodes: Vec<DebugNode> = Vec::with_capacity(invs.len());
let mut tx_total_cu: u64 = 0;
for inv in invs {
let ctx = match load_program_ctx(
&inv.program_id,
programs,
manifest_dir,
&mut program_ctx,
) {
Some(c) => c,
None => continue,
};
let regular_regs = fs::read(&inv.regs_path)
.with_context(|| format!("read {}", inv.regs_path.display()))?;
let regular_insns = fs::read(&inv.insns_path)
.with_context(|| format!("read {}", inv.insns_path.display()))?;
let regular_count = (regular_regs.len() / REGS_ENTRY_SIZE)
.min(regular_insns.len() / INSN_ENTRY_SIZE);
let (regs, insns, exact_cu) =
match load_gdb_sidecars(inv, ®ular_regs, ®ular_insns, regular_count)? {
Some(sidecar) => (sidecar.regs, sidecar.insns, Some(sidecar.cu_costs)),
None => (regular_regs, regular_insns, None),
};
let count = (regs.len() / REGS_ENTRY_SIZE).min(insns.len() / INSN_ENTRY_SIZE);
if count == 0 {
continue;
}
let program_label = program_label(&inv.program_id, programs);
let mut steps: Vec<DebugStep> = Vec::with_capacity(count);
let mut node_cu: u64 = 0;
let budget = ComputeBudget::new_with_defaults(false, false);
let mut step_idx = 0usize;
stream_trace(
®s,
&insns,
count,
&ctx.symbols,
&ctx.syscalls,
&program_label,
&budget,
|s| {
let cu_cost = exact_cu
.as_ref()
.and_then(|costs| costs.get(step_idx).copied())
.unwrap_or(s.cu_cost);
step_idx += 1;
node_cu += cu_cost;
let disasm = disassemble(&ctx.analysis, &s.insn, s.pc as usize);
let disasm_spans = highlight_asm(&disasm).spans;
let src_loc = ctx.source.resolve(s.pc);
steps.push(DebugStep {
pc: s.pc,
regs: s.regs,
insn: s.insn,
disasm,
disasm_spans,
func: s.func.to_owned(),
call_depth: s.call_stack.len().saturating_sub(1).max(1),
cu_cost,
cu_cumulative: node_cu,
syscall: s.syscall,
src_loc,
});
},
);
tx_total_cu += node_cu;
nodes.push(DebugNode {
program_label,
program_id: inv.program_id.clone(),
steps,
});
}
if nodes.iter().all(|n| n.steps.is_empty()) {
continue;
}
txs.push(DebugTx {
test_name: test_name.clone(),
tx_seq,
total_cu: tx_total_cu,
nodes,
});
}
}
txs.sort_by(|a, b| {
a.test_name
.cmp(&b.test_name)
.then_with(|| a.tx_seq.cmp(&b.tx_seq))
});
let mut programs_disasm: BTreeMap<String, ProgramDisasm> = BTreeMap::new();
for (pid, ctx) in &program_ctx {
let mut d = build_static_disasm(ctx);
d.has_dwarf = !ctx.source.is_empty();
programs_disasm.insert(pid.clone(), d);
}
Ok(DebugSession {
txs,
src_roots,
path_rewrites,
cwd,
programs: programs_disasm,
})
}
struct GdbSidecars {
regs: Vec<u8>,
insns: Vec<u8>,
cu_costs: Vec<u64>,
}
fn load_gdb_sidecars(
inv: &InvocationFiles,
regular_regs: &[u8],
regular_insns: &[u8],
regular_count: usize,
) -> Result<Option<GdbSidecars>> {
let regs_path = inv.regs_path.with_extension("gdb.regs");
let insns_path = inv.regs_path.with_extension("gdb.insns");
let cu_path = inv.regs_path.with_extension("gdb.cu");
if !regs_path.exists() && !insns_path.exists() && !cu_path.exists() {
return Ok(None);
}
if !regs_path.exists() || !insns_path.exists() || !cu_path.exists() {
bail!(
"incomplete gdb sidecar set for {}; expected {}, {}, and {}",
inv.regs_path.display(),
regs_path.display(),
insns_path.display(),
cu_path.display(),
);
}
let mut regs = fs::read(®s_path).with_context(|| format!("read {}", regs_path.display()))?;
let insns = fs::read(&insns_path).with_context(|| format!("read {}", insns_path.display()))?;
let cu = fs::read(&cu_path).with_context(|| format!("read {}", cu_path.display()))?;
let count = (regs.len() / REGS_ENTRY_SIZE).min(insns.len() / INSN_ENTRY_SIZE);
if count != regular_count {
bail!(
"gdb sidecar count mismatch for {}: gdb has {count} steps, regular trace has \
{regular_count}",
inv.regs_path.display(),
);
}
validate_and_normalize_gdb_regs(&mut regs, regular_regs, count)
.with_context(|| format!("validate {}", regs_path.display()))?;
if insns != regular_insns {
bail!(
"gdb sidecar instruction bytes differ from regular trace for {}",
inv.regs_path.display(),
);
}
if cu.len() / std::mem::size_of::<u64>() != count {
bail!(
"gdb sidecar CU count mismatch for {}: gdb has {} CU entries, trace has {count} steps",
inv.regs_path.display(),
cu.len() / std::mem::size_of::<u64>(),
);
}
Ok(Some(GdbSidecars {
regs,
insns,
cu_costs: cu_costs_from_remaining(&cu, count),
}))
}
fn validate_and_normalize_gdb_regs(
gdb_regs: &mut [u8],
regular_regs: &[u8],
count: usize,
) -> Result<()> {
if gdb_regs.len() < count * REGS_ENTRY_SIZE || regular_regs.len() < count * REGS_ENTRY_SIZE {
bail!("register trace is shorter than declared step count");
}
let mut text_addr = None::<u64>;
for i in 0..count {
let offset = i * REGS_ENTRY_SIZE;
for reg in 0..12 {
let start = offset + reg * 8;
let gdb = u64::from_le_bytes(gdb_regs[start..start + 8].try_into().unwrap());
let regular = u64::from_le_bytes(regular_regs[start..start + 8].try_into().unwrap());
if reg == 11 {
let expected_text_addr = gdb
.checked_sub(
regular
.checked_mul(INSN_ENTRY_SIZE as u64)
.ok_or_else(|| anyhow!("trace PC overflow at step {i}"))?,
)
.ok_or_else(|| anyhow!("gdb PC precedes trace PC at step {i}"))?;
match text_addr {
Some(addr) if addr != expected_text_addr => bail!(
"gdb PC/text address mismatch at step {i}: expected text addr {addr:#x}, \
got {expected_text_addr:#x}",
),
None => text_addr = Some(expected_text_addr),
_ => {}
}
gdb_regs[start..start + 8].copy_from_slice(®ular.to_le_bytes());
} else if gdb != regular {
bail!("gdb register r{reg} mismatch at step {i}: gdb={gdb:#x}, trace={regular:#x}",);
}
}
}
Ok(())
}
fn cu_costs_from_remaining(data: &[u8], count: usize) -> Vec<u64> {
let mut costs = Vec::with_capacity(count);
let mut prev = None::<u64>;
for i in 0..count {
let offset = i * std::mem::size_of::<u64>();
let Some(bytes) = data.get(offset..offset + 8) else {
costs.push(0);
continue;
};
let remaining = u64::from_le_bytes(bytes.try_into().unwrap());
costs.push(prev.map(|p| p.saturating_sub(remaining)).unwrap_or(0));
prev = Some(remaining);
}
costs
}
fn build_static_disasm(ctx: &ProgramCtx) -> ProgramDisasm {
let (_, text) = ctx.executable.get_text_bytes();
let n = text.len() / solana_sbpf::ebpf::INSN_SIZE;
let mut insns: Vec<StaticInsn> = Vec::with_capacity(n);
let mut pc_to_idx: BTreeMap<u64, usize> = BTreeMap::new();
let mut pc = 0;
while pc < n {
let mut raw = ebpf::get_insn_unchecked(text, pc);
let mut step = 1;
if raw.opc == ebpf::LD_DW_IMM && pc + 1 < n {
ebpf::augment_lddw_unchecked(text, &mut raw);
step = 2;
}
let disasm = ctx.analysis.disassemble_instruction(&raw, pc);
let disasm_spans = highlight_asm(&disasm).spans;
let func_label = ctx.symbols.get(&(pc as u64)).cloned();
pc_to_idx.insert(pc as u64, insns.len());
if step == 2 {
pc_to_idx.insert((pc + 1) as u64, insns.len());
}
insns.push(StaticInsn {
pc: pc as u64,
disasm_spans,
func_label,
});
pc += step;
}
ProgramDisasm {
insns,
pc_to_idx,
has_dwarf: false,
}
}
struct ProgramCtx {
symbols: BTreeMap<u64, String>,
syscalls: BTreeMap<u32, String>,
analysis: Analysis<'static>,
executable: &'static solana_sbpf::elf::Executable<NoopCtx>,
source: SourceResolver,
}
#[derive(Default)]
struct NoopCtx;
impl solana_sbpf::vm::ContextObject for NoopCtx {
fn consume(&mut self, _amount: u64) {}
fn get_remaining(&self) -> u64 {
0
}
}
fn syscall_stub(
_vm: *mut solana_sbpf::vm::EbpfVm<NoopCtx>,
_r1: u64,
_r2: u64,
_r3: u64,
_r4: u64,
_r5: u64,
) {
}
fn load_program_ctx<'a>(
program_id: &str,
programs: &BTreeMap<String, PathBuf>,
manifest_dir: Option<&Path>,
cache: &'a mut BTreeMap<String, ProgramCtx>,
) -> Option<&'a ProgramCtx> {
if !cache.contains_key(program_id) {
let ctx = build_program_ctx(program_id, programs, manifest_dir)?;
cache.insert(program_id.to_owned(), ctx);
}
cache.get(program_id)
}
fn build_program_ctx(
program_id: &str,
programs: &BTreeMap<String, PathBuf>,
manifest_dir: Option<&Path>,
) -> Option<ProgramCtx> {
let elf_path = programs.get(program_id)?;
let elf_bytes = fs::read(elf_path).ok()?;
let (symbols, syscalls) = match load_function_map(elf_path, manifest_dir) {
Ok(x) => x,
Err(e) => {
eprintln!(
"warning: can't parse {} ({e}). Run `cargo build-sbf -p <crate>` from the \
workspace root to produce a debugger-compatible `target/deploy/<name>.so`.",
elf_path.display()
);
return None;
}
};
let mut loader_inner =
solana_sbpf::program::BuiltinProgram::new_loader(solana_sbpf::vm::Config {
enable_symbol_and_section_labels: true,
..solana_sbpf::vm::Config::default()
});
for name in KNOWN_SYSCALLS {
let _ = loader_inner.register_function(name, syscall_stub);
}
let loader = Arc::new(loader_inner);
let executable = solana_sbpf::elf::Executable::<NoopCtx>::from_elf(&elf_bytes, loader).ok()?;
let exec_ref: &'static solana_sbpf::elf::Executable<NoopCtx> = Box::leak(Box::new(executable));
let analysis = Analysis::from_executable(exec_ref).ok()?;
let dwarf_path =
find_unstripped_binary(elf_path, manifest_dir).unwrap_or_else(|| elf_path.to_path_buf());
let source = SourceResolver::from_elf_path(&dwarf_path);
Some(ProgramCtx {
symbols,
syscalls,
analysis,
executable: exec_ref,
source,
})
}
fn disassemble(analysis: &Analysis<'_>, insn_bytes: &[u8; 8], pc: usize) -> String {
let insn = ebpf::Insn {
ptr: pc,
opc: insn_bytes[0],
dst: insn_bytes[1] & 0x0f,
src: (insn_bytes[1] & 0xf0) >> 4,
off: i16::from_le_bytes([insn_bytes[2], insn_bytes[3]]),
imm: i32::from_le_bytes([insn_bytes[4], insn_bytes[5], insn_bytes[6], insn_bytes[7]])
as i64,
};
analysis.disassemble_instruction(&insn, pc)
}
fn program_label(program_id: &str, programs: &BTreeMap<String, PathBuf>) -> String {
let short = short_pid(program_id);
match programs.get(program_id) {
Some(elf) => elf
.file_stem()
.and_then(|s| s.to_str())
.map(|n| format!("{n} ({short})"))
.unwrap_or_else(|| format!("program {program_id}")),
None => format!("[unresolved {short}]"),
}
}
fn short_pid(pid: &str) -> String {
if pid.len() <= 13 {
pid.to_owned()
} else {
format!("{}…{}", &pid[..8], &pid[pid.len() - 4..])
}
}
#[cfg(test)]
mod tests {
use {super::*, tempfile::tempdir};
fn invocation(dir: &Path) -> InvocationFiles {
InvocationFiles {
inv_seq: 1,
tx_seq: 1,
program_id: "Program1111111111111111111".to_string(),
regs_path: dir.join("0001__tx1.regs"),
insns_path: dir.join("0001__tx1.insns"),
}
}
fn regs_entries(pcs: &[u64]) -> Vec<[u64; 12]> {
pcs.iter()
.map(|pc| {
let mut regs = [0u64; 12];
regs[0] = 0x55;
regs[11] = *pc;
regs
})
.collect()
}
fn regs_bytes(entries: &[[u64; 12]]) -> Vec<u8> {
let mut out = Vec::with_capacity(entries.len() * REGS_ENTRY_SIZE);
for regs in entries {
for reg in regs {
out.extend_from_slice(®.to_le_bytes());
}
}
out
}
fn insns_bytes(count: usize, byte: u8) -> Vec<u8> {
vec![byte; count * INSN_ENTRY_SIZE]
}
fn cu_bytes(values: &[u64]) -> Vec<u8> {
values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect()
}
fn gdb_entries_for_regular(entries: &[[u64; 12]], text_addr: u64) -> Vec<[u64; 12]> {
entries
.iter()
.map(|regular| {
let mut gdb = *regular;
gdb[11] = text_addr + regular[11] * INSN_ENTRY_SIZE as u64;
gdb
})
.collect()
}
fn write_sidecars(
inv: &InvocationFiles,
gdb_regs: &[[u64; 12]],
gdb_insns: &[u8],
cu_remaining: &[u64],
) {
fs::write(
inv.regs_path.with_extension("gdb.regs"),
regs_bytes(gdb_regs),
)
.unwrap();
fs::write(inv.regs_path.with_extension("gdb.insns"), gdb_insns).unwrap();
fs::write(
inv.regs_path.with_extension("gdb.cu"),
cu_bytes(cu_remaining),
)
.unwrap();
}
fn sidecar_err(
inv: &InvocationFiles,
regular_regs: &[u8],
regular_insns: &[u8],
regular_count: usize,
) -> String {
match load_gdb_sidecars(inv, regular_regs, regular_insns, regular_count) {
Ok(_) => panic!("expected gdb sidecar validation error"),
Err(err) => format!("{err:#}"),
}
}
#[test]
fn gdb_sidecars_normalize_virtual_pc_and_override_cu_costs() {
let dir = tempdir().unwrap();
let inv = invocation(dir.path());
let regular_entries = regs_entries(&[0, 1, 2]);
let regular_regs = regs_bytes(®ular_entries);
let regular_insns = insns_bytes(3, 0xab);
let gdb_entries = gdb_entries_for_regular(®ular_entries, 0x1000);
write_sidecars(&inv, &gdb_entries, ®ular_insns, &[100, 93, 90]);
let sidecar = load_gdb_sidecars(&inv, ®ular_regs, ®ular_insns, 3)
.unwrap()
.unwrap();
assert_eq!(sidecar.regs, regular_regs);
assert_eq!(sidecar.insns, regular_insns);
assert_eq!(sidecar.cu_costs, vec![0, 7, 3]);
}
#[test]
fn gdb_sidecars_reject_incomplete_sets() {
let dir = tempdir().unwrap();
let inv = invocation(dir.path());
let regular_entries = regs_entries(&[0]);
let regular_regs = regs_bytes(®ular_entries);
let regular_insns = insns_bytes(1, 0xab);
fs::write(
inv.regs_path.with_extension("gdb.regs"),
regs_bytes(&gdb_entries_for_regular(®ular_entries, 0x1000)),
)
.unwrap();
let err = sidecar_err(&inv, ®ular_regs, ®ular_insns, 1);
assert!(err.contains("incomplete gdb sidecar set"));
}
#[test]
fn gdb_sidecars_reject_count_mismatch() {
let dir = tempdir().unwrap();
let inv = invocation(dir.path());
let regular_entries = regs_entries(&[0, 1]);
let regular_regs = regs_bytes(®ular_entries);
let regular_insns = insns_bytes(2, 0xab);
let gdb_entries = gdb_entries_for_regular(®ular_entries[..1], 0x1000);
write_sidecars(
&inv,
&gdb_entries,
®ular_insns[..INSN_ENTRY_SIZE],
&[100],
);
let err = sidecar_err(&inv, ®ular_regs, ®ular_insns, 2);
assert!(err.contains("gdb sidecar count mismatch"));
}
#[test]
fn gdb_sidecars_reject_instruction_mismatch() {
let dir = tempdir().unwrap();
let inv = invocation(dir.path());
let regular_entries = regs_entries(&[0, 1]);
let regular_regs = regs_bytes(®ular_entries);
let regular_insns = insns_bytes(2, 0xab);
let gdb_entries = gdb_entries_for_regular(®ular_entries, 0x1000);
write_sidecars(&inv, &gdb_entries, &insns_bytes(2, 0xcd), &[100, 99]);
let err = sidecar_err(&inv, ®ular_regs, ®ular_insns, 2);
assert!(err.contains("gdb sidecar instruction bytes differ"));
}
#[test]
fn gdb_sidecars_reject_register_mismatch() {
let dir = tempdir().unwrap();
let inv = invocation(dir.path());
let regular_entries = regs_entries(&[0]);
let regular_regs = regs_bytes(®ular_entries);
let regular_insns = insns_bytes(1, 0xab);
let mut gdb_entries = gdb_entries_for_regular(®ular_entries, 0x1000);
gdb_entries[0][0] = 0x66;
write_sidecars(&inv, &gdb_entries, ®ular_insns, &[100]);
let err = sidecar_err(&inv, ®ular_regs, ®ular_insns, 1);
assert!(err.contains("gdb register r0 mismatch"));
}
#[test]
fn gdb_sidecars_reject_inconsistent_text_address() {
let dir = tempdir().unwrap();
let inv = invocation(dir.path());
let regular_entries = regs_entries(&[0, 1]);
let regular_regs = regs_bytes(®ular_entries);
let regular_insns = insns_bytes(2, 0xab);
let mut gdb_entries = gdb_entries_for_regular(®ular_entries, 0x1000);
gdb_entries[1][11] = 0x2000 + INSN_ENTRY_SIZE as u64;
write_sidecars(&inv, &gdb_entries, ®ular_insns, &[100, 99]);
let err = sidecar_err(&inv, ®ular_regs, ®ular_insns, 2);
assert!(err.contains("gdb PC/text address mismatch"));
}
}