Skip to content

Commit

Permalink
Add ARM Cortex-M OpenOCD arch, command, and session manager (#83)
Browse files Browse the repository at this point in the history
This is for debugging ARM cortex-m targets through JTAG/SWD using the
gdbserver implemented in OpenOCD.

Manually tested with a debugger and openocd
  • Loading branch information
Grazfather authored Jan 3, 2025
1 parent eac4c06 commit 694924f
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 18 deletions.
39 changes: 21 additions & 18 deletions archs/arm-blackmagicprobe.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import gdb

assert 'gef' in globals(), "This file must be source after gef.py"


class ARMBlackMagicProbe(ARM):
arch = "ARMBlackMagicProbe"
Expand All @@ -31,29 +33,25 @@ def maps():

@register
class BMPRemoteCommand(GenericCommand):
"""GDB `target remote` command on steroids. This command will use the remote procfs to create
a local copy of the execution environment, including the target binary and its libraries
in the local temporary directory (the value by default is in `gef.config.tempdir`). Additionally, it
will fetch all the /proc/PID/maps and loads all its information. If procfs is not available remotely, the command
will likely fail. You can however still use the limited command provided by GDB `target remote`."""
"""This command is intended to replace `gef-remote` to connect to a
BlackMagicProbe. It uses a special session manager that knows how to
connect and manage the server running over a tty."""

_cmdline_ = "gef-bmp-remote"
_syntax_ = f"{_cmdline_} [OPTIONS] TTY"
_example_ = [
f"{_cmdline_} --scan /dev/ttyUSB1"
f"{_cmdline_} --scan /dev/ttyUSB1 --power"
f"{_cmdline_} --scan /dev/ttyUSB1 --power --keep-power"
_example_ = [f"{_cmdline_} --scan /dev/ttyUSB1",
f"{_cmdline_} --scan /dev/ttyUSB1 --power",
f"{_cmdline_} --scan /dev/ttyUSB1 --power --keep-power",
f"{_cmdline_} --file /path/to/binary.elf --attach 1 /dev/ttyUSB1",
f"{_cmdline_} --file /path/to/binary.elf --attach 1 --power /dev/ttyUSB1",
]
f"{_cmdline_} --file /path/to/binary.elf --attach 1 --power /dev/ttyUSB1"]

def __init__(self) -> None:
super().__init__(prefix=False)
return

@parse_arguments({"tty": ""}, {"--file": "", "--attach": "", "--power": False,
"--keep-power": False, "--scan": False})
def do_invoke(self, _: List[str], **kwargs: Any) -> None:
def do_invoke(self, _: list[str], **kwargs: Any) -> None:
if gef.session.remote is not None:
err("You're already in a remote session. Close it first before opening a new one...")
return
Expand Down Expand Up @@ -95,8 +93,8 @@ def do_invoke(self, _: List[str], **kwargs: Any) -> None:


class GefBMPRemoteSessionManager(GefRemoteSessionManager):
"""Class for managing remote sessions with GEF. It will create a temporary environment
designed to clone the remote one."""
"""This subclass of GefRemoteSessionManager specially handles the
intricacies involved with connecting to a BlackMagicProbe."""
def __init__(self, tty: str="", file: str="", attach: int=1,
scan: bool=False, power: bool=False, keep_power: bool=False) -> None:
self.__tty = tty
Expand All @@ -106,7 +104,12 @@ def __init__(self, tty: str="", file: str="", attach: int=1,
self.__power = power
self.__keep_power = keep_power
self.__local_root_fd = tempfile.TemporaryDirectory()
self.__local_root_path = pathlib.Path(self.__local_root_fd.name)
self.__local_root_path = Path(self.__local_root_fd.name)
class BMPMode():
def prompt_string(self) -> str:
return Color.boldify("(BMP) ")

self._mode = BMPMode()

def __str__(self) -> str:
return f"BMPRemoteSessionManager(tty='{self.__tty}', file='{self.__file}', attach={self.__attach})"
Expand All @@ -123,7 +126,7 @@ def close(self) -> None:
return

@property
def root(self) -> pathlib.Path:
def root(self) -> Path:
return self.__local_root_path.absolute()

@property
Expand All @@ -135,9 +138,9 @@ def sync(self, src: str, dst: Optional[str] = None) -> bool:
return None

@property
def file(self) -> Optional[pathlib.Path]:
def file(self) -> Optional[Path]:
if self.__file:
return pathlib.Path(self.__file).expanduser()
return Path(self.__file).expanduser()
return None

def connect(self) -> bool:
Expand Down
177 changes: 177 additions & 0 deletions archs/arm-openocd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
ARM through OpenOCD support for GEF
To use, source this file *after* gef
Author: Grazfather
"""

from typing import Optional
from pathlib import Path

import gdb

assert 'gef' in globals(), "This file must be source after gef.py"


class ARMOpenOCD(ARM):
arch = "ARMOpenOCD"
aliases = ("ARMOpenOCD",)
all_registers = ("$r0", "$r1", "$r2", "$r3", "$r4", "$r5", "$r6",
"$r7", "$r8", "$r9", "$r10", "$r11", "$r12", "$sp",
"$lr", "$pc", "$xPSR")
flag_register = "$xPSR"
@staticmethod
def supports_gdb_arch(arch: str) -> Optional[bool]:
if "arm" in arch and arch.endswith("-m"):
return True
return None

@staticmethod
def maps():
yield from GefMemoryManager.parse_info_mem()


@register
class OpenOCDRemoteCommand(GenericCommand):
"""This command is intended to replace `gef-remote` to connect to an
OpenOCD-hosted gdbserver. It uses a special session manager that knows how
to connect and manage the server."""

_cmdline_ = "gef-openocd-remote"
_syntax_ = f"{_cmdline_} [OPTIONS] HOST PORT"
_example_ = [f"{_cmdline_} --file /path/to/binary.elf localhost 3333",
f"{_cmdline_} localhost 3333"]

def __init__(self) -> None:
super().__init__(prefix=False)
return

@parse_arguments({"host": "", "port": 0}, {"--file": ""})
def do_invoke(self, _: list[str], **kwargs: Any) -> None:
if gef.session.remote is not None:
err("You're already in a remote session. Close it first before opening a new one...")
return

# argument check
args: argparse.Namespace = kwargs["arguments"]
if not args.host or not args.port:
err("Missing parameters")
return

# Try to establish the remote session, throw on error
# Set `.remote_initializing` to True here - `GefRemoteSessionManager` invokes code which
# calls `is_remote_debug` which checks if `remote_initializing` is True or `.remote` is None
# This prevents some spurious errors being thrown during startup
gef.session.remote_initializing = True
session = GefOpenOCDRemoteSessionManager(args.host, args.port, args.file)

dbg(f"[remote] initializing remote session with {session.target} under {session.root}")

# Connect can return false if it wants us to disconnect
if not session.connect():
gef.session.remote = None
gef.session.remote_initializing = False
return
if not session.setup():
gef.session.remote = None
gef.session.remote_initializing = False
raise EnvironmentError("Failed to setup remote target")

gef.session.remote_initializing = False
gef.session.remote = session
reset_all_caches()
gdb.execute("context")
return


# We CANNOT use the normal session manager because it assumes we have a PID
class GefOpenOCDRemoteSessionManager(GefRemoteSessionManager):
"""This subclass of GefRemoteSessionManager specially handles the
intricacies involved with connecting to an OpenOCD-hosted GDB server.
Specifically, it does not have the concept of PIDs which we need to work
around."""
def __init__(self, host: str, port: str, file: str="") -> None:
self.__host = host
self.__port = port
self.__file = file
self.__local_root_fd = tempfile.TemporaryDirectory()
self.__local_root_path = Path(self.__local_root_fd.name)
class OpenOCDMode():
def prompt_string(self) -> str:
return Color.boldify("(OpenOCD) ")

self._mode = OpenOCDMode()

def __str__(self) -> str:
return f"OpenOCDRemoteSessionManager(='{self.__tty}', file='{self.__file}', attach={self.__attach})"

def close(self) -> None:
self.__local_root_fd.cleanup()
try:
gef_on_new_unhook(self.remote_objfile_event_handler)
gef_on_new_hook(new_objfile_handler)
except Exception as e:
warn(f"Exception while restoring local context: {str(e)}")
return

@property
def target(self) -> str:
return f"{self.__host}:{self.__port}"

@property
def root(self) -> Path:
return self.__local_root_path.absolute()

def sync(self, src: str, dst: Optional[str] = None) -> bool:
# We cannot sync from this target
return None

@property
def file(self) -> Optional[Path]:
if self.__file:
return Path(self.__file).expanduser()
return None

def connect(self) -> bool:
"""Connect to remote target. If in extended mode, also attach to the given PID."""
# before anything, register our new hook to download files from the remote target
dbg(f"[remote] Installing new objfile handlers")
try:
gef_on_new_unhook(new_objfile_handler)
except SystemError:
# the default objfile handler might already have been removed, ignore failure
pass

gef_on_new_hook(self.remote_objfile_event_handler)

# Connect
with DisableContextOutputContext():
self._gdb_execute(f"target extended-remote {self.target}")

try:
with DisableContextOutputContext():
if self.file:
self._gdb_execute(f"file '{self.file}'")
except Exception as e:
err(f"Failed to connect to {self.target}: {e}")
# a failure will trigger the cleanup, deleting our hook
return False

return True

def setup(self) -> bool:
dbg(f"Setting up as remote session")

# refresh gef to consider the binary
reset_all_caches()
if self.file:
gef.binary = Elf(self.file)
# We'd like to set this earlier, but we can't because of this bug
# https://sourceware.org/bugzilla/show_bug.cgi?id=31303
reset_architecture("ARMOpenOCD")
return True

def _gdb_execute(self, cmd):
dbg(f"[remote] Executing '{cmd}'")
gdb.execute(cmd)
4 changes: 4 additions & 0 deletions docs/archs/arm-openocd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## ARMOpenOCD

The ARM OpenOCD architecture is a special arcthtecture used with the `gef-openocd-remote` command.
Please read the [documentation](../commands/gef-openocd-remote.md) for the command.
14 changes: 14 additions & 0 deletions docs/commands/gef-openocd-remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## Command gef-openocd-remote

The `gef-openocd-command` is used with the [`ARMOpenOCD`](../../archs/arm-openocd.py] architecture.

The [arm-openocd.py](../../archs/arm-openocd.py) script adds an easy way to extend the `gef-remote`
functionality to easily debug ARM targets using a OpenOCD gdbserver. It creates a custom ARM-derived
`Architecture`, as well as the `gef-openocd-remote` command, which lets you easily connect to the
target, optionally loading the accompanying ELF binary.

### Usage

```bash
gef-openocd-remote localhost 3333 --file /path/to/elf
```

0 comments on commit 694924f

Please sign in to comment.