4

E.g. !ls would execute ls command in gdb itself, but how to do it on remote side?

It should be simple, but I can't figure it out. Per documentation something like target remote | ls or target remote | !ls ought to do the trick, but either it's wrong or I don't understand something: such command makes gdb to try to close current session, and start debugging ls binary.

I also found some monitor cmd mentioned, but monitor !ls just triggers Unknown monitor command message.

Hi-Angel
  • 4,933
  • 8
  • 63
  • 86
  • `target remote | command` is used when you want to run a `command` locally where that `command` is (typically) a custom-written proxy for a gdbserver running somewhere else. Do you want to have the remote gdbserver run `ls` as a target process to be debugged, or do you want to run `ls` and view its output without affecting the remote target? – Mark Plotnick Nov 05 '14 at 15:25
  • @MarkPlotnick yes, I want just run `ls` and look at the output, without affecting the process that being debugged. – Hi-Angel Nov 05 '14 at 15:32
  • Also as I just understood, the output most probably would go to the shell where «gdbserver» is running, not to the gdb that is attached to. To me this is okay anyway. – Hi-Angel Nov 05 '14 at 15:35
  • 1
    I see that the remote debugging protocol includes, as one of the remote file i/o requests, a request to run an arbitrary command in a remote shell (see https://sourceware.org/gdb/onlinedocs/gdb/system.html) but I don't know offhand how to tell gdb to make that request. – Mark Plotnick Nov 05 '14 at 15:41

2 Answers2

1

A workaround is to implement a custom gdb command that does the following:

  1. fork the remote process;
  2. switch to the child inferior;
  3. replace the child process with a shell that executes the user's provided command;
  4. display the remote command's output;
  5. go back to the parent inferior.

There are several limitations to take into account:

  • Symbols are not resolved on gdb call command: we need to call libc functions by address, which requires loading addresses beforehand;
  • Requires libc debug info to be installed, otherwise symbols can't be retrieved;
  • Huge slowdown can be experienced during automatic loading of child's remote library symbols: instead, we can preload addresses using the parent process, then disable automatic symbol loading;
  • Shell command output executed from gdb call command is not echoed in the gdb terminal: however, we can capture it in a remote temporary file, then read to memory and print.

Example gdb session:

# 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

remote-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()
fzbd
  • 477
  • 2
  • 8
  • Wow, this is amazing! One nitpick: instead of setting `br main`, and later manually deleting it, you can do it all in one command with `tb main`. That's a temporary breakpoint, which will be deleted on the first hit. – Hi-Angel Jun 23 '21 at 17:37
0
! ls > /tmp/ls.txt

All the command that you try to execute using ! cmd will works but you can't get the output because gdbserver is not able to redirect the stream correctly.
Just open a new remote connection to see the output of your gdb remote command redirect to a file like the example before.

user1823890
  • 704
  • 8
  • 7
  • I just tested, and this does not execute the command on the remote side. Steps I did: 1. in /tmp/ dir run `gdbserver :2345 ls` 2. in $HOME dir run `gdb`, and evaluate in it `target extended-remote localhost:2345`. 3. evaluate in gdb `! ls`. It shows content of home but not /tmp. 4. Evaluate in gdb `! ls > 1` This creates a file `1` in $HOME but not in /tmp, and the content of the `1` file is the list of files in $HOME. – Hi-Angel Sep 18 '19 at 21:50