Skip to content

Commit 694924f

Browse files
authored
Add ARM Cortex-M OpenOCD arch, command, and session manager (#83)
This is for debugging ARM cortex-m targets through JTAG/SWD using the gdbserver implemented in OpenOCD. Manually tested with a debugger and openocd
1 parent eac4c06 commit 694924f

File tree

4 files changed

+216
-18
lines changed

4 files changed

+216
-18
lines changed

archs/arm-blackmagicprobe.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import gdb
1212

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

1416
class ARMBlackMagicProbe(ARM):
1517
arch = "ARMBlackMagicProbe"
@@ -31,29 +33,25 @@ def maps():
3133

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

4040
_cmdline_ = "gef-bmp-remote"
4141
_syntax_ = f"{_cmdline_} [OPTIONS] TTY"
42-
_example_ = [
43-
f"{_cmdline_} --scan /dev/ttyUSB1"
44-
f"{_cmdline_} --scan /dev/ttyUSB1 --power"
45-
f"{_cmdline_} --scan /dev/ttyUSB1 --power --keep-power"
42+
_example_ = [f"{_cmdline_} --scan /dev/ttyUSB1",
43+
f"{_cmdline_} --scan /dev/ttyUSB1 --power",
44+
f"{_cmdline_} --scan /dev/ttyUSB1 --power --keep-power",
4645
f"{_cmdline_} --file /path/to/binary.elf --attach 1 /dev/ttyUSB1",
47-
f"{_cmdline_} --file /path/to/binary.elf --attach 1 --power /dev/ttyUSB1",
48-
]
46+
f"{_cmdline_} --file /path/to/binary.elf --attach 1 --power /dev/ttyUSB1"]
4947

5048
def __init__(self) -> None:
5149
super().__init__(prefix=False)
5250
return
5351

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

9694

9795
class GefBMPRemoteSessionManager(GefRemoteSessionManager):
98-
"""Class for managing remote sessions with GEF. It will create a temporary environment
99-
designed to clone the remote one."""
96+
"""This subclass of GefRemoteSessionManager specially handles the
97+
intricacies involved with connecting to a BlackMagicProbe."""
10098
def __init__(self, tty: str="", file: str="", attach: int=1,
10199
scan: bool=False, power: bool=False, keep_power: bool=False) -> None:
102100
self.__tty = tty
@@ -106,7 +104,12 @@ def __init__(self, tty: str="", file: str="", attach: int=1,
106104
self.__power = power
107105
self.__keep_power = keep_power
108106
self.__local_root_fd = tempfile.TemporaryDirectory()
109-
self.__local_root_path = pathlib.Path(self.__local_root_fd.name)
107+
self.__local_root_path = Path(self.__local_root_fd.name)
108+
class BMPMode():
109+
def prompt_string(self) -> str:
110+
return Color.boldify("(BMP) ")
111+
112+
self._mode = BMPMode()
110113

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

125128
@property
126-
def root(self) -> pathlib.Path:
129+
def root(self) -> Path:
127130
return self.__local_root_path.absolute()
128131

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

137140
@property
138-
def file(self) -> Optional[pathlib.Path]:
141+
def file(self) -> Optional[Path]:
139142
if self.__file:
140-
return pathlib.Path(self.__file).expanduser()
143+
return Path(self.__file).expanduser()
141144
return None
142145

143146
def connect(self) -> bool:

archs/arm-openocd.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""
2+
ARM through OpenOCD support for GEF
3+
4+
To use, source this file *after* gef
5+
6+
Author: Grazfather
7+
"""
8+
9+
from typing import Optional
10+
from pathlib import Path
11+
12+
import gdb
13+
14+
assert 'gef' in globals(), "This file must be source after gef.py"
15+
16+
17+
class ARMOpenOCD(ARM):
18+
arch = "ARMOpenOCD"
19+
aliases = ("ARMOpenOCD",)
20+
all_registers = ("$r0", "$r1", "$r2", "$r3", "$r4", "$r5", "$r6",
21+
"$r7", "$r8", "$r9", "$r10", "$r11", "$r12", "$sp",
22+
"$lr", "$pc", "$xPSR")
23+
flag_register = "$xPSR"
24+
@staticmethod
25+
def supports_gdb_arch(arch: str) -> Optional[bool]:
26+
if "arm" in arch and arch.endswith("-m"):
27+
return True
28+
return None
29+
30+
@staticmethod
31+
def maps():
32+
yield from GefMemoryManager.parse_info_mem()
33+
34+
35+
@register
36+
class OpenOCDRemoteCommand(GenericCommand):
37+
"""This command is intended to replace `gef-remote` to connect to an
38+
OpenOCD-hosted gdbserver. It uses a special session manager that knows how
39+
to connect and manage the server."""
40+
41+
_cmdline_ = "gef-openocd-remote"
42+
_syntax_ = f"{_cmdline_} [OPTIONS] HOST PORT"
43+
_example_ = [f"{_cmdline_} --file /path/to/binary.elf localhost 3333",
44+
f"{_cmdline_} localhost 3333"]
45+
46+
def __init__(self) -> None:
47+
super().__init__(prefix=False)
48+
return
49+
50+
@parse_arguments({"host": "", "port": 0}, {"--file": ""})
51+
def do_invoke(self, _: list[str], **kwargs: Any) -> None:
52+
if gef.session.remote is not None:
53+
err("You're already in a remote session. Close it first before opening a new one...")
54+
return
55+
56+
# argument check
57+
args: argparse.Namespace = kwargs["arguments"]
58+
if not args.host or not args.port:
59+
err("Missing parameters")
60+
return
61+
62+
# Try to establish the remote session, throw on error
63+
# Set `.remote_initializing` to True here - `GefRemoteSessionManager` invokes code which
64+
# calls `is_remote_debug` which checks if `remote_initializing` is True or `.remote` is None
65+
# This prevents some spurious errors being thrown during startup
66+
gef.session.remote_initializing = True
67+
session = GefOpenOCDRemoteSessionManager(args.host, args.port, args.file)
68+
69+
dbg(f"[remote] initializing remote session with {session.target} under {session.root}")
70+
71+
# Connect can return false if it wants us to disconnect
72+
if not session.connect():
73+
gef.session.remote = None
74+
gef.session.remote_initializing = False
75+
return
76+
if not session.setup():
77+
gef.session.remote = None
78+
gef.session.remote_initializing = False
79+
raise EnvironmentError("Failed to setup remote target")
80+
81+
gef.session.remote_initializing = False
82+
gef.session.remote = session
83+
reset_all_caches()
84+
gdb.execute("context")
85+
return
86+
87+
88+
# We CANNOT use the normal session manager because it assumes we have a PID
89+
class GefOpenOCDRemoteSessionManager(GefRemoteSessionManager):
90+
"""This subclass of GefRemoteSessionManager specially handles the
91+
intricacies involved with connecting to an OpenOCD-hosted GDB server.
92+
Specifically, it does not have the concept of PIDs which we need to work
93+
around."""
94+
def __init__(self, host: str, port: str, file: str="") -> None:
95+
self.__host = host
96+
self.__port = port
97+
self.__file = file
98+
self.__local_root_fd = tempfile.TemporaryDirectory()
99+
self.__local_root_path = Path(self.__local_root_fd.name)
100+
class OpenOCDMode():
101+
def prompt_string(self) -> str:
102+
return Color.boldify("(OpenOCD) ")
103+
104+
self._mode = OpenOCDMode()
105+
106+
def __str__(self) -> str:
107+
return f"OpenOCDRemoteSessionManager(='{self.__tty}', file='{self.__file}', attach={self.__attach})"
108+
109+
def close(self) -> None:
110+
self.__local_root_fd.cleanup()
111+
try:
112+
gef_on_new_unhook(self.remote_objfile_event_handler)
113+
gef_on_new_hook(new_objfile_handler)
114+
except Exception as e:
115+
warn(f"Exception while restoring local context: {str(e)}")
116+
return
117+
118+
@property
119+
def target(self) -> str:
120+
return f"{self.__host}:{self.__port}"
121+
122+
@property
123+
def root(self) -> Path:
124+
return self.__local_root_path.absolute()
125+
126+
def sync(self, src: str, dst: Optional[str] = None) -> bool:
127+
# We cannot sync from this target
128+
return None
129+
130+
@property
131+
def file(self) -> Optional[Path]:
132+
if self.__file:
133+
return Path(self.__file).expanduser()
134+
return None
135+
136+
def connect(self) -> bool:
137+
"""Connect to remote target. If in extended mode, also attach to the given PID."""
138+
# before anything, register our new hook to download files from the remote target
139+
dbg(f"[remote] Installing new objfile handlers")
140+
try:
141+
gef_on_new_unhook(new_objfile_handler)
142+
except SystemError:
143+
# the default objfile handler might already have been removed, ignore failure
144+
pass
145+
146+
gef_on_new_hook(self.remote_objfile_event_handler)
147+
148+
# Connect
149+
with DisableContextOutputContext():
150+
self._gdb_execute(f"target extended-remote {self.target}")
151+
152+
try:
153+
with DisableContextOutputContext():
154+
if self.file:
155+
self._gdb_execute(f"file '{self.file}'")
156+
except Exception as e:
157+
err(f"Failed to connect to {self.target}: {e}")
158+
# a failure will trigger the cleanup, deleting our hook
159+
return False
160+
161+
return True
162+
163+
def setup(self) -> bool:
164+
dbg(f"Setting up as remote session")
165+
166+
# refresh gef to consider the binary
167+
reset_all_caches()
168+
if self.file:
169+
gef.binary = Elf(self.file)
170+
# We'd like to set this earlier, but we can't because of this bug
171+
# https://sourceware.org/bugzilla/show_bug.cgi?id=31303
172+
reset_architecture("ARMOpenOCD")
173+
return True
174+
175+
def _gdb_execute(self, cmd):
176+
dbg(f"[remote] Executing '{cmd}'")
177+
gdb.execute(cmd)

docs/archs/arm-openocd.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## ARMOpenOCD
2+
3+
The ARM OpenOCD architecture is a special arcthtecture used with the `gef-openocd-remote` command.
4+
Please read the [documentation](../commands/gef-openocd-remote.md) for the command.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## Command gef-openocd-remote
2+
3+
The `gef-openocd-command` is used with the [`ARMOpenOCD`](../../archs/arm-openocd.py] architecture.
4+
5+
The [arm-openocd.py](../../archs/arm-openocd.py) script adds an easy way to extend the `gef-remote`
6+
functionality to easily debug ARM targets using a OpenOCD gdbserver. It creates a custom ARM-derived
7+
`Architecture`, as well as the `gef-openocd-remote` command, which lets you easily connect to the
8+
target, optionally loading the accompanying ELF binary.
9+
10+
### Usage
11+
12+
```bash
13+
gef-openocd-remote localhost 3333 --file /path/to/elf
14+
```

0 commit comments

Comments
 (0)