Skip to content

Commit 8e432dd

Browse files
authored
Merge pull request #4 from jon-hirst/target-remote
Target --remote
2 parents 2f55a8d + e05bd3e commit 8e432dd

7 files changed

Lines changed: 203 additions & 9 deletions

File tree

skills/gdb-cli/SKILL.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ If gdb-cli is not installed, guide the user: `pip install git+https://github.com
4242

4343
### Step 1: Initialize Debug Session
4444

45+
**For debugging a binary file through a target running on a remote host and port**
46+
```bash
47+
gdb-cli target --binary <binary_path> --remote <host_name>:<port_number> [--gdb-path <gdb_path>]
48+
```
49+
4550
**For core dump analysis:**
4651
```bash
4752
gdb-cli load --binary <binary_path> --core <core_path> [--gdb-path <gdb_path>]

src/gdb_cli/cli.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Usage:
55
gdb-cli load --binary ./my_program --core ./core.1234
66
gdb-cli attach --pid 9876
7+
gdb-cli target --remote 192.168.0.21:3333
78
gdb-cli eval-cmd --session <id> "lock_mgr->buckets[0]"
89
gdb-cli threads --session <id> [--limit 20]
910
gdb-cli bt --session <id> [--thread 12] [--limit 30]
@@ -21,11 +22,12 @@
2122
from . import __version__
2223
from .client import GDBClient, GDBClientError, GDBCommandError
2324
from .i18n import t
24-
from .launcher import GDBLauncherError, launch_attach, launch_core
25+
from .launcher import GDBLauncherError, launch_attach, launch_core, launch_target
2526
from .session import (
2627
cleanup_dead_sessions,
2728
find_session_by_core,
2829
find_session_by_pid,
30+
find_session_by_remote,
2931
get_session,
3032
list_sessions,
3133
)
@@ -181,6 +183,68 @@ def attach(
181183
raise click.exceptions.Exit(1)
182184

183185

186+
@main.command()
187+
@click.option("--remote", "-r", required=True, type=str, help=t("cli.target.remote_help"))
188+
@click.option("--binary", "-b", help=t("cli.target.binary_help"))
189+
@click.option("--scheduler-locking/--no-scheduler-locking", default=True, help=t("cli.target.scheduler_locking_help"))
190+
@click.option("--non-stop/--no-non-stop", default=False, help=t("cli.target.non_stop_help"))
191+
@click.option("--timeout", default=600, help=t("cli.target.timeout_help"))
192+
@click.option("--allow-write", is_flag=True, help=t("cli.target.allow_write_help"))
193+
@click.option("--allow-call", is_flag=True, help=t("cli.target.allow_call_help"))
194+
@click.option("--gdb-path", default="gdb", help=t("cli.load.gdb_path_help"))
195+
def target(
196+
remote: str,
197+
binary: Optional[str],
198+
scheduler_locking: bool,
199+
non_stop: bool,
200+
timeout: int,
201+
allow_write: bool,
202+
allow_call: bool,
203+
gdb_path: str,
204+
) -> None:
205+
"""Connect to remote GDB server"""
206+
existing = find_session_by_remote(remote)
207+
if existing:
208+
print_json({
209+
"session_id": existing.session_id,
210+
"mode": existing.mode,
211+
"remote": existing.remote,
212+
"status": "reused",
213+
"message": "Session already exists for this remote"
214+
})
215+
return
216+
217+
try:
218+
gdb_process = launch_target(
219+
remote=remote,
220+
binary=binary,
221+
scheduler_locking=scheduler_locking,
222+
non_stop=non_stop,
223+
timeout=timeout,
224+
allow_write=allow_write,
225+
allow_call=allow_call,
226+
gdb_path=gdb_path
227+
)
228+
229+
session = gdb_process.session
230+
231+
print_json({
232+
"session_id": session.session_id,
233+
"mode": session.mode,
234+
"remote": session.remote,
235+
"binary": session.binary,
236+
"remote": session.remote,
237+
"sock_path": session.sock_path,
238+
"gdb_pid": gdb_process.pid,
239+
"safety_level": session.safety_level,
240+
"status": "started"
241+
})
242+
243+
except GDBLauncherError as e:
244+
print_error("Failed to connect to target", str(e))
245+
raise click.exceptions.Exit(1)
246+
247+
184248
@main.command()
185249
@click.option("--session", "-s", required=True, help=t("cli.eval_cmd.session_help"))
186250
@click.argument("expr")
@@ -330,7 +394,9 @@ def sessions() -> None:
330394
"mode": s.mode,
331395
"binary": s.binary,
332396
"pid": s.pid,
397+
"remote": s.remote,
333398
"core": s.core,
399+
"gdb_pid": s.gdb_pid,
334400
"started_at": s.started_at,
335401
}
336402
for s in session_list

