Repository navigation
Unable to set breakpoint or pause on a running application with extended-remote connection debug #7719
Description
Bug type: Debugger
Describe the bug
- OS and Version: Ubuntu 20.04
- VS Code Version: 1.56.2
- C/C++ Extension Version: v1.4.1
- Other extensions you installed (and if the issue persists after disabling them):
- A clear and concise description of what the bug is.
Unable to set breakpoint or pause on a running application with extended-remote connection debug
To Reproduce
Please include a code sample and launch.json configuration.
Source code of hello:
`#include <stdio.h>
#include <unistd.h>
int a = 10000;
int main(){
int i = 0;
while(i < a){
i++;
printf("test \n");
sleep(2);
}
printf("test \n");
}`
launch.json:
` "configurations": [
{
"name": "gcc - Build and debug active file",
"type": "cppdbg",
"request": "launch",
"program": "/folk/bfu/log/hello",
"args": [],
"stopAtEntry": false,
"cwd": ".",
"environment": [],
"debugServerPath": "",
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"text": "target extended-remote 192.168.1.20:12345"
},
{
"text": "set remote exec-file /folk/bfu/log/hello"
},
{
"text": "set logging file /folk/bfu/log/gdbClient.log"
},
{
"text": "set logging on"
},
{
"text": "-enable-pretty-printing"
}
],
"miDebuggerPath": "/usr/bin/gdb",
"logging": {
"trace": true,
"engineLogging": true,
"traceResponse": true
}
}`
Steps to reproduce the behavior:
- Build source code to file "hello", and put it on host 192.168.1.20 which run gdb server;
- on host 192.168.1.20, run gdb server "gdbserver --multi --remote-debug --debug-file=gdbServer.log :12345"
- run hello in VSC using launch configuration in above;
- Try to set breakpoint on the line "printf(**)", it failed and the breakpoint icon is gray
<-- C (setBreakpoints-8): {"command":"setBreakpoints","arguments":{"source":{"name":"hello.c","path":"/buildarea/bfu/devcode/testFolder/cPrj/hello.c"},"lines":[8],"breakpoints":[{"line":8}],"sourceModified":false},"type":"request","seq":8}
--> E (output): {"type":"event","event":"output","body":{"category":"console","output":"1: (7935) Send Event AD7BreakpointErrorEvent\n"},"seq":374}
1: (7935) Send Event AD7BreakpointErrorEvent
--> R (setBreakpoints-8): {"type":"response","request_seq":8,"success":true,"command":"setBreakpoints","body":{"breakpoints":[{"id":1,"verified":true,"line":8}]},"seq":376}
--> E (breakpoint): {"type":"event","event":"breakpoint","body":{"reason":"changed","breakpoint":{"id":1,"verified":false,"message":"Attempting to bind the breakpoint....","line":8}},"seq":378} - Click pause in debug menu:
<-- C (pause-9): {"command":"pause","arguments":{"threadId":1},"type":"request","seq":9}
--> R (pause-9): {"type":"response","request_seq":9,"success":true,"command":"pause","body":{},"seq":381}
- From the log in gdbserver, set BP or pause the application, gdbserver can't received any information from gdb client.
I also try to parse the message using wireshark, gdbserver doesn't receive any message about the action set BP or pause.
-
#Cannot pause debugger on Linux #807 , check this similar bug, it's different as vsc has get response from gdbserver, in my case no response.
-
If set breakpoint first then run the application, it works well and it can hit the breakpoint.
-
It works well if run gdbserver on localhost like "target extended-remote 127.0.0.1:12345", VSC received signal "SIGTRAP" first when set BP.
<-- C (setBreakpoints-8): {"command":"setBreakpoints","arguments":{"source":{"name":"hello.c","path":"/buildarea/bfu/devcode/testFolder/cPrj/hello.c"},"lines":[8],"breakpoints":[{"line":8}],"sourceModified":false},"type":"request","seq":8} --> E (output): {"type":"event","event":"output","body":{"category":"console","output":"1: (16377) ->~\"\\nProgram\"\n"},"seq":374} 1: (16377) ->~"\nProgram" --> E (output): {"type":"event","event":"output","body":{"category":"stdout","output":"\nProgram"},"seq":376} Program --> E (output): {"type":"event","event":"output","body":{"category":"console","output":"1: (16377) ->~\" received signal SIGTRAP, Trace/breakpoint trap.\\n\"\n"},"seq":377} 1: (16377) ->~" received signal SIGTRAP, Trace/breakpoint trap.\n" --> E (output): {"type":"event","event":"output","body":{"category":"stdout","output":" received signal SIGTRAP, Trace/breakpoint trap.\n"},"seq":380} received signal SIGTRAP, Trace/breakpoint trap.
Activity
fbs2016 commentedon Jun 18, 2021
gdbServer.log
debugConsoleLog.txt
gdbClient.log
WardenGnaw commentedon Jun 21, 2021
Thank you for reporting this issue. We’ll let you know if we need more information to investigate it.
fbs2016 commentedon Aug 27, 2021
Andrew Wang (@WardenGnaw)
Have any progress? Or is there any guide to setup dev environment to debug it by myself?
BillPlunkett commentedon Sep 1, 2021
I had similar problems until I set MIDebuggerServerAddress to the ip address:port of the gdb server
haneefdm commentedon Sep 1, 2021
fbs2016, I saw your commend on #844. I am not sure what your question is. I will try to help see if there is a better way. I know a lot more about gdb-servers than I did two years ago so, maybe there is a better way in general to handle remote gdb servers.
But, as things stand now, what BillPlunkett is saying is probably right. If your gdb-server is truly running on a remote machine, then you probably do need to use
MIDebuggerServerAddressIf that does not work, let me know and I can look into it.
fbs2016 commentedon Sep 3, 2021
BillPlunkett Haneef Mohammed (@haneefdm) Thanks.
Firstly, we launch gdbserver with multi mode "gdbserver --multi ...", the address set in MIDebuggerServerAddress can't support multi mode connection. So I configure setupCommands ""text": "target extended-remote 192.168.1.20:12345"".
Secondly, I put the out file to target using setupCommands "remote put *out" and other commands which depends on remote connection.
In cppdbg launch, if you configure MIDebuggerServerAddress and setupCommands, it run setupCommands first then connect to MIDebuggerServerAddress . Then "remote put" will failed as the connection doesn't established.
Do you have any advice to configure it to support this scenario? Thanks in advance.
BillPlunkett commentedon Sep 3, 2021
fbs2016 commentedon Sep 6, 2021
BillPlunkett
Thanks a lot. It works.
It confused me that "MIDebuggerServerAddress" can work with gdbserver multi mode after replacing "setupCommands" with"customLaunchSetupCommands".
Form the debug console, I just see the connection log of command "target extended-remote xxx" but no log about "MIDebuggerServerAddress", I try to remove the the option "MIDebuggerServerAddress" but it failed.
And another different is that the option "targetArchitecture" must be set, otherwise the launch report err. But it can work before apply your advice as vsc set it as x86 auto.
Thanks
github-actions commentedon Mar 6, 2022
This issue has been closed automatically because it has not had recent activity.
lingr7 commentedon Sep 1, 2026
Source-Protected Remote Debugging with cppdbg: Principles & Key Findings
文中所有 MIEngine 行号引用基于本地检出一份主干源码核实(
src/MICore/Debugger.cs、src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs、src/MICore/LaunchOptions.cs、src/MICore/Transports/ServerTransport.cs等),不同版本可能略有偏移,以符号名为准。0. 一句话总览
把
debugServerPath当作「让 MIEngine 把会话判成远程 + 在 MI 通道之外旁路传递动态端口」的杠杆,加上一个由该 debugServer 进程在 B 侧拉起gdbserver --multi localhost:<port>并经ssh -N -L反向建隧道的常驻管理器,即可在「A 持源码与符号、B 仅持 stripped 二进制」的前提下,获得 F6 暂停、运行中增删断点、源码定位全可用的远程调试体验——且 launch.json 保持静态、无需写死端口。整条链路没有任何黑魔法,全是 MIEngine 已有能力与 GDB/MI + RSP 协议既有语义的组合。下面逐条拆解,并标注每一处「为什么」对应上游 issue 的哪个根因。
1. 问题溯源:为什么直接用
setupCommands连target extended-remote会失效这是 #7719 / #833 / MIEngine#844 的共同根因,也是本方案要解决的核心痛点。
1.1 MIEngine 用 launch.json 字段——而非 gdb 命令——判定 local/remote
MIEngine 的
IsLocalGdbTarget()/IsRemoteGdbTarget()判定依据是 launch options 的类型与字段,不是info target的输出,也不是你在 setupCommands 里写了target extended-remote:关键:当 launch.json 既没填
miDebuggerServerAddress、也没填debugServerPath,但你在setupCommands里手写了target extended-remote host:port时——MIEngine 看不到这条命令的语义,它只认字段。于是IsLocalGdbTarget()==true,会话被当成「本地」。1.2 误判为 local 的灾难性后果:暂停 = 给 inferior pid 发
kill -5CmdBreakInternal()(Debugger.cs~L713)的分支:CmdBreakUnix→UnixUtilities.Interrupt(pid)→Kill(pid, 5)(UnixUtilities.csL254-263),即对外inferior 的 pid 跑kill -5(SIGTRAP)。当 inferior 真在本地,SIGTRAP 经终端进程组投递,恰好等价于 Ctrl-C,意外地能用。但一旦 inferior 在远端 B:
CmdBreakUnix分支,对「它以为在本地的 inferior pid」发kill -5;这就是 #7719 / #833 / #12890 的机制级根因:不是 gdb/gdbserver 的 bug,是 MIEngine 的分类器在「字段缺失 + setupCommands 手连」这一组合下的必然误判。haneefdm 在 #844 提议用 gdb 启动后
info target的mode == "connected"来纠正,gregg-miskelly 回复「更干净的做法是让IsLocalGdb在这些场景返回 false」——而社区侧无需改 MIEngine 即可达成该效果的办法,就是填上让它判 remote 的字段。1.3 两条让 MIEngine 判 remote 的字段
只要 launch.json 满足任一,
IsLocalLaunchUsingServer()==true→IsLocalGdbTarget()==false,后续暂停/断点走-exec-interrupt(对所有 remote target 普遍可靠):miDebuggerServerAddress: "host:port"—— 经典做法(Unable to set breakpoint or pause on a running application with extended-remote connection debug #7719 中 BillPlunkett 给出的可工作配置即此)。但它是静态地址端口,且 MIEngine 会在 setupCommands 之后自己发-target-select remote/extended-remote(见DebuggedProcess.cs~L836-840),与--multi+ 需要remote put/set remote exec-file先于连接的场景时序冲突(Unable to set breakpoint or pause on a running application with extended-remote connection debug #7719 报告者的困惑正在于此)。debugServerPath+serverStarted—— MIEngine 先拉起你指定的 debugServer 进程,等其 stdout 匹配serverStarted正则后才放行 setupCommands(含你手写的target extended-remote)。时序由你掌控,且能传递动态端口。本方案选这一条。2. 整体架构(语言无关)
四个角色的职责边界是语言无关的设计要点:
debugServerPath字段)localhost:不是:Bport)3. 关键原理逐条(每条都标注「为什么」与「语言无关的实现要点」)
3.1 原理一:
debugServerPath+serverStarted是「判 remote + 传动态端口」的干净杠杆机制(MIEngine 源码核实):
LaunchOptions.csInitializeServerOptions(~L480-494):当debugServerPath非空,记下DebugServer/DebugServerArgs/ServerStarted/FilterStdout/FilterStderr,且若 filterStderr 与 filterStdout 都没显式设,默认FilterStdout=true(L489-492)。LaunchOptions.csShouldStartServer()(L530-533):DebugServer非空即启 server 模式。DebuggedProcess.cs~L193-201:ShouldStartServer()为真时构造ClientServerTransport(LocalTransport, ServerTransport(killOnClose:true, filterStdout, filterStderr))。注意:MI 通道是 client transport(你的本地 gdb),ServerTransport 仅用于拉起/监控 debugServer 进程,二者解耦。ServerTransport.csL29-42InitStreams:用DebugServer当 FileName、DebugServerArgs当 Arguments 起进程,_startPattern = ServerStarted。ServerTransport.csL44-54FilterLine:逐行匹配_startPattern正则,首次匹配置_started=true并StartedEvent.Set();之后始终返回 null(不喂给 MI 解析)。ClientServerTransport.csL28-46Init→WaitForStart():StartedEvent.WaitOne(launchTimeout),超时抛Error_DebugServerInitializationFailed。这是 MIEngine 等 serverStarted 再放行 setupCommands 的同步点。为什么这是干净路径:
debugServerPath非空 →IsLocalLaunchUsingServer()==true→IsLocalGdbTarget()==false(§1.1)→ 暂停/断点走-exec-interrupt,绕开kill -5误杀(§1.2)。直接修掉 Unable to set breakpoint or pause on a running application with extended-remote connection debug #7719/installation issue #844/Unable to pause, step button while using gdb connect to remote gdbserver #12890 的根因,无需改 MIEngine。.gdb_remote_connect.gdb(target extended-remote 127.0.0.1:<动态端口>),setupCommands 末尾source该文件。launch.json 因此保持静态、无端口——避开了 「preLaunchTask 改 launch.json 不生效」「${command:}/${env:}注入 port 皆不可行」的 VSCode 限制。serverStarted匹配后才跑 setupCommands,此时连接 .gdb 已就绪,source不会落空。语言无关实现要点:
-L→ 写连接文件 → 往 stdout 打一行 READY marker(flush确保立即输出)→ 常驻到会话结束。FilterLine在匹配后仍把每一行经LogText转发到 Debug Console(见 §3.7),多打噪声会污染调试控制台,但不影响 serverStarted 判定(那是个 latch)。killOnClose整树回收(会话结束 MIEngine 杀 debugServer 进程树)——你的实现里 ssh 隧道、远端 gdbserver 都得挂在该进程树下,或由该进程的信号处理统一清理。3.2 原理二:隧道就绪判定只看 LISTEN,绝不在 gdb 连接前 connect()
这是最容易踩的坑,且与语言完全无关。
gdbserver --multi localhost:<port>这一族版本有一个行为:在尚未用 vRun 启动 inferior 时,若收到一个「连上但不发任何 RSP 包即关闭」的连接,会直接退出。于是留给真正 gdbtarget extended-remote的就是Connection refused。表现(来自实测 + 上游 issue 共有现象):
Remote communication error. Target disconnected.: Connection reset by peer;channel 2: open failed: connect failed: Connection refused;Remote debugging from host ...,之后再无连接。根因: 你在「隧道就绪」判定里用了
socket.create_connection(localhost, port)主动 connect 探测。该 connect 经ssh -L打穿到 B 侧 gdbserver,触发了上述「空连接即退出」。正确做法(语言无关):
-L本地转发端口是否进入 LISTEN 状态」。Linux 上ss -tln列内核监听表,只读、不发起连接。ss不可用 → 假阴性 → 重试(由超时 + ssh 死亡检测兜底)。kill -0 $pid(包 try/except),不要连 RSP 端口。3.3 原理三:远端常驻守护进程的后台化写法(exec + 子 shell 重定向)
经 ssh 远程启动一个需常驻的
gdbserver --multi,写法不对会 sshTimeoutExpired永不返回。错误与正确对照:要点(语言无关):
( )子 shell 上,内部用exec替换——这样后台化的进程不再持有 sshd 会话管道,sshd 拿到 EOF,ssh 正常返回。exec后$!即 gdbserver 真实 pid(不是中间 nohup/sh 的 pid),供后续kill -0探活、会话结束时kill清理。/dev/null——否则 gdbserver 可能继承 ssh 的 stdin,干扰隧道。gdbserver --multi localhost:PORT用localhost:(只绑 loopback),不要用:PORT(绑 INADDR_ANY,B 机其他用户可连执行任意代码)。3.4 原理四:
set remote exec-file是远端 inferior stdio 的唯一有效杠杆cppdbg 下,inferior 跑在 B、gdb 在 A。你无法用常规手段重定向 inferior 的 stdio:
run > file、set args >、set inferior-tty:对 remote inferior 实测全部无效(remote inferior 的 stdio 由 gdbserver 侧 fork+exec 决定,gdb 端的 tty 设置不跨 RSP 生效)。set remote exec-file指向一个 wrapper 脚本(而非 inferior 本体),由 wrapper 在 B 侧重定向 inferior 的 stdio(接到 pty / 写日志文件 / 接独立 TCP 端口皆可)。-exec-run经 vRun 发到 gdbserver,gdbserver exec 的就是这个 wrapper。这条原理解决两个独立问题:
cd <deploy>+export LD_LIBRARY_PATH=<deploy>:<deploy>/tool/lib,gdbserver 继承给 inferior,替代旧 stdio-pipe 模式里内嵌在 ssh 命令里的环境注入。3.5 原理五:符号/源码定位——
solib-search-path在 F5 下必被 MIEngine 覆盖这条对「源码保护但要在 A 上看源码栈」至关重要,且是个反直觉的覆盖行为。
MIEngine 源码核实的执行顺序(
DebuggedProcess.csGetInitializeCommands~L617-663):GetSOLibSearchPath()→GetSOLibSearchPathCandidates()(LaunchOptions.csL1832-1876)仅两个来源:dirname(program)(L1845-1857);additionalSOLibSearchPath字段,分号分隔(L1860-1875)。结论: 你在 source 脚本里、或 setupCommands 里写的
set solib-search-path,在 F5(经 MIEngine)路径下必然被 L656 那条覆盖。这是 #11949 等报告「symbols not loaded」的一类根因——多产物项目(exe 在out/,关键 .so 在out/tool/lib/)只设了脚本里的solib-search-path,MIEngine 覆盖后只剩<exe-dir>:,子目录里的 .so 符号挂不上,栈帧??。正确做法(语言无关):
additionalSOLibSearchPath字段喂给 MIEngine(分号分隔,Windows 转;、Unix 用:由 MIEngine 处理)。多目录项目要列全:${workspaceFolder}/out;${workspaceFolder}/out/tool/lib。set solib-search-path,两条路径并存、同源(都源自同一份项目清单solib_dirs),不可互相替代。solib-search-path不下钻子目录——只按 solib basename 在每条路径下查。所以必须显式列出子目录。add-symbol-file——不带加载地址时符号停在链接期地址,PIE 下断点永不命中。3.6 原理六:inferior 输出转发的语义陷阱与 pty 桥接
3.6.1 MIEngine 的输出语义(源码核实,决定你该怎么设计转发)
cppdbg 下 inferior 的 stdout/stderr 怎么进 Debug Console?
ServerTransport.FilterLine(L44-54)逐行处理后:LogText(prefix + ": " + line)(L52);LogText→OutputStringEvent→ DAPOutputEvent,category =stdout(StdOut 默认开启)。即:
filterStdout: true并不是「隐藏 debugServer stdout」,而是「把该 stdout 不当 MI 协议解析、转而以stdoutcategory 打到 Debug Console」。所以你的 debugServer 进程(隧道管理器)往 stdout 打的每一行都会混进 Debug Console——这就是为什么 READY marker「只打一行」很重要,且隧道管理器不应把 inferior 输出往自己 stdout 倒(会与 MI 噪声混在一起)。3.6.2 inferior 输出根本到不了 A(TCP 隧道模式)
F5 TCP 隧道下,B 上 gdbserver 的 stdio 被重定向到
gdbserver.log,inferior 继承之;MIEngine 的 OutputEvent 只能进 Debug Console 且与 MI 噪声混合。filterStdout只是「不当 MI 解析」并非「分流/隐藏」。要拿到「独立终端实时显示 + stdin 可交互」必须在 gdbserver 侧(B 侧)解决,不能靠 MIEngine 开关。3.6.3 唯一杠杆是 wrapper + pty 桥接(§3.4 的延伸)
set remote exec-file指向 wrapper,wrapper 把 inferior stdio 接到 B 侧一个 pty;一个本地(B 侧)桥接进程把 pty master 的数据经第二条ssh -L隧道回 A,A 侧一个独立终端任务(console子命令)连本地转发端口,在 VSCode 集成终端实时显示 + 透传 stdin。pty 桥接必须满足三语义(语言无关,socat 等简单桥接实测做不到):
write();简单 TCP 桥接无连接时不 drain 即挂死。主循环无条件读 master,无 client 时丢弃,inferior 永不因「没人看」阻塞。EIO或返回b"";两分支都只continue,等下一次 vRun 重新打开 slave,绝不退出桥接。slave 保持
openpty默认(canonical + ECHO + ISIG):远端回显、按行交付、Ctrl-C 经 ISIG 转 SIGINT 给 inferior(与真实终端一致)。A 侧本地终端 stdin 才 setraw 透传字节。3.6.4 回退
桥接不可用 /
console连不上 / CLI stdio-pipe 模式(无第二条隧道)→console回退ssh tail -F <deploy>/console.log单向显示;wrapper 见 pty 不存在走console.log回退。无console任务时桥接仍 drain 丢弃,inferior 不阻塞(输出丢失,pty 模式下 console.log 不写)。3.7 原理七:abort 抢救——远程调试特有的 stdout 丢失
远程调试下 inferior 的 stdout 不是 TTY(gdbserver 经 RSP
Opacket 转发,或经 §3.6 的 pty)。glibc 对非 TTY 的 stdout 默认全缓冲(4KB block),abort()(glogCHECK/LOG(FATAL)/assert→ SIGABRT)由信号直接杀进程,stdout 用户态 buffer 里未 flush 的内容丢失。stderr 无缓冲,故 glog FATAL stacktrace 不丢;丢的是程序自身printf走 stdout 的部分。抢救(语言无关,注入到连接 .gdb 末尾):
break abort:在abort()入口、信号未 raise 前拦下,栈完整直指 CHECK 现场,call (void)fflush(0)稳定抢救缓冲。set breakpoint pending on让 libc 加载后自动落位(脚本生成时 inferior 未启动、abort符号未定义)。写fflush(0)不写fflush(NULL)——NULL是宏不是符号,GDB 表达式不识别。catch signal SIGABRT:拦直接raise(SIGABRT)绕过abort()的路径;此时栈已进信号投递路径,call可能失败但不影响其余流程。handle SIGABRT stop nopass对抗 cpptools(cppdbg) 默认的nostop——否则信号直接递交 inferior 杀进程、GDB 不停、catchpoint 永不触发(F5 命中 abort 直接退出的根因)。bt看现场后手动continue。彻底治本(不依赖 catchpoint,所有场景受益): 在 exe 的 main 入口
setvbuf(stdout, NULL, _IOLBF, 0); setvbuf(stderr, NULL, _IOLBF, 0);(行缓冲)。注意setvbuf操作的是进程级全局FILE*——即便调用点在.so,对整个 inferior 的 stdout/stderr 同样生效;exe 上nm|grep setvbuf为空不代表没生效(调用编进了 .so)。这是常见误判。3.7.1 A/B 系统库镜像(
break abort的远程前提)远程调试时 GDB 的 solib 符号来自 A 本地文件(
set sysroot /即 A 的/lib/...)。A/B 发行版不同(glibc 版本差异)时 build-id 不符,GDB 地址校验失败拒读符号 →break abort永不解析、call fflush报 no symbol、bt 里 libc 帧??。解法(语言无关):部署时把
ldd列出的 B 系统库镜像到 A 的.debug/b-sysroot/(按 B 上绝对路径布局,md5 缓存增量拉取,B 无需装 binutils),连接脚本里set sysroot <镜像>覆盖缺省/;项目自有 .so 不镜像(仍由 solib-search-path 命中本地未 strip 副本,DWARF 不丢)。GDB 对不存在的 sysroot 目录不回退原始路径,故镜像未生成时保持/。3.7.2 排查第一动作:B 上纯跑
调试会话「零输出」≠ 程序无输出。在
--multi+ vRun + abort 抢救块场景,inferior 的 stderr(glog FATAL)常不回显到 UI,造成「无声 abort」假象。第一动作:ssh 到 B,在部署目录里按 wrapper 同款LD_LIBRARY_PATH与 cwd 不挂 gdb 纯跑同一命令,拿退出码与 stdout/stderr:EXIT=134(128+SIGABRT)即确认 abort——问题在程序本身/环境,与 gdb/缓冲/链路无关;看 stderr 的 glog stacktrace 直接拿 CHECK 现场。EXIT=0且有正常输出 → 仅 gdb 下复现,查 fork 子进程时序、抢救块continue放行、inferior-tty 转发等调试器侧因素。3.8 原理八:带版本号 SONAME 库的部署命名
带版本号的库(如
libFoo)本地是 symlink 链:ld.so按 SONAME(libFoo.so.1)查找,本地靠 symlink 解析。若按真实文件名libFoo.so.1.0部署到 B,B 上既无.so.1文件也无 symlink →ld.sonot found,程序退出。其他库(真实文件名 == SONAME)不受影响,唯独带版本号的库中招。解法(语言无关): 远端文件名一律取 SONAME(
readelf -d <so> | grep SONAME得到),与 staging 命名一致,保证 B 上文件名 == SONAME;带版本号库无需在 B 上重建 symlink 链。排障:B 上LD_LIBRARY_PATH=<deploy> ldd <exe> | grep 'not found',缺的库readelf -d确认文件名是否 == SONAME。3.9 原理九:孤儿清理的判活规则
gdbserver --multi断连后驻留等待新连接(设计与生俱来,不是 bug)。异常退出会堆积孤儿。回环调试(A==B)尤甚。判活规则(语言无关,不依赖「归属」):
--multi localhost:PORT的 gdbserver:看端口是否有 ESTABLISHED 连接(无连接 = 孤儿)。注意ss -tn state established在某些版本是 4 列布局(无 State 列),按列下标判 ESTABLISHED 会全漏 → 活跃会话误判孤儿 → 误杀。用正则127.0.0.1:{port}\b整行扫。gdbserver [-] <exe>:看 PPid==1(init 收养 = 原启动它的 ssh 管道已退出)为孤儿,PPid!=1 保留。日常清理按归属(只清自己,不误杀他人);治脚本自身残留用
--all按判活清全量孤儿。这条对所有实现通用:清理要么按部署目录锚定(归属),要么按端口/PPid 判活,二者不要混。3.10 原理十:配置的单一事实来源
整条链路有两类事实,必须各有唯一来源、且工具只读消费:
rdbg.json,JSONC,容忍//注释与尾逗号)。_前缀字段(VSCode 不识别会忽略,不报错不弹窗),工具只读。字段级优先级:命令行参数 > 清单/launch.json > 内置默认。CLI 与 F5 共用同一份 launch.json。切换调试对象 = 改
program路径末段 exe 名(F5,set remote exec-file用此 basename)/--target(CLI)。4. 时序契约(语言无关,实现任何语言版本都要满足)
红线(违反任一会出问题):
additionalSOLibSearchPath,不能只靠脚本(§3.5)。localhost:不绑:port(§3.3)。5. 与上游 issue 的对应关系
debugServerPath让 MIEngine 判 remote,走-exec-interruptIsLocalGdbTarget()==false,不进CmdBreakUnix/kill -5分支additionalSOLibSearchPath列全子目录set mi-async on(每次会话 MIEngine 自设),不改 gdb 异步档位给社区的可以这样讲: 你不需要等 MIEngine 修
IsLocalGdb*的分类器(#844 提议的info target纠正至今未合)——debugServerPath+serverStarted是 MIEngine 既有能力的合法用法,填上即等效「让IsLocalGdbTarget返回 false」,从分类层根除kill -5误杀与-exec-interrupt不发的问题。代价只是一个你自己掌控生命周期的 debugServer 进程(隧道管理器),且换来了动态端口与静态 launch.json 两个额外好处。6. 迁移到其他语言/项目的清单
实现语言无关,拷贝时只需替换脚本语言:
-L→ 轮询 LISTEN → 生成连接 .gdb → 打 READY marker → 常驻/清理。任何能起子进程、读ss/ss -tln、写文件的运行时都能实现。_字段,工具只读。set sysroot/set solib-search-path(CLI 用) /set substitute-path/set remote exec-file <wrapper>/target extended-remote 127.0.0.1:<动态端口>/ abort 抢救块。debugServerPath指隧道管理器、serverStarted指 READY 正则、filterStdout: true、additionalSOLibSearchPath列全 solib 目录、setupCommands末尾source连接 .gdb。7. 术语速查(让其他语言实现者对齐)
--multiset remote exec-file指向的目标)Opacketsolib-search-path按 basename 查符号DT_SONAME,ld.so按此查找OutputEvent的落点本文档基于本仓库
apps/scripts/remote_debug/的实现(Python)整理,但所述原理与语言解耦。若你用 Node.js / Rust / Go 重新实现,只需遵循 §3 各条的机制契约与时序红线即可。欢迎在对应上游 issue 评论区补充指正。lingr7 commentedon Sep 3, 2026
上面的方案,pty双向交互是一个不应该支持的功能,也无法支持,会导致wrapper和实际执行的程序之间有断点,导致静态库编译进可执行程序本身的场景无法下断点,
用 vscode(F5)远程调试程序 A 时,入口函数 main 的断点不命中,程序体内任何一行断点都失效——下断点后状态一直停在 pending,程序直接跑飞。而用同样的方式调试程序 B 时,同样位置的断点完全正常。改成命令行(CLI)方式调试时,A、B 都正常。
根因(有调试器日志取证,非推测)
在早期的远程桥接方案里,调试器连接脚本把"远程待执行文件"指向了一个包装脚本(一个 #!/bin/sh 的 shell 脚本)。远程调试服务在拉起目标程序时,是 fork 后 exec 这个"待执行文件"——而它是个脚本,内核会先启动 /bin/sh(在常见发行版上即 dash)来解释脚本。
于是第一个进程其实是 shell 解释器,调试器把它当成了被调试程序的主可执行文件,去读它的符号表(自然读不到业务符号)。随后脚本内部再 exec 切换到真正的目标程序,但此时调试器已经报过"无法为目标程序加载符号"——主模块的符号此后再没有被重新绑定到正在运行的进程上。
后果:所有落在主模块上的断点(入口函数、以及被静态链接进主模块的业务代码行)在重设时报"函数未定义",全部转为 pending、永不命中。
▎ 现方案已废弃包装脚本:待执行文件直接指向真实目标程序,远程调试服务只用原生机制调整标准库缓冲、不改动 exec 链,此问题不再出现(保留取证记录备查)。
为什么程序 B 不受影响、偏偏程序 A 中招
两者的差异在于业务代码放在哪:
这是"胖可执行文件"与"普通可执行文件"在该链路下的本质差异:胖 exe 的业务行落在主模块,而主模块断点恰好是 exec 切换后无人重绑的那一类。
新方案如下:
机制受限于"GDB over ssh + gdbserver"这个底层模型,存在若干无法在当前架构内消除的本质缺陷:
不支持被调试程序读 stdin。这是被认定为"本质限制"而非待办的缺陷:
CLI(stdio pipe)模式下,gdbserver 的 stdio 被 GDB Remote Serial Protocol 作为传输通道占用,inferior 的 stdin 无从接入;
F5(TCP 隧道)模式下,gdbserver –multi 的 inferior stdout/stderr 可落日志供 console 子命令 tail -F 单向只读,但 RSP 没有 inferior stdin 的反向通道。
旧的 pty/wrapper 双向方案曾支持 stdin,但 set remote exec-file 须指向 wrapper 导致胖 exe断点全挂,已废弃。console.py 与 README 明确注明"RSP 本就无 inferior stdio 通道,stdin 交互无源"。
仅支持 GDB,不支持 LLDB。整条链路(pygdbmi、MIEngine cppdbg、gdbserver、target extended-remote、checks.py 硬检 gdb/objcopy/readelf)全部针对 GDB,MIMode 固定 gdb。
attach 模式受限:CLI 支持 –attach (gdbserver –attach),但 F5/TCP 隧道模式固定 gdbserver –multi,不支持 attach 到已运行进程。
强依赖 ssh ControlMaster 与本地端口转发:硬编码 ControlMaster=auto + ControlPath + ControlPersist=300,隧道用 ssh -N -L。B 侧 sshd 禁用 ControlMaster 或防火墙限制 localhost 转发时机制直接失效;异常退出会留孤儿隧道(有 kill-stale/clean –all 兜底,按判活清理,但不杜绝)。
无符号服务器 / 源码索引集成:符号源依赖 A 本地未 strip 产物 + solib-search-path + substitute-path,无 debuginfod 或源码索引服务器。
A/B 系统库一致性强依赖:A/B glibc 版本不同会导致 build-id 不符、GDB 拒读符号,需镜像 B 系统库到 A 作 sysroot 缓解,但未消除。
单会话语义:一个 F5 会话调试一个 target,不支持一会话内并发调试多进程;多人共享 B 同部署目录仅靠 sid+pid 隔离日志。
非 TTY 下 stdout 全缓冲:glibc 全缓冲导致 abort 丢日志,靠 break abort + fflush(0) 抢救块缓解,属远程调试固有缺陷。
这些缺陷共同源于一个根本取舍:为了保留 set remote exec-file 直指真 exe 的纯净 exec 链(保证断点精度与胖 exe 可调试),放弃了任何 inferior stdio 代理通道。在该模型下,stdin 不可逆地丧失;其余限制则是 GDB/gdbserver + ssh 隧道工具栈的固有边界