【问题标题】:gdbserver: execute shell commands of the targetgdbserver:执行目标的shell命令
【发布时间】:2015-01-01 15:16:28
【问题描述】:

例如!ls 会在 gdb 本身中执行 ls 命令,但是如何在远程端执行呢?

应该很简单,但我想不通。 Per documentation target remote | lstarget remote | !ls 之类的东西应该可以解决问题,但要么是错误的,要么我不明白:这样的命令使 gdb 尝试关闭当前会话,并开始调试 ls 二进制文件。

我还发现提到了一些monitor cmd,但monitor !ls 只是触发Unknown monitor command 消息。

【问题讨论】:

  • target remote | command 用于在本地运行 command 时,command (通常)是在其他地方运行的 gdbserver 的自定义编写代理。您是想让远程 gdbserver 运行 ls 作为要调试的目标进程,还是要运行 ls 并查看其输出而不影响远程目标?
  • @MarkPlotnick 是的,我只想运行ls 并查看输出,而不会影响正在调试的进程。
  • 正如我刚刚理解的那样,输出很可能会发送到运行«gdbserver»的shell,而不是连接到的gdb。反正对我来说这没问题。
  • 我看到远程调试协议作为远程文件 i/o 请求之一包括在远程 shell 中运行任意命令的请求(请参阅sourceware.org/gdb/onlinedocs/gdb/system.html),但我没有立即知道如何告诉 gdb 发出该请求。

标签: shell gdb remote-debugging gdbserver


【解决方案1】:
! ls > /tmp/ls.txt

您尝试使用! cmd 执行的所有命令都将起作用,但您无法获得输出,因为 gdbserver 无法正确重定向流。
只需打开一个新的远程连接,即可看到 gdb 远程命令的输出重定向到之前示例中的文件。

【讨论】:

  • 我刚测试过,这个并没有在远端执行命令。我执行的步骤: 1. 在 /tmp/ 目录中运行 gdbserver :2345 ls 2. 在 $HOME 目录中运行 gdb,并在其中评估 target extended-remote localhost:2345。 3. 在 gdb ! ls 中评估。它显示 home 的内容,但不显示 /tmp。 4. 在gdb中求值! ls > 1 这会在$HOME而不是/tmp中创建一个文件11文件的内容是$HOME中的文件列表。
【解决方案2】:

一种解决方法是实现执行以下操作的自定义 gdb 命令:

  1. fork 远程进程;
  2. 切换到子劣;
  3. 将子进程替换为执行用户提供的命令的shell;
  4. 显示远程命令的输出;
  5. 回到父下级。​​

有几个限制需要考虑:

  • gdb call命令上无法解析符号:我们需要通过地址调用libc函数,这需要预先加载地址;
  • 需要安装 libc 调试信息,否则无法检索符号;
  • 在自动加载子远程库符号的过程中会出现巨大的减速:相反,我们可以使用父进程预加载地址,然后禁用自动符号加载;
  • 从 gdb call 命令执行的 Shell 命令输出不会在 gdb 终端中回显:但是,我们可以将其捕获到远程临时文件中,然后读取到内存并打印。

gdb 会话示例:

# Given remote terminal running `gdbserver :2345 ./remote_executable`, we connect to that server.
target extended-remote 192.168.1.4:2345

# Load our custom gdb command `rcmd`.
source ./remote-cmd.py

# Run until a point where libc has been loaded on the remote process, e.g. start of main().
b main
r

# Don't need the main() breakpoint anymore.
del 1

# Run the remote command, e.g. `ls`.
rcmd ls

远程-cmd.py:

#!/usr/bin/env python3

import gdb
import re
import traceback
import uuid


class RemoteCmd(gdb.Command):
    def __init__(self):
        self.addresses = {}

        self.tmp_file = f'/tmp/{uuid.uuid4().hex}'
        gdb.write(f"Using tmp output file: {self.tmp_file}.\n")

        gdb.execute("set detach-on-fork off")
        gdb.execute("set follow-fork-mode parent")

        gdb.execute("set max-value-size unlimited")
        gdb.execute("set pagination off")
        gdb.execute("set print elements 0")
        gdb.execute("set print repeats 0")

        super(RemoteCmd, self).__init__("rcmd", gdb.COMMAND_USER)

    def preload(self):
        for symbol in [
            "close",
            "execl",
            "fork",
            "free",
            "lseek",
            "malloc",
            "open",
            "read",
        ]:
            self.load(symbol)

    def load(self, symbol):
        if symbol not in self.addresses:
            address_string = gdb.execute(f"info address {symbol}", to_string=True)
            match = re.match(
                f'Symbol "{symbol}" is at ([0-9a-fx]+) .*', address_string, re.IGNORECASE
            )
            if match and len(match.groups()) > 0:
                self.addresses[symbol] = match.groups()[0]
            else:
                raise RuntimeError(f'Could not retrieve address for symbol "{symbol}".')

        return self.addresses[symbol]

    def output(self):
        # From `fcntl-linux.h`
        O_RDONLY = 0
        gdb.execute(
            f'set $fd = (int){self.load("open")}("{self.tmp_file}", {O_RDONLY})'
        )

        # From `stdio.h`
        SEEK_SET = 0
        SEEK_END = 2
        gdb.execute(f'set $len = (int){self.load("lseek")}($fd, 0, {SEEK_END})')
        gdb.execute(f'call (int){self.load("lseek")}($fd, 0, {SEEK_SET})')
        if int(gdb.convenience_variable("len")) <= 0:
            gdb.write("No output was captured.")
            return

        gdb.execute(f'set $mem = (void*){self.load("malloc")}($len)')
        gdb.execute(f'call (int){self.load("read")}($fd, $mem, $len)')
        gdb.execute('printf "%s\\n", (char*) $mem')

        gdb.execute(f'call (int){self.load("close")}($fd)')
        gdb.execute(f'call (int){self.load("free")}($mem)')

    def invoke(self, arg, from_tty):
        try:
            self.preload()

            is_auto_solib_add = gdb.parameter("auto-solib-add")
            gdb.execute("set auto-solib-add off")

            parent_inferior = gdb.selected_inferior()
            gdb.execute(f'set $child_pid = (int){self.load("fork")}()')
            child_pid = gdb.convenience_variable("child_pid")
            child_inferior = list(
                filter(lambda x: x.pid == child_pid, gdb.inferiors())
            )[0]
            gdb.execute(f"inferior {child_inferior.num}")

            try:
                gdb.execute(
                    f'call (int){self.load("execl")}("/bin/sh", "sh", "-c", "exec {arg} >{self.tmp_file} 2>&1", (char*)0)'
                )
            except gdb.error as e:
                if (
                    "The program being debugged exited while in a function called from GDB"
                    in str(e)
                ):
                    pass
                else:
                    raise e
            finally:
                gdb.execute(f"inferior {parent_inferior.num}")
                gdb.execute(f"remove-inferiors {child_inferior.num}")

            self.output()
        except Exception as e:
            gdb.write("".join(traceback.TracebackException.from_exception(e).format()))
            raise e
        finally:
            gdb.execute(f'set auto-solib-add {"on" if is_auto_solib_add else "off"}')


RemoteCmd()

【讨论】:

  • 哇,这太棒了!一个挑剔:您可以使用tb main 在一个命令中完成所有操作,而不是设置br main,然后手动删除它。这是一个临时断点,将在第一次点击时被删除。
猜你喜欢
  • 2011-06-15
  • 2013-01-05
  • 2010-09-14
  • 2017-04-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多