src/gdb_cli/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def __init__(self, message: str, suggestion: Optional[str] = None, details: Opti
154154

155155
# 连接错误
156156
"socket_not_found": {
157-
"suggestion": "GDB 会话可能已终止,重新运行 'gdb-cli load' 或 'gdb-cli attach'",
157+
"suggestion": "GDB 会话可能已终止,重新运行 'gdb-cli load' 或 'gdb-cli attach' 或 'gdb-cli target'",
158158
},
159159
"connection_refused": {
160160
"suggestion": "GDB RPC Server 未响应,检查 GDB 进程是否正常运行",

src/gdb_cli/gdb_server/handlers.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77

88
import os
9+
import queue
910
from pathlib import Path
1011
from typing import Any, List, Optional, Tuple
1112

@@ -571,7 +572,26 @@ def handle_exec(
571572
}
572573

573574
try:
574-
output = gdb.execute(command, to_string=True)
575+
576+
# The command must run in the main thread, not in the thread that
577+
# receives from the socket. Create a function which will execute
578+
# the command and pass the output through a queue. This definition
579+
# uses lexical scoping for the command and the queue.
580+
result_queue = queue.Queue()
581+
582+
def run_command():
583+
try:
584+
output = gdb.execute(command, to_string=True)
585+
result_queue.put(("ok", output))
586+
except Exception as e:
587+
result_queue.put(("error", str(e)))
588+
589+
# Pass the function through post_event() to the main thread,
590+
# where it will run, and this thread waits to receive the output
591+
# from the queue.
592+
gdb.post_event(run_command)
593+
output_status, output = result_queue.get(timeout=30.0)
594+
575595
return {
576596
"command": command,
577597
"output": output or "(no output)"
@@ -588,7 +608,7 @@ def handle_status(**kwargs) -> dict:
588608
589609
Returns:
590610
{
591-
"mode": "core" | "attach",
611+
"mode": "core" | "attach" | "target",
592612
"binary": "...",
593613
"threads_count": N,
594614
"current_thread": {...},
@@ -604,6 +624,7 @@ def handle_status(**kwargs) -> dict:
604624
"state": "ready",
605625
"mode": session_meta.get("mode", "unknown"),
606626
"binary": session_meta.get("binary"),
627+
"gdb_pid": session_meta.get("gdb_pid"),
607628
}
608629

609630
# 线程信息
@@ -640,6 +661,9 @@ def handle_status(**kwargs) -> dict:
640661
if session_meta.get("mode") == "attach":
641662
result["pid"] = session_meta.get("pid")
642663

664+
if session_meta.get("mode") == "target":
665+
result["remote"] = session_meta.get("remote")
666+
643667
return result
644668

645669

src/gdb_cli/launcher.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,10 @@ def launch_attach(
185185
# non-stop 模式
186186
if non_stop:
187187
gdb_commands.append("set non-stop on")
188-
gdb_commands.append("set target-async on")
188+
gdb_commands.append("set mi-async on")
189+
else:
190+
gdb_commands.append("set non-stop off")
191+
gdb_commands.append("set mi-async off")
189192

190193
# 加载 binary (可选)
191194
if binary:
@@ -214,6 +217,82 @@ def launch_attach(
214217
return gdb_process
215218

216219

220+
def launch_target(
221+
remote: str,
222+
binary: Optional[str] = None,
223+
scheduler_locking: bool = True,
224+
non_stop: bool = False,
225+
timeout: int = 600,
226+
allow_write: bool = False,
227+
allow_call: bool = False,
228+
gdb_path: str = "gdb"
229+
) -> GDBProcess:
230+
"""
231+
Args:
232+
remote: host:port
233+
binary: 可执行文件路径 (可选)
234+
scheduler_locking: 是否启用 scheduler-locking
235+
non_stop: 是否启用 non-stop 模式
236+
timeout: 心跳超时秒数
237+
allow_write: 是否允许内存修改
238+
allow_call: 是否允许函数调用
239+
gdb_path: GDB 可执行文件路径
240+
241+
Returns:
242+
GDBProcess 实例
243+
"""
244+
# 创建 session
245+
safety_level = "full" if (allow_write or allow_call) else "readonly"
246+
if allow_write and not allow_call:
247+
safety_level = "readwrite"
248+
249+
session = create_session(
250+
mode="target",
251+
remote=remote,
252+
binary=binary,
253+
timeout=timeout,
254+
safety_level=safety_level
255+
)
256+
257+
# 构建 GDB 启动命令
258+
gdb_commands = [
259+
"set pagination off",
260+
"set print elements 0",
261+
"set confirm off",
262+
]
263+
264+
# non-stop 模式
265+
if non_stop:
266+
gdb_commands.append("set non-stop on")
267+
gdb_commands.append("set target-async on")
268+
269+
# 加载 binary (可选)
270+
if binary:
271+
gdb_commands.append(f"file {binary}")
272+
273+
# Target
274+
gdb_commands.append(f"target extended-remote {remote}")
275+
276+
# scheduler-locking
277+
if scheduler_locking:
278+
gdb_commands.append("set scheduler-locking on")
279+
280+
# 启动 RPC Server
281+
gdb_commands.extend(_build_server_commands(session))
282+
gdb_commands.append("python _gdb_rpc_server.set_ready()")
283+
284+
# 构建 GDB 参数
285+
gdb_args = [gdb_path, "-nx", "-q"]
286+
for cmd in gdb_commands:
287+
gdb_args.extend(["-ex", cmd])
288+
289+
# 启动进程
290+
_start_gdb_process(gdb_args, session, timeout=float(timeout))
291+
gdb_process = GDBProcess(session)
292+
gdb_process._process = session._gdb_process
293+
return gdb_process
294+
295+
217296
def _build_server_commands(session: SessionMeta) -> List[str]:
218297
"""构建 RPC Server 启动命令"""
219298
session_meta = {
@@ -222,6 +301,7 @@ def _build_server_commands(session: SessionMeta) -> List[str]:
222301
"binary": session.binary,
223302
"core": session.core,
224303
"pid": session.pid,
304+
"remote": session.remote,
225305
"sock_path": str(session.sock_path),
226306
"started_at": session.started_at,
227307
}
@@ -241,7 +321,7 @@ def _build_server_commands(session: SessionMeta) -> List[str]:
241321
# 加载 Server 脚本 (source 会将定义加载到全局命名空间)
242322
f"source {GDB_SERVER_SCRIPT}",
243323
# 启动 Server (直接调用全局命名空间中的 start_server)
244-
f"python start_server('{session.sock_path}', {json.dumps(session_meta)}, {session.heartbeat_timeout})",
324+
f"python start_server('{session.sock_path}', {session_meta}, {session.heartbeat_timeout})",
245325
]
246326

247327

src/gdb_cli/safety.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class CommandCheckResult:
5959
# 始终禁止的命令
6060
FORBIDDEN_COMMANDS: Set[str] = {
6161
"quit", "kill", "shell", "python-interactive",
62-
"signal", "detach", "attach",
62+
"signal", "detach", "attach", "target",
6363
}
6464

6565
# 命令别名映射

src/gdb_cli/session.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@
2323
class SessionMeta:
2424
"""会话元数据"""
2525
session_id: str # UUID
26-
mode: str # "core" | "attach"
26+
mode: str # "core" | "attach" | "target"
2727
binary: Optional[str] = None # 可执行文件路径
2828
core: Optional[str] = None # Core dump 路径 (core 模式)
2929
pid: Optional[int] = None # 目标进程 PID (attach 模式)
30+
remote: Optional[str] = None # host:port for target
3031
gdb_pid: Optional[int] = None # GDB 进程 PID
3132
sock_path: Optional[str] = None # Unix Socket 路径
3233
started_at: float = field(default_factory=time.time)
@@ -49,17 +50,19 @@ def create_session(
4950
binary: Optional[str] = None,
5051
core: Optional[str] = None,
5152
pid: Optional[int] = None,
53+
remote: Optional[int] = None,
5254
timeout: int = 600,
5355
safety_level: str = "readonly"
5456
) -> SessionMeta:
5557
"""
5658
创建新会话
5759
5860
Args:
59-
mode: 模式 ("core" | "attach")
61+
mode: 模式 ("core" | "attach" | "target")
6062
binary: 可执行文件路径
6163
core: Core dump 路径
6264
pid: 目标进程 PID
65+
remote: host:port
6366
timeout: 心跳超时秒数
6467
safety_level: 安全级别
6568
@@ -83,6 +86,7 @@ def create_session(
8386
binary=binary,
8487
core=core,
8588
pid=pid,
89+
remote=remote,
8690
sock_path=str(sock_path),
8791
heartbeat_timeout=timeout,
8892
safety_level=safety_level
@@ -290,6 +294,21 @@ def find_session_by_core(core: str) -> Optional[SessionMeta]:
290294
return None
291295

292296

297+
def find_session_by_remote(remote: str) -> Optional[SessionMeta]:
298+
"""
299+
Args:
300+
remote: host:port
301+
302+
Returns:
303+
已存在的 SessionMeta 或 None
304+
"""
305+
sessions = list_sessions(alive_only=True)
306+
for session in sessions:
307+
if session.mode == "target" and session.remote == remote:
308+
return session
309+
return None
310+
311+
293312
def update_session_activity(session_id: str) -> None:
294313
"""更新会话最后活跃时间"""
295314
meta = _read_meta(session_id)

0 commit comments

Comments
 (0)