A lightweight command-line interface for ARM binary patching designed for AI agent integration.
fpb_cli.py is a pure CLI tool located at Tools/WebServer/fpb_cli.py. All commands output JSON for easy parsing by AI assistants or scripts.
- Python 3.8+
- ARM GCC toolchain (
arm-none-eabi-gcc) - pyserial (
pip install pyserial) for device communication - Optional: Ghidra for decompilation
cd Tools/WebServer
pip install pyserialfpb_cli.py [OPTIONS] <command> [args...]
Options:
-v, --verbose Enable verbose output
--version Show version
--port, -p <device> Serial port (e.g., /dev/ttyACM0, COM3)
--baudrate, -b <rate> Serial baudrate (default: 115200)
--elf <path> Path to ELF file (global default)
--compile-commands <path> Path to compile_commands.json
--tx-chunk-size <bytes> TX chunk size for serial commands (0=disabled, default: 0)
--tx-chunk-delay <secs> Delay between TX chunks in seconds (default: 0.005)
--max-retries <num> Maximum retry attempts for file transfer (default: 10)
--direct Force direct serial connection (skip WebServer proxy detection)
--server-url <url> WebServer URL for proxy mode (default: http://localhost:5500)fpb_cli.py analyze <elf_path> <func_name>Output:
{
"success": true,
"analysis": {
"func_name": "digitalWrite",
"addr": "0x08001234",
"signature": "void digitalWrite(uint8_t, uint8_t)",
"asm_lines": 12
}
}fpb_cli.py disasm <elf_path> <func_name>Output:
{
"success": true,
"func_name": "digitalWrite",
"disasm": "push {r7, lr}\nmov r7, sp\n...",
"language": "arm_asm"
}fpb_cli.py decompile <elf_path> <func_name>Requires Ghidra. Set
ghidra_pathin config or ensureanalyzeHeadlessis in PATH.
fpb_cli.py signature <elf_path> <func_name>fpb_cli.py search <elf_path> <pattern>Output:
{
"success": true,
"pattern": "gpio",
"count": 5,
"symbols": [
{"name": "gpio_init", "addr": "0x08001000"},
{"name": "gpio_write", "addr": "0x08001020"}
]
}More comprehensive than search — returns all symbol types via nm, with optional filtering.
fpb_cli.py get-symbols <elf_path> [--filter <pattern>] [--limit <num>]Options:
--filter <pattern>— Case-insensitive substring filter (default: all symbols)--limit <num>— Maximum results, 0 for unlimited (default: 0)
Output:
{
"success": true,
"count": 3,
"total": 150,
"symbols": [
{"name": "gpio_init", "addr": "0x08001000", "type": "func"},
{"name": "gpio_pin_map", "addr": "0x08002000", "type": "other"}
]
}fpb_cli.py compile <source_file> --elf <elf> --compile-commands <path> [--addr <base_addr>]Options:
--addr <base_addr>— Base address for patch code (default: 0x20001000)
Output:
{
"success": true,
"binary_size": 55,
"base_addr": "0x20001000",
"symbols": {"digitalWrite": "0x20001000"}
}fpb_cli.py --port /dev/ttyACM0 connectEstablishes a serial connection. Required before any online command when using --direct mode.
fpb_cli.py disconnectfpb_cli.py server-stop [--server-port <port>]Terminates a WebServer background process that was auto-launched by the CLI.
PID files are stored per-port in the WebServer directory (.cli_server_<port>.pid),
so multiple CLI servers on different ports can coexist.
If --server-port is omitted and only one CLI server is running, it auto-detects.
If multiple are running, you must specify which port to stop.
Output:
{"success": true, "message": "Server on port 5500 (PID 12345) terminated"}If no CLI-launched server is running:
{"success": false, "error": "No CLI-launched server is running"}fpb_cli.py --port /dev/ttyACM0 infoOutput:
{
"success": true,
"info": {
"slots": [...],
"total_slots": 6,
"active_slots": 0
}
}fpb_cli.py --port <device> --elf <elf> --compile-commands <path> \
inject <target_func> <source_file> [options]
Options:
--mode <mode> Patch mode: trampoline|debugmon|direct (default: trampoline)
--comp <num> FPB slot number (-1 for auto, default: -1)
--verify Verify patch after injectionExample:
fpb_cli.py --port /dev/ttyACM0 --elf firmware.elf \
--compile-commands build/compile_commands.json \
inject digitalWrite patch_digitalWrite.cOutput:
{
"success": true,
"result": {
"compile_time": 0.03,
"upload_time": 0.02,
"total_time": 0.13,
"code_size": 55,
"inject_func": "digitalWrite",
"target_addr": "0x08008608",
"inject_addr": "0x20000250",
"slot": 0,
"patch_mode": "trampoline"
}
}fpb_cli.py --port /dev/ttyACM0 unpatch --comp <slot>
fpb_cli.py --port /dev/ttyACM0 unpatch --all3-phase probing to find optimal transfer parameters.
fpb_cli.py --port /dev/ttyACM0 test-serial [options]
Options:
--start-size <bytes> Starting test size (default: 16)
--max-size <bytes> Maximum test size (default: 4096)
--timeout <secs> Timeout per test (default: 2.0)fpb_cli.py --port /dev/ttyACM0 serial-send <data> [options]
Options:
--no-read Don't read response after sending
--timeout <secs> Response read timeout (default: 1.0)Output:
{
"success": true,
"sent": "ps",
"response": " PID GROUP PRI POLICY TYPE NPX STATE ..."
}WARNING: Avoid sending
flcommands directly — useinject/unpatch/infoinstead.
fpb_cli.py --port /dev/ttyACM0 serial-read [options]
Options:
--timeout <secs> How long to wait for data (default: 1.0)
--lines <num> Max log lines to return (default: 50)Output:
{
"success": true,
"new_data": "Patched: pin=13 val=1\r\n",
"log": ["Patched: pin=13 val=1"],
"log_count": 1,
"total_buffered": 1
}fpb_cli.py --port /dev/ttyACM0 mem-read <addr> <length> [--fmt hex|raw|u32]Example:
fpb_cli.py --port /dev/ttyACM0 mem-read 0x20000000 64 --fmt hexfpb_cli.py --port /dev/ttyACM0 mem-write <addr> <hex_data>Example:
fpb_cli.py --port /dev/ttyACM0 mem-write 0x20001000 DEADBEEF01020304fpb_cli.py --port /dev/ttyACM0 mem-dump <addr> <length> <output_file>Example:
fpb_cli.py --port /dev/ttyACM0 mem-dump 0x20000000 4096 /tmp/ram.binfpb_cli.py --port /dev/ttyACM0 file-list [path]Default path is /.
fpb_cli.py --port /dev/ttyACM0 file-stat <path>Returns size, modification time, and type (file/dir).
fpb_cli.py --port /dev/ttyACM0 file-download <remote_path> <local_path>Transfers via chunked Base64 encoding with CRC verification.
Example:
fpb_cli.py --port /dev/ttyACM0 file-download /data/log.bin /tmp/log.bin# Step 1: Search for target functions (offline)
fpb_cli.py search firmware.elf "write"
# Step 2: Analyze the target function
fpb_cli.py analyze firmware.elf digitalWrite
# Step 3: Get disassembly for understanding
fpb_cli.py disasm firmware.elf digitalWrite
# Step 4: Compile and validate patch offline
fpb_cli.py compile patch.c --elf firmware.elf --compile-commands build/compile_commands.json
# Step 5: Inject to device
fpb_cli.py --port /dev/ttyACM0 --elf firmware.elf \
--compile-commands build/compile_commands.json \
inject digitalWrite patch.c
# Step 6: Verify or rollback
fpb_cli.py --port /dev/ttyACM0 info
fpb_cli.py --port /dev/ttyACM0 unpatch --comp 0Create a source file with /* FPB_INJECT */ marker:
// patch_digitalWrite.c
#include <stdint.h>
#include <stdio.h>
/* FPB_INJECT */
__attribute__((section(".fpb.text"), used))
void digitalWrite(uint8_t pin, uint8_t val) {
printf("Patched: pin=%d val=%d\r\n", (int)pin, (int)val);
}Note: Calling the original function from injected code is NOT supported due to FPB hardware limitations.
Replace and log parameters:
/* FPB_INJECT */
__attribute__((section(".fpb.text"), used))
void myFunc(int a, int b) {
printf("myFunc: a=%d, b=%d\r\n", a, b);
// Original function is NOT called - this code replaces it entirely
}Log and continue (trampoline mode):
// In trampoline mode, after your inject function returns,
// the original function is automatically called
/* FPB_INJECT */
__attribute__((section(".fpb.text"), used))
void myFunc(int a, int b) {
printf("myFunc called\r\n");
// Trampoline will redirect to original automatically
}Skip original function:
/* FPB_INJECT */
__attribute__((section(".fpb.text"), used))
void myFunc(void) {
// Return without doing anything - effectively disables the function
return;
}All commands return JSON:
// Success
{"success": true, "data": {...}}
// Error
{"success": false, "error": "Error message"}- Always check the
successfield before processing results - Use
jqfor parsing complex JSON outputs:fpb_cli.py search firmware.elf gpio | jq '.symbols[].name'
- Use
-vverbose mode for debugging - FPB slots range from 0-5 (6 slots typical)
- Patch functions MUST include the
/* FPB_INJECT */comment marker - Use
\r\nline endings for proper serial output display
- Architecture - Technical implementation details
- WebServer Guide - Web-based injection interface