Browse Source

Decommission rbpf-cli, replaced by solana-ledger-tool run subcommand (#31512)

Dmitri Makarov 2 years ago
parent
commit
078304888d
4 changed files with 39 additions and 469 deletions
  1. 0 10
      Cargo.lock
  2. 35 32
      docs/src/developing/on-chain-programs/debugging.md
  3. 1 9
      rbpf-cli/Cargo.toml
  4. 3 418
      rbpf-cli/src/main.rs

+ 0 - 10
Cargo.lock

@@ -4175,16 +4175,6 @@ dependencies = [
 [[package]]
 name = "rbpf-cli"
 version = "1.16.0"
-dependencies = [
- "clap 3.2.23",
- "serde",
- "serde_json",
- "solana-bpf-loader-program",
- "solana-logger 1.16.0",
- "solana-program-runtime",
- "solana-sdk 1.16.0",
- "solana_rbpf",
-]
 
 [[package]]
 name = "rcgen"

+ 35 - 32
docs/src/developing/on-chain-programs/debugging.md

@@ -113,21 +113,17 @@ To turn on SBF interpreter trace messages in a local cluster configure the
 ## Source level debugging
 
 Source level debugging of on-chain programs written in Rust or C can
-be done using the stand-alone tool rbpf-cli, included in the SDK, and
-lldb, distrubuted with Solana Rust and Clang compiler binary package
+be done using the `run` subcommand of `solana-ledger-tool`, and lldb,
+distrubuted with Solana Rust and Clang compiler binary package
 platform-tools.
 
-The rbpf-cli tool loads a compiled on-chain program, executes it in
-RBPF virtual machine and runs a gdb server that accepts incoming
-connections from LLDB or GDB.  Once lldb is connected to rbpf-cli
-gdbserver, it can control execution of an on-chain program.  The
-execution environment of rbpf-cli is limited to the loaded program and
-the input data only.  Run `rbpf-cli --help` for an example of
-specifying input data for parameters of the program entrypoint
-function.  Any information that exists on a blockchain is not
-available to the program when executed in rbpf-cli. For example, CPI
-calls don't work, reading any information from blockchain doesn't
-work, etc.
+The `solana-ledger-tool run` subcommand loads a compiled on-chain
+program, executes it in RBPF virtual machine and runs a gdb server
+that accepts incoming connections from LLDB or GDB.  Once lldb is
+connected to `solana-ledger-tool` gdbserver, it can control execution of
+an on-chain program.  Run `solana-ledger-tool run --help`
+for an example of specifying input data for parameters of the program
+entrypoint function.
 
 To compile a program for debugging use cargo-build-sbf build utility
 with the command line option `--debug`. The utility will generate two
@@ -135,28 +131,35 @@ loadable files, one a usual loadable module with the extension `.so`,
 and another the same loadable module but containing Dwarf debug
 information, a file with extension `.debug`.
 
-To execute a program in debugger, run rbpf-cli with `-u debugger`
-command line option. For example, a crate named 'helloworld' is
-compiled and an executable program is built in `target/deploy`
-directory. There should be three files in that directory
+To execute a program in debugger, run `solana-ledger-tool run` with
+`-u debugger` command line option. For example, a crate named
+'helloworld' is compiled and an executable program is built in
+`target/deploy` directory. There should be three files in that
+directory
 - helloworld-keypair.json -- a keypair for deploying the program,
 - helloworld.debug -- a binary file containg debug information,
 - helloworld.so -- an executable file loadable into the virtual machine.
-The command line for running rbp-cli would be something like this
+The command line for running `solana-ledger-tool` would be something like this
 ```
-rbpf-cli -u debugger target/deploy/helloworld.so
+solana-ledger-tool run -l test-ledger -u debugger target/deploy/helloworld.so
 ```
-
-In debugger mode rbpf-cli loads an `.so` file and starts listening for
-an incoming connection from a debugger
+Note that `solana-ledger-tool` always loads a ledger database. Most
+on-chain programs interact with a ledger in some manner. Even if for
+debugging purpose a ledger is not needed, it has to be provided to
+`solana-ledger-tool`. A minimal ladger database can be created by
+running `solana-test-validator`, which creates a ledger in
+`test-ledger` subdirectory.
+
+In debugger mode `solana-ledger-tool run` loads an `.so` file and
+starts listening for an incoming connection from a debugger
 ```
 Waiting for a Debugger connection on "127.0.0.1:9001"...
 ```
 
-To connect to rbpf-cli and execute the program, run lldb. For
+To connect to `solana-ledger-tool` and execute the program, run lldb. For
 debugging rust programs it may be beneficial to run solana-lldb
 wrapper to lldb, i.e. at a new shell prompt (other than the one used
-to run rbpf-cli) run the command
+to run `solana-ledger-tool`) run the command
 
 ```
 solana-lldb
@@ -178,8 +181,8 @@ If the debugger finds the file, it will print something like this
 Current executable set to '/path/helloworld.debug' (bpf).
 ```
 
-Now, connect to the gdb server that rbpf-cli implements, and debug the
-program as usual. Enter the following command at lldb prompt
+Now, connect to the gdb server that `solana-ledger-tool` implements, and
+debug the program as usual. Enter the following command at lldb prompt
 ```
 (lldb) gdb-remote 127.0.0.1:9001
 ```
@@ -225,15 +228,15 @@ First file is `tasks.json` with the following content
             }
         },
         {
-            "label": "rbpf",
+            "label": "solana-debugger",
             "type": "shell",
-            "command": "rbpf-cli -u debugger ${workspaceFolder}/target/deploy/helloworld.so"
+            "command": "solana-ledger-tool run -l test-ledger -u debugger ${workspaceFolder}/target/deploy/helloworld.so"
         }
     ]
 }
 ```
 The first task is to build the on-chain program using cargo-build-sbf
-utility. The second task is to run rbpf-cli in debugger mode.
+utility. The second task is to run `solana-ledger-tool run` in debugger mode.
 
 Another file is `launch.json` with the following content
 ```
