Using Qemu within GDB Part 4

In the previous post, all of the commands were condensed into a single ~/.gdbinit file and start and run were overloaded to work with Qemu. However, there was a subtle-bug: the run command did not pass arguments because it never had them in the first place (as it was really hook-run). In order to fix this, the hook-run command needed to obtain the arguments without overriding the run command.

Possible Approaches

While this could have been fixed with a patch to GDB sending command arguments also to their hooks in the methods execute_cmd_pre_hook in the source file gdb/gdb/cli/cli-script.c and where it is called in the execute_command function in gdb/gdb/top.c, it is would require a more significant change. Another approach is to get the arguments directly from the user input. This could be done using gdb-multiarch | tee .tmp.gdb.log, although it would not be a clean solution because it does not all fit inside ~/.gdbinit. A similar method would be to turn on set trace-commands on as discussed here; however, this introduces too much garbage stdout messages. It turns out there is actually a command called show commands, which lists the most recent commands, and this enables the ~/.gdbinit to obtain the start command’s arguments.

.gdbinit

### --------------------------------------------------------------------
### .gdbinit
### Author: William Ughetta
### Overloads `start` and `run` commands to debug aarch64 binaries with
### Qemu user-space emulation on port 1234.
### --------------------------------------------------------------------

# Temporary Files Created in the current working directory
# .tmp.gdb.log - GDB command and target output
# .tmp.gdb.src - temporary file for sourcing dynamic GDB commands
# .tmp.gdb.isStart - run was called by the start command if file exists
# .tmp.gdb.args - space-separated list of start's arguments

# Helpful resources:
# https://gist.github.com/nojhan/c3bc28e2fa0608f21551
# https://stackoverflow.com/a/17960363
# https://stackoverflow.com/a/12452235
# https://stackoverflow.com/a/15032459
# https://sourceware.org/gdb/onlinedocs/gdb/Command-History.html
# https://stackoverflow.com/q/25195501

# Execute run if aarch64 otherwise continue with start
define hook-start
  get-aarch64-args
  if ($aarch64)
    shell touch .tmp.gdb.isStart
    run
  end
end

define get-aarch64-args
  # Enable Logging
  shell rm -f .tmp.gdb.*
  set logging file .tmp.gdb.log
  set logging overwrite on
  set logging redirect on
  set pagination off
  set logging on

  # Get the arguments and target architecture.
  show commands
  info target
  show args

  # Disable Logging
  set logging off
  set pagination on
  set logging redirect off
  set logging overwrite off

  # Set Architecture
  shell echo -n 'set $aarch64 = 0' > .tmp.gdb.src
  shell if [[ -n $(grep "file type elf64-.*aarch64." .tmp.gdb.log) ]]; \
    then echo 1 >> .tmp.gdb.src; fi
  source .tmp.gdb.src

  # Get Arguments from `show commands`
  shell grep -o "start .*" .tmp.gdb.log | tail -1 | cut -c 7- > \
    .tmp.gdb.args

  # Get Arguments from `show args` if no arguments passed to start
  shell if [[ -z $(cat .tmp.gdb.args) ]]; then grep -o "started is .*" \
    .tmp.gdb.log | cut -c 13- | rev | cut -c 3- | rev > .tmp.gdb.args; \
    fi

  # Cleanup
  shell rm -f .tmp.gdb.log .tmp.gdb.src
end

python
### --------------------------------------------------------------------
### start.py
### Author: William Ughetta
### Overrides GDB run command to enable seamless aarch64 debugging.
### --------------------------------------------------------------------

import os.path
import re
import subprocess

