oep-client-python 0.0.54__py3-none-any.whl → 0.0.55__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oep_client/__init__.py +1 -1
- oep_client/brokers.py +149 -0
- oep_client/gdb_server.py +528 -0
- oep_client/host.py +4 -1
- oep_client/ide.py +341 -0
- oep_client/link.py +27 -4
- oep_client/pyserial/protocol_oep.py +2 -2
- oep_client/pytest_plugin.py +433 -0
- oep_client/{rp2040_peer.py → rp2040_swd.py} +11 -77
- oep_client/virtual_bench.py +1 -1
- oep_client/virtual_bench_mdns.py +9 -4
- {oep_client_python-0.0.54.dist-info → oep_client_python-0.0.55.dist-info}/METADATA +13 -1
- {oep_client_python-0.0.54.dist-info → oep_client_python-0.0.55.dist-info}/RECORD +16 -13
- {oep_client_python-0.0.54.dist-info → oep_client_python-0.0.55.dist-info}/entry_points.txt +3 -0
- oep_client/pytest_rp2040_peer.py +0 -68
- {oep_client_python-0.0.54.dist-info → oep_client_python-0.0.55.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.54.dist-info → oep_client_python-0.0.55.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
oep_client/brokers.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Finding a broker that serves a probe, and opening a session that shares its lock (OEP host guide §7.1, §7.2).
|
|
2
|
+
|
|
3
|
+
A broker publishes itself in the user's directory `oep/brokers/<unit_id>.json`: where to connect (`endpoints`, each with
|
|
4
|
+
a `kind`; `tcp` is transports §1's TCP form) and the sharing extensions it takes (`share`). A tool tries, in order:
|
|
5
|
+
|
|
6
|
+
1. the environment variable `OEP_BROKER` (`<host>:<port>`), for a broker on another machine or a test;
|
|
7
|
+
2. the file: each endpoint of a kind it knows, checking that fn 0's describe names the file's unit_id (a dead broker's
|
|
8
|
+
port may now be another probe's broker);
|
|
9
|
+
3. the probe itself.
|
|
10
|
+
|
|
11
|
+
The tool joins with the sharing extension `oep.probe.share` (critical): sessions and resources are the client's, the
|
|
12
|
+
broker holds the probe's lock and lease. A tool never deletes a broker's files: only the broker holding
|
|
13
|
+
`<unit_id>.lock` removes or rewrites `<unit_id>.json`, and `<unit_id>.lock` stays.
|
|
14
|
+
|
|
15
|
+
from oep_client import brokers
|
|
16
|
+
hst = brokers.open_host("30eda0e31108", owner="my-tool") # through the broker when one serves it, else directly
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import stat
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
SHARE = "oep.probe.share" # host guide §7.1
|
|
30
|
+
ENV = "OEP_BROKER" # host guide §7.2: <host>:<port>, before the file
|
|
31
|
+
LEASE_MS = 60000
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def directory() -> Path:
|
|
35
|
+
"""The user's broker directory (host guide §7.2): Linux $XDG_RUNTIME_DIR/oep/brokers, else
|
|
36
|
+
$TMPDIR/oep-<uid>/brokers (/tmp without TMPDIR); macOS $TMPDIR/oep/brokers; Windows %LOCALAPPDATA%\\oep\\brokers."""
|
|
37
|
+
if sys.platform == "win32":
|
|
38
|
+
return Path(os.environ.get("LOCALAPPDATA") or Path.home() / "AppData" / "Local") / "oep" / "brokers"
|
|
39
|
+
if sys.platform == "darwin":
|
|
40
|
+
return Path(os.environ.get("TMPDIR") or tempfile.gettempdir()) / "oep" / "brokers"
|
|
41
|
+
runtime = os.environ.get("XDG_RUNTIME_DIR")
|
|
42
|
+
if runtime:
|
|
43
|
+
return Path(runtime) / "oep" / "brokers"
|
|
44
|
+
return Path(os.environ.get("TMPDIR") or "/tmp") / f"oep-{os.getuid()}" / "brokers"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _private(path: Path) -> bool:
|
|
48
|
+
"""The directory is the user's own and closed to others (0700); not checked on Windows (its ACLs)."""
|
|
49
|
+
if sys.platform == "win32":
|
|
50
|
+
return path.is_dir()
|
|
51
|
+
try:
|
|
52
|
+
st = path.stat()
|
|
53
|
+
except OSError:
|
|
54
|
+
return False
|
|
55
|
+
return stat.S_ISDIR(st.st_mode) and st.st_uid == os.getuid() and stat.S_IMODE(st.st_mode) & 0o077 == 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Broker:
|
|
60
|
+
"""One broker as its file (or OEP_BROKER) names it."""
|
|
61
|
+
unit_id: str
|
|
62
|
+
endpoints: list[dict] = field(default_factory=list)
|
|
63
|
+
share: list[str] = field(default_factory=list)
|
|
64
|
+
pid: int | None = None
|
|
65
|
+
implementation: dict = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
def tcp(self) -> list[tuple[str, int]]:
|
|
68
|
+
"""The endpoints this client can use: kind tcp, `host:port`. Other kinds are skipped."""
|
|
69
|
+
out = []
|
|
70
|
+
for e in self.endpoints:
|
|
71
|
+
if isinstance(e, dict) and e.get("kind") == "tcp" and isinstance(e.get("address"), str):
|
|
72
|
+
host, _, port = e["address"].rpartition(":")
|
|
73
|
+
if host and port.isdigit():
|
|
74
|
+
out.append((host, int(port)))
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find(unit_id: str, *, environ=None, base: Path | None = None) -> Broker | None:
|
|
79
|
+
"""The broker for this probe: OEP_BROKER, else its file in the user's directory, else None. Not connected yet."""
|
|
80
|
+
environ = os.environ if environ is None else environ
|
|
81
|
+
unit = unit_id.lower()
|
|
82
|
+
forced = environ.get(ENV)
|
|
83
|
+
if forced:
|
|
84
|
+
return Broker(unit, [{"kind": "tcp", "address": forced}], [SHARE])
|
|
85
|
+
if unit.startswith("x-"):
|
|
86
|
+
return None # not unique: never keyed by (core §7.5)
|
|
87
|
+
folder = base or directory()
|
|
88
|
+
if not _private(folder):
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
doc = json.loads((folder / f"{unit}.json").read_text(encoding="utf-8"))
|
|
92
|
+
except (OSError, ValueError):
|
|
93
|
+
return None
|
|
94
|
+
if not isinstance(doc, dict) or str(doc.get("unit_id", "")).lower() != unit:
|
|
95
|
+
return None
|
|
96
|
+
share = [s for s in doc.get("share") or [] if isinstance(s, str)]
|
|
97
|
+
endpoints = [e for e in doc.get("endpoints") or [] if isinstance(e, dict)]
|
|
98
|
+
pid = doc.get("pid") if isinstance(doc.get("pid"), int) else None
|
|
99
|
+
impl = doc.get("implementation") if isinstance(doc.get("implementation"), dict) else {}
|
|
100
|
+
return Broker(unit, endpoints, share, pid, impl)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def connect(broker: Broker, *, timeout: float = 3.0):
|
|
104
|
+
"""A Host on the first endpoint that answers with the broker's unit_id in fn 0's describe, not opened; None when
|
|
105
|
+
none does (a dead broker, or its port reused by another probe's broker)."""
|
|
106
|
+
from . import core, host as oh, link
|
|
107
|
+
for address, port in broker.tcp():
|
|
108
|
+
try:
|
|
109
|
+
lk = link.SerialLink.on_stream(link.TcpStream(address, port), "length", timeout)
|
|
110
|
+
except OSError:
|
|
111
|
+
continue
|
|
112
|
+
lk.resend = False # the broker retries towards the probe itself (link.open_host)
|
|
113
|
+
lk.transport = "tcp"
|
|
114
|
+
h = oh.Host(lk.send)
|
|
115
|
+
lk.attach_host(h)
|
|
116
|
+
h.link = lk
|
|
117
|
+
try:
|
|
118
|
+
same = (core.unit_id(h) or "").lower() == broker.unit_id
|
|
119
|
+
except Exception:
|
|
120
|
+
same = False
|
|
121
|
+
if same:
|
|
122
|
+
return h
|
|
123
|
+
lk.close()
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def open_host(unit_id: str, owner: str, *, lease_ms: int = LEASE_MS, direct: str | None = None, environ=None):
|
|
128
|
+
"""An opened Host on the probe: sharing the broker's lock when one serves it (host guide §7.2), else the probe
|
|
129
|
+
itself (`direct`, default oep://<unit_id>). A broker that does not take oep.probe.share is used without it."""
|
|
130
|
+
from . import host as oh, link, message as m
|
|
131
|
+
broker = find(unit_id, environ=environ)
|
|
132
|
+
h = connect(broker) if broker else None
|
|
133
|
+
if h is not None:
|
|
134
|
+
try:
|
|
135
|
+
if SHARE in broker.share:
|
|
136
|
+
try:
|
|
137
|
+
h.open(lease_ms, owner=owner, extensions=[SHARE])
|
|
138
|
+
return h
|
|
139
|
+
except oh.Unsupported as e:
|
|
140
|
+
if e.tag != m.TAG_NAMED_EXTENSION | m.TAG_CRITICAL:
|
|
141
|
+
raise
|
|
142
|
+
h.open(lease_ms, owner=owner)
|
|
143
|
+
return h
|
|
144
|
+
except Exception:
|
|
145
|
+
h.link.close()
|
|
146
|
+
raise
|
|
147
|
+
h = link.open_host(direct or f"oep://{unit_id}")
|
|
148
|
+
h.open(lease_ms, owner=owner)
|
|
149
|
+
return h
|
oep_client/gdb_server.py
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""A GDB remote server for a Cortex-M on an OEP probe's SWD: what an IDE's debugger connects to.
|
|
2
|
+
|
|
3
|
+
The Arduino IDE debugs through cortex-debug, which with `servertype: external` connects gdb to `gdbTarget`
|
|
4
|
+
(`target extended-remote HOST:PORT`) and sends `monitor reset halt`, `load` and `monitor halt`. This server answers
|
|
5
|
+
gdb's remote protocol on a TCP port and does the work over the probe: registers and memory through the MEM-AP, run,
|
|
6
|
+
stop and single-step through DHCSR, breakpoints in the FPB, `load` into flash by the chip's own routine.
|
|
7
|
+
|
|
8
|
+
The target is a `Target`-shaped object (`Rp2040Debug` here: an RP2040's core 0 through `rp2040_swd`), so the server
|
|
9
|
+
runs against a fake one in tests.
|
|
10
|
+
|
|
11
|
+
What gdb gets:
|
|
12
|
+
- `qSupported`: PacketSize, no-ack mode, `qXfer:features:read` (an M-profile target.xml: r0-r12, sp, lr, pc, xpsr)
|
|
13
|
+
and `qXfer:memory-map:read` (the target's flash, RAM and the rest; gdb then puts breakpoints in flash as hardware
|
|
14
|
+
ones and writes flash with vFlashErase / vFlashWrite / vFlashDone).
|
|
15
|
+
- `g` `G` `p` `P` `m` `M` `X`, `c` `s` `vCont`, Ctrl-C, `Z0` / `Z1` (both in the FPB) `z0` / `z1`, `qRcmd`
|
|
16
|
+
(`monitor reset halt`, `monitor reset`, `monitor halt`, `monitor resume`), `D`, `k`, `qAttached`, threads as one.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import binascii
|
|
22
|
+
import select
|
|
23
|
+
import socket
|
|
24
|
+
import struct
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
from typing import Callable
|
|
28
|
+
|
|
29
|
+
POLL = 0.05 # how often a running core is looked at
|
|
30
|
+
KEEPALIVE_S = 5.0 # a request to the probe this often while gdb is idle (the session's lease)
|
|
31
|
+
|
|
32
|
+
TARGET_XML = """<?xml version="1.0"?>
|
|
33
|
+
<!DOCTYPE target SYSTEM "gdb-target.dtd">
|
|
34
|
+
<target version="1.0">
|
|
35
|
+
<architecture>arm</architecture>
|
|
36
|
+
<feature name="org.gnu.gdb.arm.m-profile">
|
|
37
|
+
""" + "".join(f'<reg name="r{i}" bitsize="32" regnum="{i}" type="uint32" group="general"/>\n' for i in range(13)) + """\
|
|
38
|
+
<reg name="sp" bitsize="32" regnum="13" type="data_ptr" group="general"/>
|
|
39
|
+
<reg name="lr" bitsize="32" regnum="14" type="uint32" group="general"/>
|
|
40
|
+
<reg name="pc" bitsize="32" regnum="15" type="code_ptr" group="general"/>
|
|
41
|
+
<reg name="xpsr" bitsize="32" regnum="16" type="uint32" group="general"/>
|
|
42
|
+
</feature>
|
|
43
|
+
</target>
|
|
44
|
+
"""
|
|
45
|
+
NREGS = 17
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _locked(fn):
|
|
49
|
+
"""One operation at a time on the probe: the gdb session's and an upload's (the IDE's Upload while gdb is
|
|
50
|
+
connected) come from two threads. An upload holds it from its halt to its run, so gdb never sees it stopped."""
|
|
51
|
+
def run(self, *a, **k):
|
|
52
|
+
with self.lock:
|
|
53
|
+
return fn(self, *a, **k)
|
|
54
|
+
run.__name__, run.__doc__ = fn.__name__, fn.__doc__
|
|
55
|
+
return run
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Rp2040Debug:
|
|
59
|
+
"""An RP2040's core 0 on an OEP port (`oep://<probe>/<port>`), for the server. Opens the probe on `open()`."""
|
|
60
|
+
|
|
61
|
+
FLASH, FLASH_SIZE, SECTOR = 0x10000000, 2 * 1024 * 1024, 4096
|
|
62
|
+
RAM, RAM_SIZE = 0x20000000, 0x42000
|
|
63
|
+
FP_CTRL, FP_COMP0 = 0xE0002000, 0xE0002008
|
|
64
|
+
C_STEP = 4
|
|
65
|
+
|
|
66
|
+
def __init__(self, address: str, owner: str = "gdb"):
|
|
67
|
+
self.address, self.owner = address, owner
|
|
68
|
+
self.host = self.t = None
|
|
69
|
+
self.bp: dict[int, int] = {} # address -> comparator
|
|
70
|
+
self.lock = threading.RLock()
|
|
71
|
+
|
|
72
|
+
# session
|
|
73
|
+
def open(self) -> None:
|
|
74
|
+
from . import rp2040_swd as rp
|
|
75
|
+
unit, port = rp.parse(self.address)
|
|
76
|
+
self.host = rp.open_host(unit, self.owner)
|
|
77
|
+
self.t = rp.Target(self.host, port)
|
|
78
|
+
self.core, self.mem = self.t.core, self.t.mem
|
|
79
|
+
self.mem.write32(self.FP_CTRL, 3) # FPB on (KEY | ENABLE)
|
|
80
|
+
n = (self.mem.read32(self.FP_CTRL) >> 4) & 0xF
|
|
81
|
+
self.comparators = n or 4
|
|
82
|
+
for i in range(self.comparators):
|
|
83
|
+
self.mem.write32(self.FP_COMP0 + 4 * i, 0)
|
|
84
|
+
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
if self.t is None:
|
|
87
|
+
return
|
|
88
|
+
try:
|
|
89
|
+
for i in range(self.comparators):
|
|
90
|
+
self.mem.write32(self.FP_COMP0 + 4 * i, 0)
|
|
91
|
+
self.core.release()
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
try:
|
|
95
|
+
self.t.close()
|
|
96
|
+
finally:
|
|
97
|
+
try:
|
|
98
|
+
self.host.end()
|
|
99
|
+
finally:
|
|
100
|
+
self.host.link.close()
|
|
101
|
+
self.t = None
|
|
102
|
+
|
|
103
|
+
@_locked
|
|
104
|
+
def keepalive(self) -> None:
|
|
105
|
+
self.host.keepalive()
|
|
106
|
+
|
|
107
|
+
# run control
|
|
108
|
+
@_locked
|
|
109
|
+
def halted(self) -> bool:
|
|
110
|
+
return self.core.halted()
|
|
111
|
+
|
|
112
|
+
@_locked
|
|
113
|
+
def halt(self) -> None:
|
|
114
|
+
self.core.halt()
|
|
115
|
+
|
|
116
|
+
@_locked
|
|
117
|
+
def resume(self) -> None:
|
|
118
|
+
self.core.resume()
|
|
119
|
+
|
|
120
|
+
@_locked
|
|
121
|
+
def step(self) -> None:
|
|
122
|
+
k = self.core
|
|
123
|
+
k.mem.write32(k.DHCSR, k.KEY | k.C_DEBUGEN | k.C_HALT | k.C_MASKINTS)
|
|
124
|
+
k.mem.write32(k.DHCSR, k.KEY | k.C_DEBUGEN | k.C_MASKINTS | self.C_STEP)
|
|
125
|
+
k._wait(k.S_HALT, 1.0)
|
|
126
|
+
k.mem.write32(k.DHCSR, k.KEY | k.C_DEBUGEN | k.C_HALT)
|
|
127
|
+
|
|
128
|
+
@_locked
|
|
129
|
+
def reset_halt(self) -> None:
|
|
130
|
+
self.core.reset_halt()
|
|
131
|
+
|
|
132
|
+
@_locked
|
|
133
|
+
def reset_run(self) -> None:
|
|
134
|
+
"""The core reset (the boot ROM runs boot2 and the image again) and let run. Not the ROM's reboot: that
|
|
135
|
+
resets the whole chip and the debug link with it, and gdb's session would be lost."""
|
|
136
|
+
self.core.reset_halt()
|
|
137
|
+
self.core.resume()
|
|
138
|
+
|
|
139
|
+
# registers
|
|
140
|
+
@_locked
|
|
141
|
+
def read_registers(self) -> list[int]:
|
|
142
|
+
return [self.core.reg(n) for n in range(NREGS)]
|
|
143
|
+
|
|
144
|
+
@_locked
|
|
145
|
+
def write_register(self, n: int, value: int) -> None:
|
|
146
|
+
self.core.set_reg(n, value)
|
|
147
|
+
|
|
148
|
+
# memory
|
|
149
|
+
@_locked
|
|
150
|
+
def read_memory(self, address: int, length: int) -> bytes:
|
|
151
|
+
start, end = address & ~3, (address + length + 3) & ~3
|
|
152
|
+
words = self.mem.read_block(start, (end - start) // 4) if end > start else []
|
|
153
|
+
raw = struct.pack(f"<{len(words)}I", *words)
|
|
154
|
+
return raw[address - start:address - start + length]
|
|
155
|
+
|
|
156
|
+
@_locked
|
|
157
|
+
def write_memory(self, address: int, data: bytes) -> None:
|
|
158
|
+
start, end = address & ~3, (address + len(data) + 3) & ~3
|
|
159
|
+
buf = bytearray(self.read_memory(start, end - start)) if (start != address or end != address + len(data)) \
|
|
160
|
+
else bytearray(end - start)
|
|
161
|
+
buf[address - start:address - start + len(data)] = data
|
|
162
|
+
self.mem.write_block(start, list(struct.unpack(f"<{len(buf) // 4}I", bytes(buf))))
|
|
163
|
+
|
|
164
|
+
def memory_map(self) -> str:
|
|
165
|
+
return ('<?xml version="1.0"?>\n<!DOCTYPE memory-map PUBLIC "+//IDN gnu.org//DTD GDB Memory Map V1.0//EN" '
|
|
166
|
+
'"http://sourceware.org/gdb/gdb-memory-map.dtd">\n<memory-map>\n'
|
|
167
|
+
'<memory type="rom" start="0x0" length="0x4000"/>\n'
|
|
168
|
+
f'<memory type="flash" start="{self.FLASH:#x}" length="{self.FLASH_SIZE:#x}">'
|
|
169
|
+
f'<property name="blocksize">{self.SECTOR:#x}</property></memory>\n'
|
|
170
|
+
f'<memory type="ram" start="{self.RAM:#x}" length="{self.RAM_SIZE:#x}"/>\n'
|
|
171
|
+
'<memory type="ram" start="0x40000000" length="0x20000000"/>\n'
|
|
172
|
+
'<memory type="ram" start="0xd0000000" length="0x10000000"/>\n'
|
|
173
|
+
'<memory type="ram" start="0xe0000000" length="0x10000000"/>\n'
|
|
174
|
+
'</memory-map>\n')
|
|
175
|
+
|
|
176
|
+
@_locked
|
|
177
|
+
def upload(self, image: bytes, log: Callable[[str], None]) -> None:
|
|
178
|
+
"""A whole flash image from the flash's start (the IDE's Upload while gdb is connected), then run it."""
|
|
179
|
+
self.core.halt()
|
|
180
|
+
self.core.reset_halt()
|
|
181
|
+
self.write_flash(self.FLASH, image, log)
|
|
182
|
+
self.reset_run()
|
|
183
|
+
|
|
184
|
+
@_locked
|
|
185
|
+
def write_flash(self, address: int, image: bytes, log: Callable[[str], None]) -> None:
|
|
186
|
+
from . import rp2040
|
|
187
|
+
if not rp2040.program_and_verify(self.core, image, address - self.FLASH, log=log):
|
|
188
|
+
raise RuntimeError("the flash did not read back as written")
|
|
189
|
+
|
|
190
|
+
# breakpoints (the FPB, v1: an address below 0x20000000, the half-word picked by REPLACE)
|
|
191
|
+
@_locked
|
|
192
|
+
def set_breakpoint(self, address: int) -> bool:
|
|
193
|
+
if address in self.bp:
|
|
194
|
+
return True
|
|
195
|
+
if address >= 0x20000000:
|
|
196
|
+
return False
|
|
197
|
+
free = [i for i in range(self.comparators) if i not in self.bp.values()]
|
|
198
|
+
if not free:
|
|
199
|
+
return False
|
|
200
|
+
i = free[0]
|
|
201
|
+
replace = 2 if address & 2 else 1
|
|
202
|
+
self.mem.write32(self.FP_COMP0 + 4 * i, (replace << 30) | (address & 0x1FFFFFFC) | 1)
|
|
203
|
+
self.bp[address] = i
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
@_locked
|
|
207
|
+
def clear_breakpoint(self, address: int) -> bool:
|
|
208
|
+
i = self.bp.pop(address, None)
|
|
209
|
+
if i is None:
|
|
210
|
+
return False
|
|
211
|
+
self.mem.write32(self.FP_COMP0 + 4 * i, 0)
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---- the protocol ------------------------------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def checksum(body: bytes) -> bytes:
|
|
218
|
+
return b"%02x" % (sum(body) & 0xFF)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def frame(body: bytes) -> bytes:
|
|
222
|
+
return b"$" + body + b"#" + checksum(body)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def unescape(data: bytes) -> bytes:
|
|
226
|
+
out, i = bytearray(), 0
|
|
227
|
+
while i < len(data):
|
|
228
|
+
c = data[i]
|
|
229
|
+
if c == 0x7D and i + 1 < len(data):
|
|
230
|
+
out.append(data[i + 1] ^ 0x20)
|
|
231
|
+
i += 2
|
|
232
|
+
else:
|
|
233
|
+
out.append(c)
|
|
234
|
+
i += 1
|
|
235
|
+
return bytes(out)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _hex32(v: int) -> bytes:
|
|
239
|
+
return binascii.hexlify(struct.pack("<I", v & 0xFFFFFFFF))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class Session:
|
|
243
|
+
"""One gdb connection: reads packets, answers them, runs the target."""
|
|
244
|
+
|
|
245
|
+
def __init__(self, conn: socket.socket, target, log: Callable[[str], None] = print):
|
|
246
|
+
self.conn, self.target, self.log = conn, target, log
|
|
247
|
+
self.ack = True
|
|
248
|
+
self.buf = b""
|
|
249
|
+
self.flash: dict[int, bytearray] = {} # vFlashWrite: start -> contiguous bytes
|
|
250
|
+
self.last_io = time.monotonic()
|
|
251
|
+
|
|
252
|
+
# transport
|
|
253
|
+
def _recv(self, timeout: float | None) -> bytes:
|
|
254
|
+
r, _, _ = select.select([self.conn], [], [], timeout)
|
|
255
|
+
if not r:
|
|
256
|
+
return b""
|
|
257
|
+
data = self.conn.recv(65536)
|
|
258
|
+
if not data:
|
|
259
|
+
raise ConnectionError("gdb closed")
|
|
260
|
+
return data
|
|
261
|
+
|
|
262
|
+
def next_packet(self) -> bytes | None:
|
|
263
|
+
"""The next packet's body (b"\\x03" for an interrupt); None after KEEPALIVE_S of silence."""
|
|
264
|
+
while True:
|
|
265
|
+
while self.buf and self.buf[:1] in (b"+", b"-"):
|
|
266
|
+
self.buf = self.buf[1:]
|
|
267
|
+
if self.buf[:1] == b"\x03":
|
|
268
|
+
self.buf = self.buf[1:]
|
|
269
|
+
return b"\x03"
|
|
270
|
+
start = self.buf.find(b"$")
|
|
271
|
+
if start >= 0:
|
|
272
|
+
end = self.buf.find(b"#", start)
|
|
273
|
+
if end >= 0 and len(self.buf) >= end + 3:
|
|
274
|
+
body, cs = self.buf[start + 1:end], self.buf[end + 1:end + 3]
|
|
275
|
+
self.buf = self.buf[end + 3:]
|
|
276
|
+
if self.ack:
|
|
277
|
+
self.conn.sendall(b"+" if checksum(body) == cs.lower() else b"-")
|
|
278
|
+
return body
|
|
279
|
+
elif self.buf and self.buf[:1] not in (b"$", b"+", b"-", b"\x03"):
|
|
280
|
+
self.buf = self.buf[1:]
|
|
281
|
+
continue
|
|
282
|
+
data = self._recv(KEEPALIVE_S)
|
|
283
|
+
if not data:
|
|
284
|
+
return None
|
|
285
|
+
self.buf += data
|
|
286
|
+
|
|
287
|
+
def send(self, body: bytes | str) -> None:
|
|
288
|
+
if isinstance(body, str):
|
|
289
|
+
body = body.encode()
|
|
290
|
+
self.conn.sendall(frame(body)) # gdb's '+' is skipped by the next read
|
|
291
|
+
|
|
292
|
+
# the loop
|
|
293
|
+
def serve(self) -> None:
|
|
294
|
+
while True:
|
|
295
|
+
body = self.next_packet()
|
|
296
|
+
if body is None:
|
|
297
|
+
self.target.keepalive()
|
|
298
|
+
continue
|
|
299
|
+
if body == b"\x03":
|
|
300
|
+
self.target.halt()
|
|
301
|
+
self.send("T02")
|
|
302
|
+
continue
|
|
303
|
+
reply = self.handle(body)
|
|
304
|
+
if reply is not None:
|
|
305
|
+
self.send(reply)
|
|
306
|
+
if body[:1] in (b"k", b"D"):
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
def wait_halt(self) -> str:
|
|
310
|
+
"""Run until the core halts or gdb interrupts."""
|
|
311
|
+
while True:
|
|
312
|
+
if self.target.halted():
|
|
313
|
+
return "T05"
|
|
314
|
+
data = self._recv(POLL)
|
|
315
|
+
if data:
|
|
316
|
+
self.buf += data
|
|
317
|
+
if b"\x03" in self.buf:
|
|
318
|
+
self.buf = self.buf.replace(b"\x03", b"", 1)
|
|
319
|
+
self.target.halt()
|
|
320
|
+
return "T02"
|
|
321
|
+
|
|
322
|
+
def handle(self, body: bytes) -> str | bytes | None:
|
|
323
|
+
t = self.target
|
|
324
|
+
cmd = body[:1]
|
|
325
|
+
try:
|
|
326
|
+
if body.startswith(b"qSupported"):
|
|
327
|
+
return "PacketSize=4000;QStartNoAckMode+;qXfer:features:read+;qXfer:memory-map:read+;vContSupported+"
|
|
328
|
+
if body == b"QStartNoAckMode":
|
|
329
|
+
self.send("OK")
|
|
330
|
+
self.ack = False
|
|
331
|
+
return None
|
|
332
|
+
if body.startswith(b"qXfer:features:read:target.xml:"):
|
|
333
|
+
return self._xfer(TARGET_XML, body)
|
|
334
|
+
if body.startswith(b"qXfer:memory-map:read::"):
|
|
335
|
+
return self._xfer(t.memory_map(), body)
|
|
336
|
+
if body == b"qAttached":
|
|
337
|
+
return "1"
|
|
338
|
+
if body == b"qC":
|
|
339
|
+
return "QC1"
|
|
340
|
+
if body == b"qfThreadInfo":
|
|
341
|
+
return "m1"
|
|
342
|
+
if body == b"qsThreadInfo":
|
|
343
|
+
return "l"
|
|
344
|
+
if body.startswith(b"qRcmd,"):
|
|
345
|
+
return self._monitor(binascii.unhexlify(body[6:]).decode(errors="replace").strip())
|
|
346
|
+
if cmd == b"?":
|
|
347
|
+
return "T05" if t.halted() else self._stop_then("T02")
|
|
348
|
+
if cmd == b"H" or body.startswith(b"T"):
|
|
349
|
+
return "OK"
|
|
350
|
+
if cmd == b"g":
|
|
351
|
+
return b"".join(_hex32(v) for v in t.read_registers())
|
|
352
|
+
if cmd == b"G":
|
|
353
|
+
raw = binascii.unhexlify(body[1:])
|
|
354
|
+
for n in range(min(NREGS, len(raw) // 4)):
|
|
355
|
+
t.write_register(n, struct.unpack_from("<I", raw, 4 * n)[0])
|
|
356
|
+
return "OK"
|
|
357
|
+
if cmd == b"p":
|
|
358
|
+
n = int(body[1:], 16)
|
|
359
|
+
return _hex32(t.read_registers()[n]) if n < NREGS else "E01"
|
|
360
|
+
if cmd == b"P":
|
|
361
|
+
n, v = body[1:].split(b"=")
|
|
362
|
+
t.write_register(int(n, 16), struct.unpack("<I", binascii.unhexlify(v))[0])
|
|
363
|
+
return "OK"
|
|
364
|
+
if cmd == b"m":
|
|
365
|
+
a, n = (int(x, 16) for x in body[1:].split(b","))
|
|
366
|
+
return binascii.hexlify(t.read_memory(a, n))
|
|
367
|
+
if cmd == b"M":
|
|
368
|
+
head, data = body[1:].split(b":", 1)
|
|
369
|
+
a, _ = (int(x, 16) for x in head.split(b","))
|
|
370
|
+
t.write_memory(a, binascii.unhexlify(data))
|
|
371
|
+
return "OK"
|
|
372
|
+
if cmd == b"X":
|
|
373
|
+
head, data = body[1:].split(b":", 1)
|
|
374
|
+
a, n = (int(x, 16) for x in head.split(b","))
|
|
375
|
+
if n:
|
|
376
|
+
t.write_memory(a, unescape(data))
|
|
377
|
+
return "OK"
|
|
378
|
+
if body == b"vCont?":
|
|
379
|
+
return "vCont;c;C;s;S;t"
|
|
380
|
+
if body.startswith(b"vCont;"):
|
|
381
|
+
action = body[6:7]
|
|
382
|
+
if action in (b"s", b"S"):
|
|
383
|
+
t.step()
|
|
384
|
+
return "T05"
|
|
385
|
+
if action == b"t":
|
|
386
|
+
t.halt()
|
|
387
|
+
return "T00"
|
|
388
|
+
t.resume()
|
|
389
|
+
return self.wait_halt()
|
|
390
|
+
if cmd in (b"c", b"C"):
|
|
391
|
+
t.resume()
|
|
392
|
+
return self.wait_halt()
|
|
393
|
+
if cmd in (b"s", b"S"):
|
|
394
|
+
t.step()
|
|
395
|
+
return "T05"
|
|
396
|
+
if cmd in (b"Z", b"z") and body[1:2] in (b"0", b"1"):
|
|
397
|
+
a = int(body[3:].split(b",")[0], 16)
|
|
398
|
+
ok = t.set_breakpoint(a) if cmd == b"Z" else t.clear_breakpoint(a)
|
|
399
|
+
return "OK" if ok else "E01"
|
|
400
|
+
if body.startswith(b"vFlashErase:"):
|
|
401
|
+
return "OK" # the write erases what it covers
|
|
402
|
+
if body.startswith(b"vFlashWrite:"):
|
|
403
|
+
head, data = body[12:].split(b":", 1)
|
|
404
|
+
self._flash_put(int(head, 16), unescape(data))
|
|
405
|
+
return "OK"
|
|
406
|
+
if body == b"vFlashDone":
|
|
407
|
+
return self._flash_done()
|
|
408
|
+
if body == b"vMustReplyEmpty" or body.startswith(b"vKill"):
|
|
409
|
+
return "" if body == b"vMustReplyEmpty" else "OK"
|
|
410
|
+
if cmd == b"D":
|
|
411
|
+
t.resume()
|
|
412
|
+
return "OK"
|
|
413
|
+
if cmd == b"k":
|
|
414
|
+
t.resume()
|
|
415
|
+
return None
|
|
416
|
+
return ""
|
|
417
|
+
except (ValueError, IndexError):
|
|
418
|
+
return "E01"
|
|
419
|
+
except TimeoutError as e:
|
|
420
|
+
self.log(f"gdb: {body[:40]!r}: {e}")
|
|
421
|
+
return "E02"
|
|
422
|
+
|
|
423
|
+
def _stop_then(self, reply: str) -> str:
|
|
424
|
+
self.target.halt()
|
|
425
|
+
return reply
|
|
426
|
+
|
|
427
|
+
def _xfer(self, text: str, body: bytes) -> str:
|
|
428
|
+
offset, length = (int(x, 16) for x in body.rsplit(b":", 1)[1].split(b","))
|
|
429
|
+
data = text[offset:offset + length]
|
|
430
|
+
return ("l" if offset + length >= len(text) else "m") + data
|
|
431
|
+
|
|
432
|
+
def _monitor(self, line: str) -> str:
|
|
433
|
+
t = self.target
|
|
434
|
+
words = line.split()
|
|
435
|
+
if words in (["reset", "halt"], ["reset", "init"]):
|
|
436
|
+
t.reset_halt()
|
|
437
|
+
msg = "core reset and halted\n"
|
|
438
|
+
elif words in (["reset"], ["reset", "run"]):
|
|
439
|
+
t.reset_run()
|
|
440
|
+
msg = "core reset, running\n"
|
|
441
|
+
elif words == ["halt"]:
|
|
442
|
+
t.halt()
|
|
443
|
+
msg = "core halted\n"
|
|
444
|
+
elif words in (["resume"], ["go"]):
|
|
445
|
+
t.resume()
|
|
446
|
+
msg = "core running\n"
|
|
447
|
+
else:
|
|
448
|
+
msg = f"unknown monitor command: {line}\n"
|
|
449
|
+
self.send("O" + binascii.hexlify(msg.encode()).decode())
|
|
450
|
+
return "OK"
|
|
451
|
+
|
|
452
|
+
def _flash_put(self, address: int, data: bytes) -> None:
|
|
453
|
+
for start, buf in self.flash.items():
|
|
454
|
+
if start + len(buf) == address:
|
|
455
|
+
buf += data
|
|
456
|
+
return
|
|
457
|
+
self.flash[address] = bytearray(data)
|
|
458
|
+
|
|
459
|
+
def _flash_done(self) -> str:
|
|
460
|
+
"""The pieces of this load as one image from the first piece's erase block to the last byte, the gaps 0xFF
|
|
461
|
+
(gdb erases whole blocks; its sections can leave a few bytes between them): one write of the chip's routine."""
|
|
462
|
+
if not self.flash:
|
|
463
|
+
return "OK"
|
|
464
|
+
try:
|
|
465
|
+
block = getattr(self.target, "SECTOR", 4096)
|
|
466
|
+
start = min(self.flash) & ~(block - 1)
|
|
467
|
+
end = max(a + len(b) for a, b in self.flash.items())
|
|
468
|
+
image = bytearray(b"\xff" * (end - start))
|
|
469
|
+
for a, b in self.flash.items():
|
|
470
|
+
image[a - start:a - start + len(b)] = b
|
|
471
|
+
self.log(f"gdb: writing {len(image)} B of flash at {start:#010x}")
|
|
472
|
+
self.target.write_flash(start, bytes(image), self.log)
|
|
473
|
+
except Exception as e:
|
|
474
|
+
self.log(f"gdb: flash write failed: {e}")
|
|
475
|
+
return "E03"
|
|
476
|
+
finally:
|
|
477
|
+
self.flash.clear()
|
|
478
|
+
return "OK"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class Server:
|
|
482
|
+
"""Listens on a TCP port; each gdb connection opens the target, is served, and closes it."""
|
|
483
|
+
|
|
484
|
+
def __init__(self, make_target: Callable[[], object], port: int = 3333, bind: str = "127.0.0.1",
|
|
485
|
+
log: Callable[[str], None] = print):
|
|
486
|
+
self.make_target, self.log = make_target, log
|
|
487
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
488
|
+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
489
|
+
self.sock.bind((bind, port))
|
|
490
|
+
self.sock.listen(1)
|
|
491
|
+
self.port = self.sock.getsockname()[1]
|
|
492
|
+
self.lock = threading.Lock() # one gdb at a time
|
|
493
|
+
|
|
494
|
+
def serve_forever(self, stop: threading.Event | None = None) -> None:
|
|
495
|
+
self.sock.settimeout(0.5)
|
|
496
|
+
while not (stop and stop.is_set()):
|
|
497
|
+
try:
|
|
498
|
+
conn, peer = self.sock.accept()
|
|
499
|
+
except socket.timeout:
|
|
500
|
+
continue
|
|
501
|
+
except OSError:
|
|
502
|
+
return
|
|
503
|
+
threading.Thread(target=self._client, args=(conn, peer), daemon=True).start()
|
|
504
|
+
|
|
505
|
+
def _client(self, conn: socket.socket, peer) -> None:
|
|
506
|
+
if not self.lock.acquire(blocking=False):
|
|
507
|
+
conn.close()
|
|
508
|
+
return
|
|
509
|
+
target = None
|
|
510
|
+
try:
|
|
511
|
+
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
512
|
+
target = self.make_target()
|
|
513
|
+
target.open()
|
|
514
|
+
self.log(f"gdb: {peer[0]}:{peer[1]} connected")
|
|
515
|
+
Session(conn, target, self.log).serve()
|
|
516
|
+
except ConnectionError:
|
|
517
|
+
pass
|
|
518
|
+
except Exception as e:
|
|
519
|
+
self.log(f"gdb: session ended: {e}")
|
|
520
|
+
finally:
|
|
521
|
+
if target is not None:
|
|
522
|
+
target.close()
|
|
523
|
+
conn.close()
|
|
524
|
+
self.lock.release()
|
|
525
|
+
self.log("gdb: disconnected")
|
|
526
|
+
|
|
527
|
+
def close(self) -> None:
|
|
528
|
+
self.sock.close()
|