@@ -251,12 +254,12 @@ Another file is `launch.json` with the following content
 }
 ```
 This file specifies how to run debugger and to connect it to the gdb
-server implemented by rbpf-cli.
+server implemented by `solana-ledger-tool`.
 
 To start debugging a program, first build it by running the build
-task. The next step is to run rbpf task. The tasks specified in
+task. The next step is to run `solana-debugger` task. The tasks specified in
 `tasks.json` file are started from `Terminal >> Run Task...` menu of
-VSCode. When rbpf-cli is running and listening from incoming
+VSCode. When `solana-ledger-tool` is running and listening from incoming
 connections, it's time to start the debugger.  Launch it from VSCode
 `Run and Debug` menu.  If everything is set up correctly, VSCode will
 start a debugging session and the porogram execution should stop on

+ 1 - 9
rbpf-cli/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "rbpf-cli"
-description = "CLI to test and analyze SBF programs"
+description = "Decommissioned CLI to test and analyze SBF programs"
 keywords = ["SBF", "interpreter", "JIT"]
 publish = false
 version = { workspace = true }
@@ -11,11 +11,3 @@ license = { workspace = true }
 edition = { workspace = true }
 
 [dependencies]
-clap = { version = "3.1.5", features = ["cargo"] }
-serde = { workspace = true }
-serde_json = { workspace = true }
-solana-bpf-loader-program = { workspace = true }
-solana-logger = { workspace = true }
-solana-program-runtime = { workspace = true }
-solana-sdk = { workspace = true }
-solana_rbpf = { workspace = true, features = ["debugger"] }

+ 3 - 418
rbpf-cli/src/main.rs

@@ -1,421 +1,6 @@
-use {
-    clap::{crate_version, Arg, Command},
-    serde::{Deserialize, Serialize},
-    serde_json::Result,
-    solana_bpf_loader_program::{
-        create_vm, load_program_from_bytes, serialization::serialize_parameters,
-        syscalls::create_loader,
-    },
-    solana_program_runtime::{
-        invoke_context::InvokeContext,
-        loaded_programs::{LoadProgramMetrics, LoadedProgramType},
-        with_mock_invoke_context,
-    },
-    solana_rbpf::{
-        assembler::assemble, elf::Executable, static_analysis::Analysis,
-        verifier::RequisiteVerifier, vm::VerifiedExecutable,
-    },
-    solana_sdk::{
-        account::AccountSharedData,
-        bpf_loader,
-        pubkey::Pubkey,
-        slot_history::Slot,
-        transaction_context::{IndexOfAccount, InstructionAccount},
-    },
-    std::{
-        fmt::{Debug, Formatter},
-        fs::File,
-        io::{Read, Seek, Write},
-        path::Path,
-        time::{Duration, Instant},
-    },
-};
-
-#[derive(Serialize, Deserialize, Debug)]
-struct Account {
-    key: Pubkey,
-    owner: Pubkey,
-    is_signer: bool,
-    is_writable: bool,
-    lamports: u64,
-    data: Vec<u8>,
-}
-#[derive(Serialize, Deserialize)]
-struct Input {
-    accounts: Vec<Account>,
-    instruction_data: Vec<u8>,
-}
-fn load_accounts(path: &Path) -> Result<Input> {
-    let file = File::open(path).unwrap();
-    let input: Input = serde_json::from_reader(file)?;
-    eprintln!("Program input:");
-    eprintln!("accounts {:?}", &input.accounts);
-    eprintln!("instruction_data {:?}", &input.instruction_data);
-    eprintln!("----------------------------------------");
-    Ok(input)
-}
-
 fn main() {
-    solana_logger::setup();
-    let matches = Command::new("Solana SBF CLI")
-        .version(crate_version!())
-        .author("Solana Labs Maintainers <maintainers@solanalabs.com>")
-        .about(
-            r##"CLI to test and analyze SBF programs.
-
-The tool executes SBF programs in a mocked environment.
-Some features, such as sysvars syscall and CPI, are not
-available for the programs executed by the CLI tool.
-
-The input data for a program execution have to be in JSON format
-and the following fields are required
-{
-    "accounts": [
-        {
-            "key": [
-                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
-            ],
-            "owner": [
-                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
-            ],
-            "is_signer": false,
-            "is_writable": true,
-            "lamports": 1000,
-            "data": [0, 0, 0, 3]
-        }
-    ],
-    "instruction_data": []
-}
-"##,
-        )
-        .arg(
-            Arg::new("PROGRAM")
-                .help(
-                    "Program file to use. This is either an ELF shared-object file to be executed, \
-                     or an assembly file to be assembled and executed.",
-                )
-                .required(true)
-                .index(1)
-        )
-        .arg(
-            Arg::new("input")
-                .help(
-                    "Input for the program to run on, where FILE is a name of a JSON file \
-with input data, or BYTES is the number of 0-valued bytes to allocate for program parameters",
-                )
-                .short('i')
-                .long("input")
-                .value_name("FILE / BYTES")
-                .takes_value(true)
-                .default_value("0"),
-        )
-        .arg(
-            Arg::new("memory")
-                .help("Heap memory for the program to run on")
-                .short('m')
-                .long("memory")
-                .value_name("BYTES")
-                .takes_value(true)
-                .default_value("0"),
-        )
-        .arg(
-            Arg::new("use")
-                .help(
-                    "Method of execution to use, where 'cfg' generates Control Flow Graph \
-of the program, 'disassembler' dumps disassembled code of the program, 'interpreter' runs \
-the program in the virtual machine's interpreter, 'debugger' is the same as 'interpreter' \
-but hosts a GDB interface, and 'jit' precompiles the program to native machine code \
-before execting it in the virtual machine.",
-                )
-                .short('u')
-                .long("use")
-                .takes_value(true)
-                .value_name("VALUE")
-                .possible_values(["cfg", "disassembler", "interpreter", "debugger", "jit"])
-                .default_value("jit"),
-        )
-        .arg(
-            Arg::new("instruction limit")
-                .help("Limit the number of instructions to execute")
-                .short('l')
-                .long("limit")
-                .takes_value(true)
-                .value_name("COUNT")
-                .default_value(&std::i64::MAX.to_string()),
-        )
-        .arg(
-            Arg::new("port")
-                .help("Port to use for the connection with a remote debugger")
-                .long("port")
-                .takes_value(true)
-                .value_name("PORT")
-                .default_value("9001"),
-        )
-        .arg(
-            Arg::new("output_format")
-                .help("Return information in specified output format")
-                .long("output")
-                .value_name("FORMAT")
-                .global(true)
-                .takes_value(true)
-                .possible_values(["json", "json-compact"]),
-        )
-        .arg(
-            Arg::new("trace")
-                .help("Output instruction trace")
-                .short('t')
-                .long("trace")
-                .takes_value(true)
-                .value_name("FILE"),
-        )
-        .get_matches();
-
-    let loader_id = bpf_loader::id();
-    let mut transaction_accounts = vec![
-        (
-            loader_id,
-            AccountSharedData::new(0, 0, &solana_sdk::native_loader::id()),
-        ),
-        (
-            Pubkey::new_unique(),
-            AccountSharedData::new(0, 0, &loader_id),
-        ),
-    ];
-    let mut instruction_accounts = Vec::new();
-    let instruction_data = match matches.value_of("input").unwrap().parse::<usize>() {
-        Ok(allocation_size) => {
-            let pubkey = Pubkey::new_unique();
-            transaction_accounts.push((
-                pubkey,
-                AccountSharedData::new(0, allocation_size, &Pubkey::new_unique()),
-            ));
-            instruction_accounts.push(InstructionAccount {
-                index_in_transaction: 0,
-                index_in_caller: 0,
-                index_in_callee: 0,
-                is_signer: false,
-                is_writable: true,
-            });
-            vec![]
-        }
-        Err(_) => {
-            let input = load_accounts(Path::new(matches.value_of("input").unwrap())).unwrap();
-            for (index, account_info) in input.accounts.into_iter().enumerate() {
-                let mut account = AccountSharedData::new(
-                    account_info.lamports,
-                    account_info.data.len(),
-                    &account_info.owner,
-                );
-                account.set_data(account_info.data);
-                transaction_accounts.push((account_info.key, account));
-                instruction_accounts.push(InstructionAccount {
-                    index_in_transaction: index as IndexOfAccount,
-                    index_in_caller: index as IndexOfAccount,
-                    index_in_callee: index as IndexOfAccount,
-                    is_signer: account_info.is_signer,
-                    is_writable: account_info.is_writable,
-                });
-            }
-            input.instruction_data
-        }
-    };
-    with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts);
-    invoke_context
-        .transaction_context
-        .get_next_instruction_context()
-        .unwrap()
-        .configure(&[0, 1], &instruction_accounts, &instruction_data);
-    invoke_context.push().unwrap();
-    let (_parameter_bytes, regions, account_lengths) = serialize_parameters(
-        invoke_context.transaction_context,
-        invoke_context
-            .transaction_context
-            .get_current_instruction_context()
-            .unwrap(),
-        true, // should_cap_ix_accounts
-        true,
-    )
-    .unwrap();
-
-    let program = matches.value_of("PROGRAM").unwrap();
-    let mut file = File::open(Path::new(program)).unwrap();
-    let mut magic = [0u8; 4];
-    file.read_exact(&mut magic).unwrap();
-    file.rewind().unwrap();
-    let mut contents = Vec::new();
-    file.read_to_end(&mut contents).unwrap();
-    // Allowing mut here, since it is needed for jit compile, which is under a config flag
-    #[allow(unused_mut)]
-    let mut verified_executable = if magic == [0x7f, 0x45, 0x4c, 0x46] {
-        let mut load_program_metrics = LoadProgramMetrics::default();
-        let result = load_program_from_bytes(
-            &invoke_context.feature_set,
-            invoke_context.get_compute_budget(),
-            None,
-            &mut load_program_metrics,
-            &contents,
-            &bpf_loader::id(),
-            contents.len(),
-            Slot::default(),
-            true, /* reject_deployment_of_broken_elfs */
-            true, /* debugging_features */
-        );
-        match result {
-            Ok(loaded_program) => match loaded_program.program {
-                LoadedProgramType::LegacyV1(program) => Ok(unsafe { std::mem::transmute(program) }),
-                _ => unreachable!(),
-            },
-            Err(err) => Err(format!("Loading executable failed: {err:?}")),
-        }
-    } else {
-        let loader = create_loader(
-            &invoke_context.feature_set,
-            invoke_context.get_compute_budget(),
-            true,
-            true,
-            true,
-        )
-        .unwrap();
-        let executable =
-            assemble::<InvokeContext>(std::str::from_utf8(contents.as_slice()).unwrap(), loader)
-                .unwrap();
-        VerifiedExecutable::<RequisiteVerifier, InvokeContext>::from_executable(executable)
-            .map_err(|err| format!("Assembling executable failed: {err:?}"))
-    }
-    .unwrap();
-
-    #[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))]
-    verified_executable.jit_compile().unwrap();
-    let mut analysis = LazyAnalysis::new(verified_executable.get_executable());
-
-    match matches.value_of("use") {
-        Some("cfg") => {
-            let mut file = File::create("cfg.dot").unwrap();
-            analysis
-                .analyze()
-                .visualize_graphically(&mut file, None)
-                .unwrap();
-            return;
-        }
-        Some("disassembler") => {
-            let stdout = std::io::stdout();
-            analysis.analyze().disassemble(&mut stdout.lock()).unwrap();
-            return;
-        }
-        _ => {}
-    }
-    create_vm!(
-        vm,
-        &verified_executable,
-        regions,
-        account_lengths,
-        &mut invoke_context,
+    println!(
+        r##"rbpf-cli is replaced by solana-ledger-tool run subcommand.
+Please, use 'solana-ledger-tool run --help' for more information."##
     );
-    let mut vm = vm.unwrap();
-    let start_time = Instant::now();
-    if matches.value_of("use").unwrap() == "debugger" {
-        vm.debug_port = Some(matches.value_of("port").unwrap().parse::<u16>().unwrap());
-    }
-    let (instruction_count, result) = vm.execute_program(matches.value_of("use").unwrap() != "jit");
-    let duration = Instant::now() - start_time;
-    if matches.occurrences_of("trace") > 0 {
-        for (frame, syscall_context) in vm
-            .env
-            .context_object_pointer
-            .syscall_context
-            .iter()
-            .enumerate()
-        {
-            if syscall_context.is_none() {
-                continue;
-            }
-            let trace_log = syscall_context.as_ref().unwrap().trace_log.as_slice();
-            if matches.value_of("trace").unwrap() == "stdout" {
-                writeln!(&mut std::io::stdout(), "Frame {frame}").unwrap();
-                analysis
-                    .analyze()
-                    .disassemble_trace_log(&mut std::io::stdout(), trace_log)
-                    .unwrap();
-            } else {
-                let filename = format!("{}.{}", matches.value_of("trace").unwrap(), frame);
-                let mut fd = File::create(filename).unwrap();
-                writeln!(&fd, "Frame {frame}").unwrap();
-                analysis
-                    .analyze()
-                    .disassemble_trace_log(&mut fd, trace_log)
-                    .unwrap();
-            }
-        }
-    }
-    drop(vm);
-
-    let output = Output {
-        result: format!("{result:?}"),
-        instruction_count,
-        execution_time: duration,
-        log: invoke_context
-            .get_log_collector()
-            .unwrap()
-            .borrow()
-            .get_recorded_content()
-            .to_vec(),
-    };
-    match matches.value_of("output_format") {
-        Some("json") => {
-            println!("{}", serde_json::to_string_pretty(&output).unwrap());
-        }
-        Some("json-compact") => {
-            println!("{}", serde_json::to_string(&output).unwrap());
-        }
-        _ => {
-            println!("Program output:");
-            println!("{output:?}");
-        }
-    }
-}
-
-#[derive(Serialize)]
-struct Output {
-    result: String,
-    instruction_count: u64,
-    execution_time: Duration,
-    log: Vec<String>,
-}
-
-impl Debug for Output {
-    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        writeln!(f, "Result: {}", self.result)?;
-        writeln!(f, "Instruction Count: {}", self.instruction_count)?;
-        writeln!(f, "Execution time: {} us", self.execution_time.as_micros())?;
-        for line in &self.log {
-            writeln!(f, "{line}")?;
-        }
-        Ok(())
-    }
-}
-
-// Replace with std::lazy::Lazy when stabilized.
-// https://github.com/rust-lang/rust/issues/74465
-struct LazyAnalysis<'a, 'b> {
-    analysis: Option<Analysis<'a>>,
-    executable: &'a Executable<InvokeContext<'b>>,
-}
-
-impl<'a, 'b> LazyAnalysis<'a, 'b> {
-    fn new(executable: &'a Executable<InvokeContext<'b>>) -> Self {
-        Self {
-            analysis: None,
-            executable,
-        }
-    }
-
-    fn analyze(&mut self) -> &Analysis {
-        if let Some(ref analysis) = self.analysis {
-            return analysis;
-        }
-        self.analysis
-            .insert(Analysis::from_executable(self.executable).unwrap())
-    }
 }