class RunCommand(gdb.Command):
    """
    Overloads the default GDB run command to enable cross-platform
    aarch64 debugging by leveraging the default GDB start command.
    If file `.tmp.gdb.isStart` exists, behaves like start instead of
    run and uses arguments from the generated file `.tmp.gdb.args`.
    """
    def __init__ (self):
        super(RunCommand, self).__init__("run", gdb.COMMAND_RUNNING,
            gdb.COMPLETE_NONE)

    def invoke (self, args, from_tty):
        infoTarget = gdb.execute("info target", from_tty, True)
        target = re.search(r"/.*', file type", infoTarget)
        if not target:
            print("No executable file specified.\n" +
                  "Use the \"file\" or \"exec-file\" command.\n")
            return
        target = target.group(0)[:-12]
        aarch64 = re.search(r"file type elf64-.*aarch64.", infoTarget)
        if (aarch64):
            ## AArch64 Run
            isStart = os.path.isfile(".tmp.gdb.isStart")
            try:
                gdb.execute("kill", from_tty, True)
            except:
                pass
            subprocess.call("kill -9 $(ps -u | grep -m 1 " +
                "'qemu-aarch64 -g 1234' | " +
                "awk '{print $2}') 2>/dev/null", shell=True)
            gdb.execute("file " + target, from_tty, True)
            if isStart:
                with open(".tmp.gdb.args") as f:
                    args = f.read()[:-1]  # strip ending newline
                subprocess.call("rm -f " + ".tmp.gdb.args", shell=True)
            elif (not args):
                args = gdb.execute("show args", from_tty, True)[68:-3]
            gdb.execute("set args " + args, from_tty, True)
            print("Starting Qemu on Port 1234: " + target + " " + args)
            subprocess.call("qemu-aarch64 -g 1234 " + target + " " +
                args + " &>/dev/stdout </dev/stdin &", shell=True)
            gdb.execute("target remote :1234", from_tty, True)
            if isStart:
                gdb.execute("tbreak main", from_tty, True)
                subprocess.call("rm -f " + ".tmp.gdb.isStart",
                    shell=True)
            gdb.execute("continue", from_tty, True)
            if isStart:
                raise gdb.GdbError("\n") # Silences another message
        else:
            ## x86-64 Run
            gdb.execute("start " + args, from_tty)
            gdb.execute("continue", from_tty, True)

RunCommand()

end

Sample Execution

The following execution examples were started with a hello.s program in a.out (compiled with aarch64-linux-gnu-gcc -static -g hello.s) and run with gdb-multiarch a.out.

The start command with an aarch64 binary:

(gdb) start
Starting Qemu on Port 1234: /home/vagrant/workspace/a.out
0x00000000004003f0 in _start ()

Temporary breakpoint 1, main () at hello.s:6
6       stp x29, x30, [sp, #-16]!


(gdb) c
Continuing.
hello, world
[Inferior 1 (Remote target) exited normally]
(gdb)

The run command with an aarch64 binary:

(gdb) run
Starting Qemu on Port 1234: /home/vagrant/workspace/a.out
0x00000000004003f0 in _start ()
hello, world
[Inferior 1 (Remote target) exited normally]
(gdb)

Mix of two start’s and on run command on aarch64:

(gdb) start
Starting Qemu on Port 1234: /home/vagrant/hello/a.out
0x00000000004003f0 in _start ()

Temporary breakpoint 2, main () at hello.s:6
6       stp x29, x30, [sp, #-16]!


(gdb) n
8       adr x0, cMes
(gdb)
9       bl  printf
(gdb)
hello, world
11      mov w0, #0
(gdb) start

QEMU: Terminated via GDBstub
Starting Qemu on Port 1234: /home/vagrant/hello/a.out
0x00000000004003f0 in _start ()

Temporary breakpoint 3, main () at hello.s:6
6       stp x29, x30, [sp, #-16]!


(gdb) run

QEMU: Terminated via GDBstub
Starting Qemu on Port 1234: /home/vagrant/hello/a.out
0x00000000004003f0 in _start ()
hello, world
[Inferior 1 (Remote target) exited normally]
(gdb)

Again, note that aarch64 and x86-64 binaries cannot be debugged in series without first quitting and re-starting GDB. An example of the error is below:

(gdb) file hello
warning: Selected architecture i386:x86-64 is not compatible with \
reported target architecture aarch64
Architecture of file not recognized.
(gdb) start
Temporary breakpoint 4 at 0x400580: file hello.s, line 6.
Starting program: /home/vagrant/workspace/hello
Warning:
Cannot insert breakpoint 4.
Cannot access memory at address 0x555555954580

(gdb)