Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

binscout is headless, agent-first binary analysis. Open a binary — ELF, PE, Mach-O, LE, or flat raw bytes — and get functions, control-flow graphs, cross-references, strings, symbols, and IR out.

There are two interfaces over the same engine:

  • binscout, a JSON CLI: every command prints exactly one JSON value to stdout.
  • binscout-mcp, an MCP server over stdio with 13 tools, one per capability.

Annotations (function renames, free-text comments) live in a redb sidecar next to the binary. The analyzed binary is never written.

Start with Installation, then the CLI reference.

Installation

Requires a Rust toolchain (stable).

From crates.io:

cargo install binscout

From the Git repository (latest master):

cargo install --git https://github.com/mimi1vx/binscout

Or from a checkout:

git clone https://github.com/mimi1vx/binscout
cd binscout
cargo install --path .

This installs two binaries: binscout (the JSON CLI) and binscout-mcp (the MCP server). Verify with:

binscout --help

CLI reference

binscout [--db PATH] <command> <binary> [args]

--db overrides the sidecar database (default <binary>.rsdbg.redb). See JSON protocol for the output contract.

CommandExampleOutput
loadbinscout load fw.elfproject info; caches the analysis snapshot
functionsbinscout functions fw.elf --query main --limit 50function rows, renames resolved; --query is a case-insensitive substring over the resolved name, --caller/--callee keep call-graph neighbors of the function containing that address, --range LO HI keeps entries in the inclusive bounds; page with --offset/--limit
funcbinscout func fw.elf 0x400000blocks with instructions, edges, calls
disasmbinscout disasm fw.elf 0x400000 32decoded instructions (addr = hex or decimal, then byte count); the valid prefix plus error_at for the first bad opcode, ranges cap at 1 MiB
cfgbinscout cfg fw.elf 0x400000block nodes plus (from, to) edges
xrefsbinscout xrefs fw.elf --to 0x400000 --kind codecross-references filtered by --to/--from/--kind (code, data, string), paged with --offset/--limit behind a {"total", "xrefs"} envelope
stringsbinscout strings fw.elf --pattern SAVE --min-len 6filtered strings by address (--pattern substring or regex: expression, --min-len, --section, --include-code; executable-memory hits hidden by default; paged by --start/--limit, --offset deprecated)
symbolsbinscout symbols fw.elfimports and exports
sectionsbinscout sections fw.elfsection table: name, vaddr, in-memory/file sizes, permissions
irbinscout ir fw.elf 0x400000 32P-code-inspired lift of the range (error_at on trailing bad bytes, ranges cap at 1 MiB)
statsbinscout stats fw.elfstorage stats: binary size, sidecar DB size, analysis counts
annotatebinscout annotate fw.elf 0x400000 --rename main --comment "entry"read/write annotation at an address

Paged lists cap at 1024 rows. Addresses accept 0x-hex or decimal strings. func/cfg snap any address inside the function to its containing entry. annotate without flags reads; --rename "" clears the rename, omitting --comment leaves the comment alone. rename only accepts known function entries; comment accepts any address.

sections rows carry name, vaddr (the flat virtual address the loader mapped the section at), size (in-memory bytes), file_off/file_size (locating the raw bytes in the file), and readable/writable/executable. On PE, vaddr = IMAGE_BASE + RVA: .text at RVA 0x1000 in a 0x400000-based image lives at 0x401000, with file_off pointing at its PointerToRawData. Raw blobs have no section table and report [].

JSON protocol

Every command prints exactly one JSON value to stdout and exits 0. A runtime failure prints a single {"error": "..."} value and exits 1, so stdout is always valid JSON. (Clap usage errors for unknown flags/subcommands still go to stderr with exit 2.)

Real example — opening the repo’s tests/fixtures/raw-x64.bin:

$ binscout load tests/fixtures/raw-x64.bin
{"annotations":0,"arch":"x86_64","binary":"tests/fixtures/raw-x64.bin","blocks":1,"content_hash":13197063251454239466,"db_path":"tests/fixtures/raw-x64.rsdbg.redb","entry":null,"entry_hex":null,"format":"raw","functions":1,"insns":3,"reason":null,"size":3,"strings":0,"supported":true,"version":"0.2.1","xrefs":0}

functions and xrefs paginate with --offset/--limit; strings pages with --start/--limit (--offset is a deprecated alias that warns). Limits cap server-side at 1024 rows. xrefs returns a {"total", "xrefs"} envelope where total is the filter-matched count before paging, so large result sets page without scanning blind. Its per-row offset field is the byte offset of the hit, not the page position. disasm and ir decode at most 1 MiB per call and report trailing bad bytes in error_at instead of failing, so the valid prefix is always usable.

MCP tools

binscout-mcp serves 13 tools over stdio (newline-delimited JSON-RPC). Each takes binary plus an optional db path and returns one JSON document as text; failures are tool errors. Attach it to any MCP client:

