docs updates

This commit is contained in:
Alexander Kiselev
2026-02-23 16:01:11 -08:00
parent 2541550b0e
commit af315c20cc
8 changed files with 433 additions and 60 deletions
+12 -8
View File
@@ -79,7 +79,7 @@ ghidra find string "password"
ghidra x-ref to 0x401000
# Generate call graph
ghidra graph calls main --depth 3
ghidra graph callers main --depth 3
```
## Commands
@@ -104,7 +104,7 @@ ghidra disasm <address> --instructions 20 # Disassemble instructions
### Symbols & Types
```bash
ghidra symbol list # List symbols
ghidra symbol create <name> <addr> # Create symbol
ghidra symbol create <addr> <name> # Create symbol
ghidra symbol rename <old> <new> # Rename symbol
ghidra type list # List data types
ghidra type get <name> # Get type details
@@ -127,9 +127,9 @@ ghidra find interesting # Find interesting patterns
### Call Graphs
```bash
ghidra graph calls <func> # Full call graph
ghidra graph callers <func> # Who calls this?
ghidra graph callees <func> # What does this call?
ghidra graph calls # Full call graph
ghidra graph callers <func> # Who calls this? (--depth optional)
ghidra graph callees <func> # What does this call? (--depth optional)
ghidra graph export dot # Export to DOT format
```
@@ -137,16 +137,20 @@ ghidra graph export dot # Export to DOT format
```bash
ghidra patch bytes <addr> "90 90" # Patch bytes
ghidra patch nop <addr> --count 5 # NOP out instructions
ghidra patch export # Export as patch file
ghidra patch export -o patched.bin # Export patched binary
```
Note: `patch nop --count` is currently parsed by the CLI, but runtime uses single-address NOP behavior.
### Comments
```bash
ghidra comment get <address> # Get comment
ghidra comment set <addr> "note" # Set comment
ghidra comment set <addr> "note" --comment-type EOL # Set comment
ghidra comment list # List all comments
```
Note: `--comment-type` currently falls back to `EOL` due client/bridge argument key mismatch.
### Scripts
```bash
ghidra script list # List available scripts
@@ -248,7 +252,7 @@ Example workflow with an AI agent:
3. `ghidra decompile <func>` - AI examines specific functions
4. `ghidra x-ref to <addr>` - AI traces data flow
5. `ghidra patch nop <addr>` - AI patches anti-debug code
6. `ghidra patch export` - Export patched binary
6. `ghidra patch export -o patched.bin` - Export patched binary
## Troubleshooting
+3
View File
@@ -1,5 +1,8 @@
# Plan: Complete All Stub Commands
> Historical planning document: parts of this plan assume an older daemon-centric architecture and may not match the current implementation.
> For current behavior, use `README.md`, `AGENTS.md`, and the CLI help output.
## Overview
ghidra-cli has 39 stub commands that output "not yet implemented". This plan implements all stub commands using a hybrid approach: grouped by category with shared helpers per group. Each category becomes a deployable unit with its own Ghidra bridge Python scripts, daemon routing, and E2E tests.
+3
View File
@@ -1,5 +1,8 @@
# E2E Test Coverage Plan
> Historical planning document: this plan captures prior design decisions and may not match current test architecture exactly.
> For current test behavior and commands, see `tests/README.md`.
## Overview
This plan addresses the critical E2E test coverage gap in ghidra-cli. Currently only 4 of 60+ CLI commands have active tests. The plan implements a modular test structure with daemon lifecycle management, enabling comprehensive testing of all CLI functionality including the 51+ untested commands.
+3
View File
@@ -1,5 +1,8 @@
# Ghidra-CLI Open Source Release Plan (Daemon-Only Architecture)
> Historical planning document: this does not reflect the current implementation.
> Current architecture is direct CLI-to-Java bridge (`GhidraCliBridge.java`) as documented in `README.md` and `AGENTS.md`.
## Overview
Prepare ghidra-cli for open source release with a **daemon-only architecture**. Binary analysis is slow enough that a persistent Ghidra process (via daemon) is always preferable to spawning new processes per command.
+117 -7
View File
@@ -312,12 +312,25 @@ pub enum FunctionCommands {
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
pub struct FunctionGetArgs {
/// Function address or name
pub target: String,
/// Function target (name/address/FUN_...)
#[arg(value_name = "TARGET", required_unless_present = "target")]
pub positional_target: Option<String>,
/// Function target (name | 0xaddr | FUN_<hex>)
#[arg(long = "target", value_name = "TARGET")]
pub target: Option<String>,
#[command(flatten)]
pub options: QueryOptions,
}
impl FunctionGetArgs {
pub fn resolved_target(&self) -> &str {
self.target
.as_deref()
.or(self.positional_target.as_deref())
.expect("clap should ensure target is provided")
}
}
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
pub struct RenameArgs {
pub old_name: String,
@@ -436,11 +449,25 @@ pub enum XRefCommands {
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
pub struct XRefArgs {
pub address: String,
/// XRef target (name | 0xaddr | FUN_<hex>)
#[arg(value_name = "TARGET", required_unless_present = "target")]
pub positional_target: Option<String>,
/// XRef target (name | 0xaddr | FUN_<hex>)
#[arg(long = "target", value_name = "TARGET")]
pub target: Option<String>,
#[command(flatten)]
pub options: QueryOptions,
}
impl XRefArgs {
pub fn resolved_target(&self) -> &str {
self.target
.as_deref()
.or(self.positional_target.as_deref())
.expect("clap should ensure target is provided")
}
}
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
pub enum TypeCommands {
/// List data types
@@ -555,11 +582,25 @@ pub struct FindFunctionArgs {
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
pub struct FindCallsArgs {
pub function: String,
/// Function target (name | 0xaddr | FUN_<hex>)
#[arg(value_name = "TARGET", required_unless_present = "target")]
pub positional_target: Option<String>,
/// Function target (name | 0xaddr | FUN_<hex>)
#[arg(long = "target", value_name = "TARGET")]
pub target: Option<String>,
#[command(flatten)]
pub options: QueryOptions,
}
impl FindCallsArgs {
pub fn resolved_target(&self) -> &str {
self.target
.as_deref()
.or(self.positional_target.as_deref())
.expect("clap should ensure target is provided")
}
}
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
pub enum GraphCommands {
/// Call graph
@@ -576,13 +617,27 @@ pub enum GraphCommands {
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
pub struct GraphFunctionArgs {
pub function: String,
/// Function target (name | 0xaddr | FUN_<hex>)
#[arg(value_name = "TARGET", required_unless_present = "target")]
pub positional_target: Option<String>,
/// Function target (name | 0xaddr | FUN_<hex>)
#[arg(long = "target", value_name = "TARGET")]
pub target: Option<String>,
#[arg(long)]
pub depth: Option<usize>,
#[command(flatten)]
pub options: QueryOptions,
}
impl GraphFunctionArgs {
pub fn resolved_target(&self) -> &str {
self.target
.as_deref()
.or(self.positional_target.as_deref())
.expect("clap should ensure target is provided")
}
}
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
pub struct GraphExportArgs {
/// Export format (e.g., dot, json)
@@ -594,14 +649,33 @@ pub struct GraphExportArgs {
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
pub struct DecompileArgs {
pub target: String,
/// Function target (name | 0xaddr | FUN_<hex>)
#[arg(value_name = "TARGET", required_unless_present = "target")]
pub positional_target: Option<String>,
/// Function target (name | 0xaddr | FUN_<hex>)
#[arg(long = "target", value_name = "TARGET")]
pub target: Option<String>,
#[command(flatten)]
pub options: QueryOptions,
}
impl DecompileArgs {
pub fn resolved_target(&self) -> &str {
self.target
.as_deref()
.or(self.positional_target.as_deref())
.expect("clap should ensure target is provided")
}
}
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
pub struct DisasmArgs {
pub address: String,
/// Disassembly target (name | 0xaddr | FUN_<hex>)
#[arg(value_name = "TARGET", required_unless_present = "target")]
pub positional_target: Option<String>,
/// Disassembly target (name | 0xaddr | FUN_<hex>)
#[arg(long = "target", value_name = "TARGET")]
pub target: Option<String>,
/// Number of instructions to disassemble
#[arg(long = "instructions", short = 'n')]
pub num_instructions: Option<usize>,
@@ -609,6 +683,15 @@ pub struct DisasmArgs {
pub options: QueryOptions,
}
impl DisasmArgs {
pub fn resolved_target(&self) -> &str {
self.target
.as_deref()
.or(self.positional_target.as_deref())
.expect("clap should ensure target is provided")
}
}
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
pub enum DiffCommands {
/// Compare two programs
@@ -838,3 +921,30 @@ pub struct SetupArgs {
#[arg(long)]
pub force: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_decompile_target_flag() {
let cli = Cli::try_parse_from(["ghidra", "decompile", "--target", "FUN_00401000"])
.expect("decompile --target should parse");
match cli.command {
Commands::Decompile(args) => assert_eq!(args.resolved_target(), "FUN_00401000"),
_ => panic!("expected decompile command"),
}
}
#[test]
fn parses_function_get_positional_target() {
let cli = Cli::try_parse_from(["ghidra", "function", "get", "main"])
.expect("function get positional target should parse");
match cli.command {
Commands::Function(FunctionCommands::Get(args)) => {
assert_eq!(args.resolved_target(), "main");
}
_ => panic!("expected function get command"),
}
}
}
+249 -8
View File
@@ -30,6 +30,7 @@ import ghidra.util.task.TaskMonitor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
@@ -188,6 +189,10 @@ public class GhidraCliBridge extends GhidraScript {
case "ping": return handlePing();
case "program_info": return handleProgramInfo();
case "list_functions": return handleListFunctions(args);
case "get_function": return handleGetFunction(args);
case "rename_function": return handleRenameFunction(args);
case "create_function": return handleCreateFunction(args);
case "delete_function": return handleDeleteFunction(args);
case "decompile": return handleDecompile(args);
case "list_strings": return handleListStrings(args);
case "list_imports": return handleListImports();
@@ -479,6 +484,242 @@ public class GhidraCliBridge extends GhidraScript {
return result;
}
private JsonObject functionToJson(Function func) {
JsonObject funcData = new JsonObject();
funcData.addProperty("name", func.getName());
funcData.addProperty("address", func.getEntryPoint().toString());
funcData.addProperty("size", func.getBody().getNumAddresses());
funcData.addProperty("entry_point", func.getEntryPoint().toString());
String sig = null;
try {
sig = func.getPrototypeString(false, false);
} catch (Exception e) {
// ignore
}
if (sig != null) {
funcData.addProperty("signature", sig);
} else {
funcData.add("signature", JsonNull.INSTANCE);
}
funcData.addProperty("calling_convention", func.getCallingConventionName());
String comment = func.getComment();
if (comment != null) {
funcData.addProperty("comment", comment);
} else {
funcData.add("comment", JsonNull.INSTANCE);
}
return funcData;
}
private String buildFunctionTargetHint(String target) {
if (currentProgram == null || target == null || target.isEmpty()) {
return "Function not found";
}
String query = target.toLowerCase();
List<String> containsMatches = new ArrayList<>();
List<String> fuzzyMatches = new ArrayList<>();
FunctionIterator iter = currentProgram.getFunctionManager().getFunctions(true);
while (iter.hasNext()) {
Function func = iter.next();
String name = func.getName();
String lname = name.toLowerCase();
if (lname.contains(query)) {
containsMatches.add(name);
} else if (query.length() >= 3 && levenshteinDistance(lname, query) <= 3) {
fuzzyMatches.add(name);
}
}
Collections.sort(containsMatches);
Collections.sort(fuzzyMatches);
List<String> suggestions = new ArrayList<>();
for (String name : containsMatches) {
suggestions.add(name);
if (suggestions.size() >= 5) break;
}
if (suggestions.size() < 5) {
for (String name : fuzzyMatches) {
if (!suggestions.contains(name)) suggestions.add(name);
if (suggestions.size() >= 5) break;
}
}
StringBuilder hint = new StringBuilder();
hint.append("Cannot resolve function target: ").append(target)
.append(". Try: ghidra function list --filter ").append(target);
if (!suggestions.isEmpty()) {
hint.append(". Closest matches: ").append(String.join(", ", suggestions));
}
return hint.toString();
}
private int levenshteinDistance(String a, String b) {
int n = a.length();
int m = b.length();
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) dp[i][0] = i;
for (int j = 0; j <= m; j++) dp[0][j] = j;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
dp[i][j] = Math.min(
Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1),
dp[i - 1][j - 1] + cost
);
}
}
return dp[n][m];
}
private JsonObject handleGetFunction(JsonObject args) {
if (currentProgram == null) return errorResult("No program loaded");
String target = getArgString(args, "address");
if (target == null || target.isEmpty()) {
return errorResult("Function target required");
}
Address addr = resolveAddress(target);
if (addr == null) {
return errorResult(buildFunctionTargetHint(target));
}
Function func = currentProgram.getFunctionManager().getFunctionContaining(addr);
if (func == null) {
return errorResult("No function at target " + target + ". Try: ghidra function list --filter " + target);
}
return functionToJson(func);
}
private JsonObject handleRenameFunction(JsonObject args) {
if (currentProgram == null) return errorResult("No program loaded");
String oldTarget = getArgString(args, "old_name");
String newName = getArgString(args, "new_name");
if (oldTarget == null || newName == null || oldTarget.isEmpty() || newName.isEmpty()) {
return errorResult("old_name and new_name required");
}
try {
Function func = findFunctionByNameOrAddress(oldTarget);
if (func == null) {
return errorResult(buildFunctionTargetHint(oldTarget));
}
int txId = currentProgram.startTransaction("Rename function");
try {
String oldName = func.getName();
func.setName(newName, SourceType.USER_DEFINED);
currentProgram.endTransaction(txId, true);
JsonObject result = new JsonObject();
result.addProperty("status", "renamed");
result.addProperty("old_name", oldName);
result.addProperty("new_name", newName);
result.addProperty("address", func.getEntryPoint().toString());
return result;
} catch (Exception e) {
currentProgram.endTransaction(txId, false);
throw e;
}
} catch (Exception e) {
return errorResult("Failed to rename function: " + e.getMessage());
}
}
private JsonObject handleCreateFunction(JsonObject args) {
if (currentProgram == null) return errorResult("No program loaded");
String target = getArgString(args, "address");
String requestedName = getArgString(args, "name");
if (target == null || target.isEmpty()) {
return errorResult("Function target required");
}
try {
Address addr = resolveAddress(target);
if (addr == null) {
return errorResult("Invalid function target: " + target + ". Expected address/symbol/FUN_<hex>.");
}
FunctionManager fm = currentProgram.getFunctionManager();
if (fm.getFunctionContaining(addr) != null) {
return errorResult("Function already exists at " + addr.toString());
}
String functionName = (requestedName == null || requestedName.isEmpty())
? ("FUN_" + addr.toString().replace(":", ""))
: requestedName;
int txId = currentProgram.startTransaction("Create function");
try {
Function created = fm.createFunction(functionName, addr, null, SourceType.USER_DEFINED);
if (created == null) {
currentProgram.endTransaction(txId, false);
return errorResult("Failed to create function at " + addr.toString());
}
currentProgram.endTransaction(txId, true);
JsonObject result = new JsonObject();
result.addProperty("status", "created");
result.addProperty("name", created.getName());
result.addProperty("address", created.getEntryPoint().toString());
return result;
} catch (Exception e) {
currentProgram.endTransaction(txId, false);
throw e;
}
} catch (Exception e) {
return errorResult("Failed to create function: " + e.getMessage());
}
}
private JsonObject handleDeleteFunction(JsonObject args) {
if (currentProgram == null) return errorResult("No program loaded");
String target = getArgString(args, "address");
if (target == null || target.isEmpty()) {
return errorResult("Function target required");
}
try {
FunctionManager fm = currentProgram.getFunctionManager();
Function func = findFunctionByNameOrAddress(target);
if (func == null) {
return errorResult(buildFunctionTargetHint(target));
}
Address entry = func.getEntryPoint();
String name = func.getName();
int txId = currentProgram.startTransaction("Delete function");
try {
fm.removeFunction(entry);
currentProgram.endTransaction(txId, true);
} catch (Exception e) {
currentProgram.endTransaction(txId, false);
throw e;
}
JsonObject result = new JsonObject();
result.addProperty("status", "deleted");
result.addProperty("name", name);
result.addProperty("address", entry.toString());
return result;
} catch (Exception e) {
return errorResult("Failed to delete function: " + e.getMessage());
}
}
private JsonObject handleDecompile(JsonObject args) {
if (currentProgram == null) {
return errorResult("No program loaded");
@@ -491,7 +732,7 @@ public class GhidraCliBridge extends GhidraScript {
Address addr = resolveAddress(addrStr);
if (addr == null) {
return errorResult("Cannot resolve address or function name: " + addrStr);
return errorResult(buildFunctionTargetHint(addrStr));
}
FunctionManager fm = currentProgram.getFunctionManager();
@@ -672,7 +913,7 @@ public class GhidraCliBridge extends GhidraScript {
Address addr = resolveAddress(addrStr);
if (addr == null) {
return errorResult("Cannot resolve address or function name: " + addrStr);
return errorResult(buildFunctionTargetHint(addrStr));
}
JsonArray xrefs = new JsonArray();
@@ -719,7 +960,7 @@ public class GhidraCliBridge extends GhidraScript {
Address addr = resolveAddress(addrStr);
if (addr == null) {
return errorResult("Cannot resolve address or function name: " + addrStr);
return errorResult(buildFunctionTargetHint(addrStr));
}
JsonArray xrefs = new JsonArray();
@@ -1310,7 +1551,7 @@ public class GhidraCliBridge extends GhidraScript {
Function targetFunc = findFunctionByNameOrAddress(functionTarget);
if (targetFunc == null) {
return errorResult("Function not found: " + functionTarget);
return errorResult(buildFunctionTargetHint(functionTarget));
}
ReferenceManager refMgr = currentProgram.getReferenceManager();
@@ -2100,7 +2341,7 @@ public class GhidraCliBridge extends GhidraScript {
int depth = getArgInt(args, "depth", 1);
Function targetFunc = findFunctionByNameOrAddress(funcName);
if (targetFunc == null) return errorResult("Function not found: " + funcName);
if (targetFunc == null) return errorResult(buildFunctionTargetHint(funcName));
ReferenceManager refMgr = currentProgram.getReferenceManager();
FunctionManager fm = currentProgram.getFunctionManager();
@@ -2151,7 +2392,7 @@ public class GhidraCliBridge extends GhidraScript {
int depth = getArgInt(args, "depth", 1);
Function targetFunc = findFunctionByNameOrAddress(funcName);
if (targetFunc == null) return errorResult("Function not found: " + funcName);
if (targetFunc == null) return errorResult(buildFunctionTargetHint(funcName));
ReferenceManager refMgr = currentProgram.getReferenceManager();
FunctionManager fm = currentProgram.getFunctionManager();
@@ -2300,8 +2541,8 @@ public class GhidraCliBridge extends GhidraScript {
Function func1 = findFunctionByNameOrAddress(func1Target);
Function func2 = findFunctionByNameOrAddress(func2Target);
if (func1 == null) return errorResult("Function not found: " + func1Target);
if (func2 == null) return errorResult("Function not found: " + func2Target);
if (func1 == null) return errorResult(buildFunctionTargetHint(func1Target));
if (func2 == null) return errorResult(buildFunctionTargetHint(func2Target));
DecompInterface decompiler = new DecompInterface();
try {
+23 -14
View File
@@ -628,20 +628,25 @@ fn execute_via_bridge(
"memory" => client.memory_map(),
other => anyhow::bail!("Query type '{}' not supported", other),
},
Commands::Decompile(args) => client.decompile(args.target.clone()),
Commands::Decompile(args) => client.decompile(args.resolved_target().to_string()),
Commands::Function(cmd) => {
use cli::FunctionCommands;
match cmd {
FunctionCommands::List(opts) => {
client.list_functions(opts.limit.or(default_limit), opts.filter.clone())
}
FunctionCommands::Decompile(args) => client.decompile(args.target.clone()),
FunctionCommands::Get(args) => {
client.send_command("get_function", Some(json!({"address": args.target})))
FunctionCommands::Decompile(args) => {
client.decompile(args.resolved_target().to_string())
}
FunctionCommands::Disasm(args) => client.disasm(&args.target, None),
FunctionCommands::Calls(args) => client.find_calls(&args.target),
FunctionCommands::XRefs(args) => client.xrefs_to(args.target.clone()),
FunctionCommands::Get(args) => {
client.send_command(
"get_function",
Some(json!({"address": args.resolved_target()})),
)
}
FunctionCommands::Disasm(args) => client.disasm(args.resolved_target(), None),
FunctionCommands::Calls(args) => client.find_calls(args.resolved_target()),
FunctionCommands::XRefs(args) => client.xrefs_to(args.resolved_target().to_string()),
FunctionCommands::Rename(args) => client.send_command(
"rename_function",
Some(json!({
@@ -659,7 +664,7 @@ fn execute_via_bridge(
FunctionCommands::Delete(args) => client.send_command(
"delete_function",
Some(json!({
"address": args.target,
"address": args.resolved_target(),
})),
),
}
@@ -712,8 +717,8 @@ fn execute_via_bridge(
Commands::XRef(cmd) => {
use cli::XRefCommands;
match cmd {
XRefCommands::To(args) => client.xrefs_to(args.address.clone()),
XRefCommands::From(args) => client.xrefs_from(args.address.clone()),
XRefCommands::To(args) => client.xrefs_to(args.resolved_target().to_string()),
XRefCommands::From(args) => client.xrefs_from(args.resolved_target().to_string()),
XRefCommands::List(_) => client.send_command("xrefs_list", None),
}
}
@@ -777,8 +782,12 @@ fn execute_via_bridge(
use cli::GraphCommands;
match cmd {
GraphCommands::Calls(opts) => client.graph_calls(opts.limit.or(default_limit)),
GraphCommands::Callers(args) => client.graph_callers(&args.function, args.depth),
GraphCommands::Callees(args) => client.graph_callees(&args.function, args.depth),
GraphCommands::Callers(args) => {
client.graph_callers(args.resolved_target(), args.depth)
}
GraphCommands::Callees(args) => {
client.graph_callees(args.resolved_target(), args.depth)
}
GraphCommands::Export(args) => client.graph_export(&args.format),
}
}
@@ -788,7 +797,7 @@ fn execute_via_bridge(
FindCommands::String(args) => client.find_string(&args.pattern),
FindCommands::Bytes(args) => client.find_bytes(&args.hex),
FindCommands::Function(args) => client.find_function(&args.pattern),
FindCommands::Calls(args) => client.find_calls(&args.function),
FindCommands::Calls(args) => client.find_calls(args.resolved_target()),
FindCommands::Crypto(_) => client.find_crypto(),
FindCommands::Interesting(_) => client.find_interesting(),
}
@@ -819,7 +828,7 @@ fn execute_via_bridge(
ScriptCommands::List => client.script_list(),
}
}
Commands::Disasm(args) => client.disasm(&args.address, args.num_instructions),
Commands::Disasm(args) => client.disasm(args.resolved_target(), args.num_instructions),
Commands::Batch(args) => {
// Read batch file and execute each command locally
let content = std::fs::read_to_string(&args.script_file)
+1 -1
View File
@@ -76,7 +76,7 @@ cargo test --test command_tests test_version
Run tests that don't need Ghidra:
```bash
cargo test --test e2e --test command_tests --test output_format_integration
cargo test --test e2e --test output_format_integration
```
## Test Requirements