Skip to main content

anchor_cli/debugger/
source.rs

1//! DWARF-backed `PC → (file, line)` resolver for SBF ELFs.
2//!
3//! Built on [`addr2line::Loader`] so we get DWARF-5 / split-DWARF / supplementary
4//! object support for free. The resolver is best-effort: if the ELF has no
5//! DWARF (stripped deploys, release builds without
6//! `CARGO_PROFILE_RELEASE_DEBUG=2`, programs built by third parties) we return
7//! `None` and the TUI source pane falls back to a "no source available" notice
8//! — the rest of the stepper is unaffected.
9//!
10//! SBF maps PCs to byte addresses as `text_addr + pc * INSN_SIZE`. LLVM emits
11//! standard DWARF line tuples on those byte addresses so `addr2line` works
12//! once we do that translation.
13
14use {
15    super::model::SrcLoc,
16    addr2line::Loader,
17    object::{Object, ObjectSection},
18    std::path::{Path, PathBuf},
19};
20
21const INSN_SIZE: u64 = 8;
22
23/// Per-ELF source resolver. Cheap to query (one interval-tree lookup).
24pub struct SourceResolver {
25    inner: Option<Inner>,
26}
27
28struct Inner {
29    loader: Loader,
30    text_addr: u64,
31}
32
33impl SourceResolver {
34    /// Builds a resolver by re-reading the ELF from disk. Returns an empty
35    /// resolver when parsing fails or no text section is present.
36    pub fn from_elf_path(path: &Path) -> Self {
37        Self {
38            inner: build(path).ok(),
39        }
40    }
41
42    /// Resolves an SBPF program counter to a `(file, line)` pair, or `None`
43    /// when DWARF is unavailable or the PC has no line entry.
44    pub fn resolve(&self, pc: u64) -> Option<SrcLoc> {
45        let inner = self.inner.as_ref()?;
46        let vaddr = inner.text_addr.checked_add(pc.checked_mul(INSN_SIZE)?)?;
47        let loc = inner.loader.find_location(vaddr).ok().flatten()?;
48        Some(SrcLoc {
49            file: PathBuf::from(loc.file?),
50            line: loc.line?,
51        })
52    }
53
54    /// Resolves an SBPF program counter to its full DWARF inlining chain,
55    /// innermost-first (the deepest inlined body) out to the physical
56    /// caller. Returns an empty vec when DWARF is unavailable.
57    ///
58    /// `resolve` (via `find_location`) returns whichever line the DWARF
59    /// line program emitted at the PC — usually innermost, but aggressive
60    /// inlining can shove the entry back to an outer callsite. For
61    /// coverage, attributing a PC to *every* frame in the chain credits
62    /// the tiny `#[inline(always)]` wrappers (`Box<T>::load`,
63    /// `Sysvar::load`, `AccountLoader::next*`, etc.) that would otherwise
64    /// show 0% despite running on every transaction, matching the
65    /// behavior of `llvm-cov show` over expansion regions.
66    pub fn resolve_frames(&self, pc: u64) -> Vec<SrcLoc> {
67        let Some(inner) = self.inner.as_ref() else {
68            return Vec::<SrcLoc>::new();
69        };
70        let Some(vaddr) = inner
71            .text_addr
72            .checked_add(match pc.checked_mul(INSN_SIZE) {
73                Some(v) => v,
74                None => return Vec::<SrcLoc>::new(),
75            })
76        else {
77            return Vec::<SrcLoc>::new();
78        };
79        let mut out: Vec<SrcLoc> = Vec::new();
80        let Ok(mut frames) = inner.loader.find_frames(vaddr) else {
81            return out;
82        };
83        while let Ok(Some(frame)) = frames.next() {
84            if let Some(loc) = frame.location {
85                if let (Some(file), Some(line)) = (loc.file, loc.line) {
86                    out.push(SrcLoc {
87                        file: PathBuf::from(file),
88                        line,
89                    });
90                }
91            }
92        }
93        out
94    }
95
96    /// `true` when no DWARF context was built — the TUI uses this to render
97    /// a single "no source info" notice instead of per-step errors.
98    pub fn is_empty(&self) -> bool {
99        self.inner.is_none()
100    }
101}
102
103/// Prefix the Solana `platform-tools` build bakes into DWARF paths for
104/// stdlib files.
105#[cfg(target_os = "macos")]
106pub const CI_PLATFORM_TOOLS_PREFIX: &str =
107    "/Users/runner/work/platform-tools/platform-tools/out/rust/library/";
108#[cfg(target_os = "linux")]
109pub const CI_PLATFORM_TOOLS_PREFIX: &str =
110    "/home/runner/work/platform-tools/platform-tools/out/rust/library/";
111#[cfg(not(any(target_os = "macos", target_os = "linux")))]
112pub const CI_PLATFORM_TOOLS_PREFIX: &str = compile_error!("Current platform is not supported");
113
114/// Locate every `platform-tools/rust/lib/rustlib/src/rust/library/` tree
115/// under `~/.cache/solana/` and return them newest-version-first. Empty
116/// vec if the solana toolchain isn't installed via `agave-install`.
117///
118/// Callers pair these with [`CI_PLATFORM_TOOLS_PREFIX`] so stdlib frames
119/// emitted against the CI build path resolve to the local source tree.
120pub fn discover_platform_tools_stdlib_roots() -> Vec<PathBuf> {
121    let Some(home) = dirs::home_dir() else {
122        return Vec::new();
123    };
124    let base = home.join(".cache/solana");
125    let Ok(entries) = std::fs::read_dir(&base) else {
126        return Vec::new();
127    };
128    let mut versions: Vec<(String, PathBuf)> = entries
129        .flatten()
130        .filter_map(|e| {
131            let name = e.file_name().to_str()?.to_owned();
132            let candidate = e
133                .path()
134                .join("platform-tools/rust/lib/rustlib/src/rust/library");
135            candidate.is_dir().then_some((name, candidate))
136        })
137        .collect();
138    // Lexical sort on version strings like `v1.41`, `v1.52` — good enough
139    // for numeric-minor ordering up to v1.99; beyond that we'd want proper
140    // semver parsing.
141    versions.sort_by(|a, b| b.0.cmp(&a.0));
142    versions.into_iter().map(|(_, p)| p).collect()
143}
144
145fn build(path: &Path) -> anyhow::Result<Inner> {
146    // Load once to read `.text` address. This parse is cheap (metadata only).
147    let bytes = std::fs::read(path)?;
148    let file = object::File::parse(&*bytes)?;
149    let text = file
150        .sections()
151        .find(|s| s.name().ok() == Some(".text"))
152        .or_else(|| {
153            file.sections()
154                .find(|s| s.kind() == object::SectionKind::Text)
155        })
156        .ok_or_else(|| anyhow::anyhow!("no .text section"))?;
157    let text_addr = text.address();
158
159    let loader = Loader::new(path).map_err(|e| anyhow::anyhow!("load DWARF: {e}"))?;
160    Ok(Inner { loader, text_addr })
161}