English | 中文
ARM Cortex-M 运行时代码注入工具。通过串口连接,在不重新烧录、不需要调试器的情况下,替换正在运行的 MCU 上的任意函数。
FPBInject 利用 Flash Patch and Breakpoint(FPB)硬件单元拦截函数调用,将执行重定向到 RAM 中的自定义代码,原始 Flash 内容保持不变。
gantt
title 迭代周期对比(典型 STM32 项目)
dateFormat s
axisFormat %Ss
section 传统方式
修改代码 : a1, 0, 5s
编译链接 : a2, after a1, 15s
擦除 Flash : a3, after a2, 3s
烧录写入 : a4, after a3, 5s
MCU 重启 : a5, after a4, 2s
复现问题 : a6, after a5, 5s
section FPBInject
修改代码 : b1, 0, 5s
编译注入 : b2, after b1, 1s
复现问题 : b3, after b2, 5s
传统方式每次迭代都要经历编译、擦除、烧录、重启,才能复现问题。FPBInject 让 MCU 全程不停机:保存补丁,不到一秒即可生效,真正实现“不熄火修车”。
flowchart LR
A["caller()<br/>调用 foo()"] -->|"FPB 拦截<br/>foo 的地址"| B["跳板代码<br/>Flash 中"]
B -->|"跳转到 RAM"| C["你的代码<br/>RAM 中"]
FPB 单元匹配目标函数地址,通过 Flash 中的跳板代码将执行重定向到 RAM 中的替换函数。全程由硬件完成,调用路径零软件开销。
FPBInject 自带浏览器工作台,支持完整工作流:浏览符号、查看反汇编、编写补丁、一键注入。
搜索固件符号表,点击函数查看反汇编或反编译源码。
用 C 语言编写替换函数,点击注入。工作台自动编译、上传、打补丁,通常不到一秒完成。
在工作台中指定源码目录并开启文件监控。在需要替换的函数前添加 /* FPB_INJECT */ 标记,保存文件即可 — 工作台自动检测变更、重新编译并注入。
FPBInject 还支持通过串口进行文件传输 — 浏览、上传、下载设备文件系统中的文件。支持拖拽上传(文件和文件夹)、CRC 校验和传输进度显示。
文件系统后端:POSIX(NuttX VFS、Linux)、FatFS、标准 C 库(stdio),或通过 fl_fs_ops_t 接口自定义实现。
git clone https://github.com/FASTSHIFT/FPBInject.git
cd FPBInject
cmake -B build -DAPP_SELECT=3 -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi-gcc.cmake
cmake --build build
st-flash write build/FPBInject.bin 0x08000000cd Tools/WebServer
pip install -r ../requirements.txt
python main.py浏览器打开 http://127.0.0.1:5500,连接串口,加载 ELF 文件,即可开始注入。
所有命令输出 JSON,适合脚本和 AI 代理集成。
# 搜索函数
python fpb_cli.py search firmware.elf "gpio"
# 查看反汇编
python fpb_cli.py disasm firmware.elf digitalWrite
# 注入补丁
python fpb_cli.py --port /dev/ttyACM0 --elf firmware.elf \
--compile-commands build/compile_commands.json \
inject digitalWrite patch.c完整命令参考见 CLI 文档。
创建带 /* FPB_INJECT */ 标记的 C 文件,函数签名必须与原始函数一致。
#include <Arduino.h>
/* FPB_INJECT */
__attribute__((section(".fpb.text"), used))
void digitalWrite(uint8_t pin, uint8_t value) {
printf("Patched: pin=%d val=%d\n", pin, value);
value ? digitalWrite_HIGH(pin)
: digitalWrite_LOW(pin);
}如需在注入代码中调用原始函数,需要两步配合:用函数指针直接指向原始地址(绕过 FPB 重定向),同时在调用前后用
fpb_enable_patch临时禁用补丁。直接按函数名调用仍会被 FPB 拦截,导致无限递归。/* 定义指向原始地址的函数指针(| 1 设置 Thumb bit) */ typedef void (*digitalWrite_fn_t)(uint8_t, uint8_t); static digitalWrite_fn_t const ORIG_DIGITALWRITE = (digitalWrite_fn_t)(0x08001234 | 1); /* FPB_INJECT */ __attribute__((section(".fpb.text"), used)) void digitalWrite(uint8_t pin, uint8_t value) { printf("Patched: pin=%d val=%d\n", pin, value); /* 禁用补丁 -> 通过指针调用原始函数 -> 恢复补丁 */ fpb_enable_patch(0, false); ORIG_DIGITALWRITE(pin, value); fpb_enable_patch(0, true); }工作台在已知原始函数地址时会自动生成上述模板。
| 特性 | 规格 |
|---|---|
| 架构 | ARMv7-M, ARMv8-M |
| 已测试 MCU | STM32F103C8T6 |
| 补丁槽位 | 6 个(FPB v1)或 8 个(FPB v2) |
| 补丁模式 | Trampoline / Direct(ARMv7-M REMAP)、DebugMonitor(ARMv8-M BKPT) |
| RTOS 支持 | 裸机、NuttX |
| 连接方式 | 串口(USB 转 UART 或 USB CDC) |
CMake 构建选项
| 选项 | 默认值 | 说明 |
|---|---|---|
APP_SELECT |
1 | 应用选择(3 = func_loader) |
FL_ALLOC_MODE |
STATIC | 内存分配方式:STATIC 或 LIBC |
FPB_NO_DEBUGMON |
OFF | 禁用 DebugMonitor 模式 |
项目结构
FPBInject/
├── Source/ # FPB 驱动、跳板代码、DebugMonitor
├── App/
│ ├── func_loader/ # 串口协议、内存分配器、FPB 控制
│ ├── inject/ # 注入辅助代码
│ └── tests/ # 固件单元测试(宿主机运行,支持覆盖率)
├── Project/ # 平台 HAL(STM32F10x、Arduino API)
├── Tools/
│ └── WebServer/ # 工作台(Flask 后端 + JS 前端)& CLI
└── Docs/ # 架构文档、CLI 参考、WebServer 指南
| 文档 | 说明 |
|---|---|
| 架构 | FPB 内部原理、补丁模式、内存布局、协议 |
| CLI 参考 | 所有 CLI 命令及示例、JSON 输出格式 |
| WebServer 指南 | 工作台安装与使用 |