{
  "mcpServers": {
    "binscout": { "command": "/path/to/binscout-mcp" }
  }
}
ToolParametersDoes
project_infobinaryOpen a binary and report arch, format, entry, and analysis counts
functionsbinary, query/caller/callee/range_lo/range_hi, offset/limitList functions with rename-resolved names, filtered (query case-insensitive substring; caller/callee addresses resolving to call-graph neighbors; range_lo + range_hi bounding entries) and paged
funcbinary, addrFull detail of one function: blocks with instructions, edges, calls
disasmbinary, addr, lenDisassemble len bytes at addr with the binary architecture; the valid prefix plus error_at for the first bad opcode (ranges cap at 1 MiB)
cfgbinary, addrControl-flow graph of one function: block nodes plus block edges
xrefsbinary, to/from/kind, offset/limitCross-references filtered by endpoint and kind, paged behind a total match count
stringsbinary, pattern/min-len/section/include_code, start/limitExtracted strings ordered by address, filtered and paged; executable-memory hits hidden unless pattern, section, or include_code is given (offset is a deprecated alias of start)
symbolsbinaryImports and exports of the binary
sectionsbinarySection table: name, virtual address (IMAGE_BASE + RVA on PE), in-memory/file sizes, permissions
irbinary, addr, lenLift len bytes at addr to P-code-inspired IR (error_at on trailing bad bytes, ranges cap at 1 MiB)
statsbinaryStorage stats: binary size, sidecar DB size, and analysis counts
renamebinary, addr, nameRename a function in the project DB (binary untouched); empty name clears
commentbinary, addr, commentComment any address in the project DB (binary untouched); empty comment clears

Addresses are 0x-hex or decimal strings. kind is one of code, data, or string. Calls are serialized behind one lock because redb allows a single open handle per file per process.

Architecture

One engine, two interfaces. Data flows one way — loader, disassembler, IR, analysis, database — and both the CLI (binscout) and the MCP server (binscout-mcp) are thin adapters over it.

  • Loader (loader): sniffs ELF, PE, Mach-O, and LE, falling back to flat raw bytes when nothing matches: unrecognized input opens as x86-64 at base 0, LE images analyze as 32-bit x86. Explicit base/architecture mapping goes through open_raw. Produces segments, sections, imports/exports, and printable strings (ASCII plus UTF-16LE/BE over the ASCII subset). Inputs over 512 MiB are rejected before parsing.
  • Disassembler (disasm): x86_64 and 32-bit x86 via iced-x86, AArch64, 32-bit ARM, and Thumb via yaxpeax-arm. Each instruction becomes an Insn with its bytes, mnemonic, operand strings, and resolved branch/data targets.
  • IR (ir): lifts one instruction to one P-code-inspired IrInsn (COPY, LOAD, BRANCH, CALL, …). Unknown mnemonics become NOP with a warning instead of failing, so analysis proceeds over partial lifts.
  • Analysis (analysis): recursive descent from the entry point and export addresses over decoded instructions, plus a prologue scan for functions unreachable from those seeds. Builds per-function CFGs, a callgraph, and code/data/string cross-references.
  • Database (db): caches the snapshot and the user annotations in a redb sidecar; see Sidecar format. A content hash gates reuse — a changed binary is re-analyzed.

Sidecar format

Analysis results and annotations persist in a redb database next to the binary: <binary>.rsdbg.redb by default, overridden per-invocation with --db PATH (CLI) or the db parameter (MCP tools). The analyzed binary itself is only ever read.

On open, the stored content hash is compared against the file; a mismatch means the binary changed and it is re-analyzed (annotations carry over by address). A sidecar written by an older schema version behaves the same way: annotations carry over and the binary re-analyzes. Row values are JSON.

TableKeyValue
metastringschema version, path, hash, size, arch, format, entry
binaries0snapshot metadata
functionsentry addressfunction row (name, callees)
blocksblock keyblock row (address range, successors)
xrefsxref keycross-reference row
stringsrow indexextracted-string row
annotationsaddressrename and/or comment

Block rows store address ranges plus per-instruction boundaries, not rendered instructions: the open path re-decodes each recorded address from the binary, so snapshots stay small no matter how large the disassembly grows.

Delete the sidecar to drop cached analysis and annotations; it is rebuilt on the next load.

Size and upkeep

binscout stats <binary> reports the binary size (size), the sidecar size (db_size, 0 before the first load), and the analysis counts (functions/blocks/insns/xrefs/strings/annotations), so the storage cost of a snapshot stays visible. Snapshots store block address ranges rather than rendered instructions, so sidecars stay proportional to the binary; CI caps the synthetic fixtures at 5 MiB to catch snapshot bloat.

save rewrites every table, so repeated re-analysis can leave free pages behind. There is no separate vacuum step: redb has no VACUUM command. Call binscout::db::compact_db (wrapping redb::Database::compact) to reclaim the space when a sidecar grows unexpectedly.

Never commit sidecars. Keep them out of version control:

*.rsdbg.redb

Contributing

The full guide lives in CONTRIBUTING.md. The short version — the gate for any change:

cargo test          # full suite (unit + CLI/MCP e2e over fixtures)
cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo doc --no-deps # warning-free (`#![warn(missing_docs)]` is on)

Work is tracked with br/bd issues; file bugs and feature requests as issues first and keep changes small and scoped. Commits follow Conventional Commits: feat: bumps minor, fix: bumps patch. New behavior needs a test; new public API needs rustdoc. The project is dual-licensed MIT OR Apache-2.0 — new files carry no headers, the license applies repo-wide.