mirror of
https://github.com/rizinorg/rizin-mcp.git
synced 2026-11-07 06:46:00 -10:00
Make the pseudo decompiler output identical on both backends
* The `pseudo` engine's text differed between backends (2652 vs 3112 bytes in the QA run) with identical eval settings. The cause is in Rizin: since rizin#6062, `ds_init()` suppresses the jump arrows (`asm.lines`) whenever `core->is_pipe` is set, and `rz_core_cmd_str()` sets it for every captured command, so in-process `pd*` text never has them. Clearing `is_pipe` from the outside is not possible, and would let `!` commands write to the MCP stdout. * Render the pseudo code without jump arrows (`asm.lines=false`), which makes both backends print the same text, and with `pdr` instead of `pdf`: on /bin/ls, `pdf @ main` takes ~30 s (in Rizin 0.9.1 and dev alike) because every pointer operand classifies a buffer as large as the whole function, while `pdr` prints the same blocks in 0.4 s. * A test decompiles `main`, `entry0` and the three largest functions on both backends and requires identical text within 10 s each.
This commit is contained in:
1 parent
410424658e
commit
df1579e0ad
3 files changed
+62
-9
No files matched your search
@@ -26,7 +26,9 @@ The server can talk to Rizin two ways, selected with `--transport`:
|
||||
| Run needs | nothing external | a `rizin` **binary** |
|
||||
| Hard-cancel a stuck command | No (in-process C call can't be force-killed) | Yes (the process is killed on timeout) |
|
||||
|
||||
Both run the exact same Rizin commands and return identical JSON. On this
|
||||
Both run the exact same Rizin commands and return identical JSON. Plain-text
|
||||
disassembly through the raw `command` tool can differ in one respect: Rizin
|
||||
omits the jump arrows (`asm.lines`) from output captured in process. On this
|
||||
machine, opening `/bin/ls` and issuing commands measured roughly:
|
||||
|
||||
```
|
||||
@@ -135,11 +137,12 @@ produces the full listing, which the server then slices.
|
||||
|
||||
### Notes on a few tools
|
||||
|
||||
- **Decompilation.** Rizin 0.9 has no bundled C decompiler. `decompile` uses
|
||||
- **Decompilation.** Rizin has no bundled C decompiler. `decompile` uses
|
||||
whichever decompiler plugin is installed — rz-ghidra (`pdg`) or jsdec (`pdd`) —
|
||||
and otherwise falls back to built-in pseudo-disassembly (an `asm.pseudo`
|
||||
rendering of the function, applied via a temporary eval so no global state
|
||||
changes). `decompiler` can force `ghidra`, `jsdec`, or `pseudo`; `decompilers`
|
||||
and otherwise falls back to built-in pseudo-disassembly: an `asm.pseudo`
|
||||
rendering of the function's basic blocks (`pdr`), without jump arrows, via
|
||||
temporary evals so no global state changes. That text is identical on both
|
||||
backends. `decompiler` can force `ghidra`, `jsdec`, or `pseudo`; `decompilers`
|
||||
reports what is available. See *External plugins* below.
|
||||
- **FLIRT.** The `flirt_*` tools wrap Rizin's built-in `F` command family:
|
||||
`flirt_list`/`flirt_apply_sigdb` work against the signature database (sigdb)
|
||||
|
||||
+8
-4
@@ -740,7 +740,8 @@ impl RizinServer {
|
||||
#[tool(
|
||||
description = "Decompile the function at a location to pseudo-C. `decompiler` selects \
|
||||
the engine: `auto` (default) tries rz-ghidra (pdg), then jsdec (pdd), then built-in \
|
||||
pseudo-disassembly; or force `ghidra`, `jsdec`, or `pseudo`. rz-ghidra and jsdec are \
|
||||
pseudo-disassembly of the function's basic blocks (pdr); or force `ghidra`, `jsdec`, \
|
||||
or `pseudo`. rz-ghidra and jsdec are \
|
||||
optional plugins; see `decompilers` for what is installed. The engine actually used is \
|
||||
reported in structuredContent."
|
||||
)]
|
||||
@@ -754,11 +755,14 @@ impl RizinServer {
|
||||
// (engine name, command) candidates for each selection.
|
||||
let ghidra = ("ghidra", format!("pdg{suffix}"));
|
||||
let jsdec = ("jsdec", format!("pdd{suffix}"));
|
||||
// Pseudo-disassembly of the whole function, via a temporary eval so the
|
||||
// global `asm.pseudo` setting is left untouched.
|
||||
// Pseudo-disassembly of the whole function, with temporary evals so the
|
||||
// global settings are left untouched. Without the jump arrows
|
||||
// (`asm.lines`), which Rizin omits from captured in-process output, the
|
||||
// text is the same on both backends. `pdr` walks the basic blocks:
|
||||
// `pdf` spends seconds per pointer operand in a large function.
|
||||
let pseudo = (
|
||||
"pseudo",
|
||||
format!("pdf @e:asm.pseudo=true{}", at_suffix(&p.at).mcp()?),
|
||||
format!("pdr @e:asm.pseudo=true @e:asm.lines=false{suffix}"),
|
||||
);
|
||||
|
||||
let candidates: Vec<(&str, String)> = match which.as_str() {
|
||||
|
||||
@@ -602,3 +602,49 @@ async fn errors_say_what_went_wrong() {
|
||||
}
|
||||
let _ = std::fs::remove_file(&pat);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pseudo_decompile_is_the_same_on_both_backends() {
|
||||
let backends = transports();
|
||||
if backends.len() < 2 {
|
||||
eprintln!("skipping: needs both backends");
|
||||
return;
|
||||
}
|
||||
let mut texts: Vec<Vec<String>> = Vec::new();
|
||||
for t in &backends {
|
||||
let s = Session::start(*t, |_| {}).await;
|
||||
s.ok(
|
||||
"open_file",
|
||||
json!({ "path": target(), "analysis": "basic" }),
|
||||
)
|
||||
.await;
|
||||
// `main`, `entry0`, and a few more of the larger functions.
|
||||
let funcs = s.items("list_functions", json!({ "limit": 1000 })).await;
|
||||
let mut sized: Vec<(u64, u64)> = funcs
|
||||
.iter()
|
||||
.filter_map(|f| Some((f["size"].as_u64()?, f["offset"].as_u64()?)))
|
||||
.collect();
|
||||
sized.sort_unstable_by(|a, b| b.cmp(a));
|
||||
let mut at: Vec<String> = vec!["main".into(), "entry0".into()];
|
||||
at.extend(sized.iter().take(3).map(|(_, o)| format!("0x{o:x}")));
|
||||
let mut out = Vec::new();
|
||||
for a in &at {
|
||||
let started = std::time::Instant::now();
|
||||
let r = s
|
||||
.call("decompile", json!({ "at": a, "decompiler": "pseudo" }))
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("{t:?} {a}: {e:?}"));
|
||||
assert!(
|
||||
started.elapsed() < std::time::Duration::from_secs(10),
|
||||
"{t:?} {a}: took {:?}",
|
||||
started.elapsed()
|
||||
);
|
||||
out.push(r.content[0].as_text().unwrap().text.clone());
|
||||
}
|
||||
texts.push(out);
|
||||
}
|
||||
for (i, (a, b)) in texts[0].iter().zip(&texts[1]).enumerate() {
|
||||
assert!(!a.trim().is_empty());
|
||||
assert_eq!(a, b, "function #{i} differs between backends");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user