最近写了一个项目:gdb-mcp。
一句话概括,它是一个面向 MCP 客户端的 GDB server,让 Codex、Claude Code 之类的 agent 可以通过结构化 tool 调用来控制 GDB,而不是把 GDB 当成普通 shell 程序来用。
TL;DR #
gdb-mcp 想解决的问题是:让 agent 能够稳定地进入动态调试循环。
- 用 GDB/MI 和 MCP tool 封装 GDB,而不是让 agent 自己拼裸命令、解析终端输出
- 显式维护多 GDB session,避免“当前调试目标”这种隐式状态
- 为 run / continue / step 这类操作返回 compact context,包括当前位置、backtrace、locals 和 stop reason
- 默认关闭写内存、调用函数、任意 GDB 命令等危险能力
- 支持本地程序、attach、core dump、gdbserver 和 reverse debugging 等逆向/漏挖常见场景
我的理解是,agent 想做自动化逆向和漏洞挖掘,不能只有静态分析能力。很多关键事实只存在于运行时,而 GDB 正是最通用的运行时观察入口。
初心 #
我一开始想做这个东西,是因为我觉得 GDB MCP 对 agent 自动化逆向和漏洞挖掘很重要。
逆向和漏挖并不只是“看静态代码”。很多时候真正有用的信息来自动态过程:
- 一个输入走到了哪条分支
- 某个变量在崩溃前被改成了什么
- 堆块、寄存器、栈帧、线程状态现在是什么样
- patch 一个条件、改一个变量、倒回一步之后行为是否变化
- core dump 里保存的现场能不能还原出漏洞路径
这些工作以前主要由人来驱动 GDB:下断点、运行、单步、看 backtrace、看 locals、读内存、继续跑,再根据结果调整下一步。Agent 想进入这个循环,就需要一个稳定的调试接口。
如果没有这样的接口,agent 只能把 GDB 当成一个普通命令行程序来用。这会带来几个问题:输出格式不稳定、上下文太长、状态不清晰,并且很难区分“读一下状态”和“修改目标程序”。
市面上已经有一些 GDB MCP,但我试下来都不太符合自己想要的工作流。主要问题如下:
- 会话模型不够明确,多目标调试时容易混在一起
- 过度依赖裸
gdb_execute,agent 需要自己拼命令、自己解析输出 - 输出没有为 LLM 上下文做压缩,一次 backtrace 或 disassemble 就可能塞进很多无关内容
- 安全边界不清楚,读状态、控制执行、写内存、调用 inferior 函数混在一起
- 对 attach、core、gdbserver、reverse debugging 这些逆向/漏洞场景常用能力支持不足
于是我决定自己(vibe)写一个。
设计目标 #
gdb-mcp 的目标不是把所有 GDB 命令原样搬到 MCP 里,而是给 agent 一个更适合自动化推理的调试抽象。
目前它的定位大概是:
- Linux-first,优先保证本地 GDB / core / attach / gdbserver 的常见路径可靠
- 每个调试目标都有显式
session_id,不存在隐式当前会话 - 常用操作都做成专用 tool,而不是让 agent 到处执行裸命令
- 执行类工具尽量直接返回当前位置、栈和局部变量,减少 agent 再发多轮查询
- 默认安全,写内存、调用函数、任意 GDB 命令必须显式开启 unsafe
- 支持 Codex、Claude Code 以及任何标准 MCP client
目前版本到了 0.3.0,工具数量五十多个,覆盖 session 管理、执行控制、断点、线程/栈帧、表达式求值、反汇编、内存读取、core、attach、gdbserver、reverse debugging 和诊断信息。
技术路线 #
整体结构并不复杂:
MCP client
|
| stdio
v
gdb-mcp lazy proxy
|
| first gdb_* tool call
v
gdb-mcp backend
|
| GDB/MI
v
GDB / gdbserver / target program
这里有两个关键选择:一是用 GDB/MI,而不是直接解析普通 CLI 输出;二是默认入口是一个 lazy stdio proxy。
GDB/MI #
GDB 普通 CLI 输出是给人看的,不是给程序解析的。同样是一个栈帧、一个断点、一次停止原因,CLI 输出经常依赖格式化文本。Agent 当然可以“读懂”文本,但如果要做自动化,就不能每一步都赌模型的自然语言解析能力。
GDB/MI 的输出更接近结构化协议,例如 result record、exec async record、stream record 都有固定形态。项目里写了一个轻量 MI parser,把 GDB 输出解析成统一的 MIRecord:
^done、^error这类 result record 用来判断命令是否成功*stopped、*running这类 async record 用来维护 inferior 状态~、@、&分别对应 console、target、log stream- token 用来把命令和返回结果关联起来
这样 MCP tool 返回给 agent 的就不是一整段终端文本,而是 JSON 结构:ok、result_class、results、stopped、console、target、async、raw 等字段都被拆出来了。
多会话 #
另一个设计点是多会话。
我不希望这个工具存在“当前正在调试的程序”这种隐式全局状态。Agent 只要跑久一点,就很容易出现:
- 上一轮开了一个 GDB 没关
- 用户又让它看另一个 binary
- 过程中还 attach 了一个进程
- 后面一句“继续运行”到底指哪个目标不明确
所以 gdb-mcp 的所有 session 操作都围绕显式 session_id 展开。SessionManager 负责创建、查找、关闭 session;每个 GdbSession 内部维护自己的 GDB 子进程、命令 token、pending command、最近事件、最近命令、last stop 和可选的 gdbserver 子进程。
这对于 agent 来说很重要。它可以先 gdb_list_sessions 看当前有哪些会话,再决定复用还是新建;多个程序并行分析时,也不会因为“当前 GDB”被覆盖而混乱。
命令路由 #
GDB/MI 比较麻烦的一点是异步。一次命令发出去之后,可能先收到 ^running,随后收到若干 stream record,最后收到 *stopped;也可能命令本身已经 ^done,但 inferior 还没有停。
gdb-mcp 的处理方式是:
- 每条命令分配一个递增 token。
_PendingCommand记录这个 token 对应的 future、result record、stopped record 和中间 records。- reader task 持续读取 GDB stdout,解析成
MIRecord。 - 根据 token 和 record 类型把输出路由到对应 pending command。
- 对
run、continue、step这类命令,可以设置wait_for_stop=True,等到真正停止后再返回。
这比简单地“发一行命令,读到 prompt 就结束”要麻烦,但对调试是必要的。因为 agent 关心的往往不是命令是否发出成功,而是程序现在停在了哪里、为什么停、还能看到什么现场。
Demo #
仓库里放了一个最小 demo,用来验证最核心的调试路径:编译一个简单 C 程序,下断点,运行到目标函数,然后让 agent 一次性拿到现场。
demo 程序如下:
#include <stdio.h>
static int add(int a, int b) {
int total = a + b;
return total;
}
int main(void) {
int value = add(2, 40);
printf("value=%d\n", value);
return value == 42 ? 0 : 1;
}
先编译:
cc -g -gdwarf-4 -O0 examples/hello.c -o /tmp/gdb-mcp-hello
然后在 Codex 或 Claude Code 里,其实只需要给一个比较自然的任务:
Use GDB MCP to debug /tmp/gdb-mcp-hello.
Set a breakpoint at add, run, show the current location, backtrace, locals,
then continue once.
对应到 MCP tool flow,大致是:
gdb_create_session加载/tmp/gdb-mcp-hellogdb_set_breakpoint在add下断点gdb_run_and_context运行到断点并返回现场gdb_continue_and_context继续运行到程序退出gdb_close_session关闭 session
关键是第三步。传统 GDB wrapper 可能只会告诉 agent “程序停了”,然后 agent 还需要继续问当前 frame、backtrace、locals。gdb_run_and_context 会把这些信息一起返回,结果类似:
action: run
stop: breakpoint-hit
location: add at examples/hello.c:3
backtrace: #0 add:3 <- #1 main:8
locals: a=2, b=40
这说明 agent 已经知道程序停在 add,参数 a 和 b 分别是 2、40,调用者是 main。如果继续执行,gdb_continue_and_context 会返回程序输出:
value=42
Context Tool #
早期只做基础 GDB wrapper 的时候,我很快发现一个问题:agent 调试程序时,常常会重复发这样的工具序列:
- run / continue / step
- current location
- backtrace
- locals
- 根据结果决定下一步
如果每一次执行控制都要再查三次状态,成本高,且上下文容易碎。所以现在提供了一组 *_and_context 工具:
gdb_run_and_contextgdb_continue_and_contextgdb_step_and_contextgdb_next_and_contextgdb_reverse_continue_and_contextgdb_reverse_step_and_contextgdb_reverse_next_and_contextgdb_reverse_finish_and_context
这些工具在执行后会自动收集:
- 当前 frame
- backtrace
- locals
- stop reason
- target output
- 一段 compact summary
例如一个典型结果会包含类似这样的 summary:
action: run
stop: breakpoint-hit
location: add at /tmp/hello.c:3
backtrace: #0 add:3 <- #1 main:8
locals: a=2, b=40
这其实是给 agent 的“最小可用现场”。它不需要立刻看到所有 MI 原始结果,但如果确实需要,也可以传 include_raw=true 拿完整 payload。
我的理解是,这是 GDB MCP 和普通 shell GDB 最大的差别之一:它不只是让 agent 能“调用 GDB”,而是把调试循环中最常用的观察结果整理成适合模型消费的形态。
工具分层 #
目前工具大致分成几类。
Session 管理 #
包括:
gdb_create_sessiongdb_attachgdb_load_coregdb_connect_gdbservergdb_launch_gdbservergdb_list_sessionsgdb_statusgdb_close_session
这里比较在意的是 attach、core 和 gdbserver。因为真实逆向/漏洞分析里,很多目标并不是简单 gdb ./a.out 就能覆盖:
- fuzzing 产生 core,需要加载 core 还原现场
- 服务进程已经在运行,需要 attach
- 目标跑在容器、虚拟机、远程环境里,需要 gdbserver
- 某些场景希望
gdbserver :0自动选择端口,再由 MCP 解析实际端口连接
gdb_launch_gdbserver 会启动本地 gdbserver、解析 banner 里的端口、创建 GDB session 并连接上去。如果连接失败,会清理刚创建的 GDB 和 gdbserver,避免留下僵尸进程。
执行控制 #
包括 run、continue、interrupt、signal、step、next、detach、kill,以及 reverse debugging 相关工具。
Reverse debugging 对自动化分析挺有意义。漏洞触发后,agent 经常知道“这里坏了”,但不知道“什么时候开始坏的”。如果目标和 GDB 支持 process record,就可以从崩溃点往回走:
gdb_start_recordinggdb_reverse_continuegdb_reverse_stepgdb_reverse_nextgdb_reverse_finish
这类能力手动用起来已经很有价值,放到 agent workflow 里会更有想象空间。
观察与分析 #
常用的包括:
gdb_contextgdb_backtracegdb_localsgdb_eval_expressiongdb_registersgdb_read_memorygdb_read_c_stringgdb_disassemblegdb_disassemble_current_framegdb_sourcegdb_memory_mappingsgdb_shared_librariesgdb_info_files
这些工具的原则是:能用更窄、更语义化的接口解决,就不要让 agent 自己拼 GDB 命令。例如读字符串就给 gdb_read_c_string,读内存就给 gdb_read_memory,看当前 PC 附近反汇编就给 gdb_disassemble_current_frame。
这会牺牲一部分 GDB 的自由度,但换来的是更稳定的自动化路径。
安全边界 #
调试器天然危险。
GDB 可以 attach 到进程,可以读内存,可以改变量,可以写内存,可以调用 inferior 函数,甚至可以通过 CLI 执行 shell 命令。把这种能力交给 agent,如果没有边界,风险很高。
所以 gdb-mcp 默认区分几类能力:
- Read:读取调试状态或目标状态
- Execution:运行、继续、中断、attach、detach、kill
- Mutation:修改调试器状态,例如断点、frame、路径
- Unsafe:可能执行目标代码或修改 inferior 状态
默认情况下,以下能力是关掉的:
gdb_executegdb_call_functiongdb_set_variablegdb_write_memorygdb_breakpoint_commands
必须显式使用:
gdb-mcp --unsafe
或者:
GDB_MCP_ALLOW_UNSAFE=1 gdb-mcp
此外,安全模式下的表达式求值也做了保守过滤。比如 gdb_eval_expression、gdb_print、gdb_set_watchpoint 会拒绝明显的函数调用、赋值、自增自减和控制字符。这个过滤不是 sandbox,不能当作对恶意输入的完整隔离,但它能防止 agent 在普通“读一下变量”的任务中顺手执行了 puts() 或改掉目标状态。
Lazy Proxy #
0.3.0 里一个比较重要的变化是默认入口变成 lazy stdio proxy。
MCP client 启动时一般会枚举 server 的工具。如果 server 一启动就拉起完整 GDB backend,体验会比较差:用户可能只是打开了一个 Codex 会话,并不一定真的要调试程序,但这时后端已经启动、依赖已经检查、进程也挂着了。
现在的默认 gdb-mcp 命令只做一件事:立刻向 MCP client 暴露完整 tool schema,但真正的 backend 等到第一次调用 gdb_* 工具时才启动。
这样有几个好处:
- MCP client 启动更快
- 不调试时没有多余 GDB 后端进程
- 仍然保留完整工具发现能力
- 需要时可以切到独立 backend 或 HTTP backend
如果想显式运行后端,也可以用:
gdb-mcp-backend --transport streamable-http --host 127.0.0.1 --port 8000
GDB_MCP_BACKEND_URL=http://127.0.0.1:8000/mcp gdb-mcp
HTTP transport 默认也应该只绑定 loopback。调试器服务不应该裸露到不可信网络里。
目前的状态 #
截至现在,gdb-mcp 已经具备了我最开始想要的核心形态:
- agent 可以创建多个隔离的 GDB session
- 可以调试本地程序、attach 进程、加载 core、连接或启动 gdbserver
- 可以用 context tool 低成本获得当前位置、栈和局部变量
- 可以做常见的断点、watchpoint、寄存器、内存、反汇编、source 查看
- 可以在显式 unsafe 模式下进行更强的修改和调用
- 可以通过 lazy proxy 适配 Codex / Claude Code 这类 MCP client 的启动模型
当然它还远不是终点。后面我比较想继续做的方向包括:
- 更好的 crash triage workflow,比如从 core 自动提取崩溃点、输入、调用链和关键内存
- 更适合漏洞挖掘的 heap / allocator 辅助工具(pwndbg?)
- 对常见 sanitizer、fuzzer 输出的整合
- 更强的远程调试和容器隔离方案
- 针对 agent 的调试策略 prompt / workflow 示例
- 更细粒度的权限模型,而不是只有 safe / unsafe 两档
小结 #
我对这个项目的判断是:agent 做自动化逆向和漏洞挖掘,需要稳定的动态调试接口。
纯静态分析当然有价值,但很多关键事实只存在于运行时。以前人类逆向工程师会通过 GDB 把这些事实一点点挖出来;如果 agent 要参与这个过程,它就需要一个能保持会话、能结构化观察、能控制风险、能压缩上下文的 GDB 接口。
gdb-mcp 目前就是朝这个方向做的一版实现。它还不复杂,也不完美,但已经能支撑实际的 agent 调试循环了。
后面应该会继续沿着“少一点裸命令,多一点面向分析任务的工具”这个方向迭代。