sbpf-debugger 0.2.4

Lightweight debugger for SBPF (Solana BPF) bytecode
Documentation
use {
    crate::{
        debugger::Debugger,
        error::DebuggerResult,
        input::ParsedInput,
        parser::{LineMap, rodata_from_section},
    },
    sbpf_assembler::{Assembler, AssemblerOption, DebugMode},
    sbpf_disassembler::program::Program,
    sbpf_runtime::{Runtime, config::RuntimeConfig},
    sbpf_vm::memory::Memory,
    std::path::{Path, PathBuf},
};

pub struct DebuggerSession {
    pub debugger: Debugger,
    pub line_map: Option<LineMap>,
    pub elf_bytes: Vec<u8>,
    pub elf_path: PathBuf,
}

pub fn load_session_from_asm(
    asm_path: &str,
    parsed: ParsedInput,
    config: RuntimeConfig,
) -> DebuggerResult<DebuggerSession> {
    let asm_path = Path::new(asm_path);
    if !asm_path.exists() {
        return Err(crate::error::DebuggerError::InvalidInput(format!(
            "Assembly file not found: {}",
            asm_path.display()
        )));
    }

    let source_code = std::fs::read_to_string(asm_path)?;
    let filename = asm_path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("unknown.s")
        .to_string();
    let directory = asm_path
        .parent()
        .and_then(|p| p.canonicalize().ok())
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_else(|| ".".to_string());

    let options = AssemblerOption::default().with_debug_mode(DebugMode {
        filename,
        directory,
    });
    let assembler = Assembler::new(options);
    let bytecode = assembler
        .assemble(&source_code)
        .map_err(|errors| crate::error::DebuggerError::Assembler(format!("{:?}", errors)))?;

    load_session_from_bytes(bytecode, parsed, config, None)
}

pub fn load_session_from_elf(
    elf_path: &str,
    parsed: ParsedInput,
    config: RuntimeConfig,
) -> DebuggerResult<DebuggerSession> {
    let elf_bytes = std::fs::read(elf_path)?;
    load_session_from_bytes(elf_bytes, parsed, config, Some(elf_path.into()))
}

fn load_session_from_bytes(
    elf_bytes: Vec<u8>,
    parsed: ParsedInput,
    config: RuntimeConfig,
    elf_path: Option<PathBuf>,
) -> DebuggerResult<DebuggerSession> {
    let mut runtime = Runtime::new(parsed.instruction.program_id, elf_bytes.clone(), config)?;
    for (program_id, elf) in &parsed.programs {
        runtime.add_program(program_id, elf.clone());
    }
    runtime.prepare(&parsed.instruction, &parsed.accounts)?;

    let mut debugger = Debugger::new(runtime);
    if let Ok(line_map) = LineMap::from_elf_data(&elf_bytes) {
        debugger.set_dwarf_line_map(line_map);
    }

    // Best effort rodata symbol extraction: decode errors are ignored here
    // since the runtime already validated the program.
    if let Ok(program) = Program::from_bytes(&elf_bytes)
        && let Ok(disassembled) = program.to_ixs()
        && let Some(ref section) = disassembled.value.rodata
    {
        let mut rodata_symbols = rodata_from_section(section);
        // Replace generated labels with actual labels from DWARF info (if available).
        if let Some(ref line_map) = debugger.dwarf_line_map {
            let text_offset = line_map.get_text_offset();
            for sym in &mut rodata_symbols {
                let rodata_offset = sym.address - Memory::RODATA_START;
                let addr = rodata_offset + text_offset;
                if let Some(name) = line_map.get_label_for_address(addr) {
                    sym.name = name.to_string();
                }
            }
        }
        if !rodata_symbols.is_empty() {
            debugger.set_rodata(rodata_symbols);
        }
    }

    Ok(DebuggerSession {
        line_map: debugger.dwarf_line_map.clone(),
        debugger,
        elf_bytes,
        elf_path: elf_path.unwrap_or_else(|| PathBuf::from("<memory>")),
    })
}