Skip to content

Commit 6cdcd21

Browse files
committed
WIP GDB Server support. Still broken.
1 parent 731b32f commit 6cdcd21

File tree

6 files changed

+315
-8
lines changed

6 files changed

+315
-8
lines changed

pyboy/plugins/__init__.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,19 @@
88

99
__pdoc__ = {
1010
# docs exclude
11-
"disable_input": False,
12-
"rewind": False,
11+
"screen_recorder": False,
1312
"window_sdl2": False,
14-
"screenshot_recorder": False,
1513
"debug_prompt": False,
16-
"screen_recorder": False,
1714
"debug": False,
1815
"manager": False,
19-
"record_replay": False,
20-
"manager_gen": False,
2116
"window_open_gl": False,
17+
"screenshot_recorder": False,
18+
"rewind": False,
2219
"auto_pause": False,
20+
"record_replay": False,
21+
"manager_gen": False,
2322
"window_null": False,
23+
"gdb_server": False,
24+
"disable_input": False,
2425
# docs exclude end
2526
}

pyboy/plugins/gdb_server.pxd

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#
2+
# License: See LICENSE.md file
3+
# GitHub: https://github.com/Baekalfen/PyBoy
4+
#
5+
6+
from pyboy.logging.logging cimport Logger
7+
from pyboy.plugins.base_plugin cimport PyBoyPlugin
8+
9+
10+
cdef Logger logger
11+
12+
cdef class GdbServer(PyBoyPlugin):
13+
cdef object sock
14+
cdef object client_socket
15+
cdef object client_address
16+
cdef object buffer
17+
cdef bint freeze

pyboy/plugins/gdb_server.py

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#
2+
# License: See LICENSE.md file
3+
# GitHub: https://github.com/Baekalfen/PyBoy
4+
#
5+
6+
import fcntl
7+
import os
8+
import re
9+
import socket
10+
11+
import pyboy
12+
from pyboy.plugins.base_plugin import PyBoyPlugin
13+
from pyboy.utils import WindowEvent
14+
15+
logger = pyboy.logging.get_logger(__name__)
16+
17+
####################################################
18+
#
19+
# A big Thank You to "chciken" for the extraordinary work of writing
20+
# a blog post on exactly this topic:
21+
#
22+
# https://www.chciken.com/tlmboy/2022/04/03/gdb-z80.html
23+
#
24+
####################################################
25+
26+
27+
class GdbServer(PyBoyPlugin):
28+
argv = [(
29+
"--gdbserver", {
30+
"nargs": "?",
31+
"default": None,
32+
"const": "127.0.0.1:1234",
33+
"type": str,
34+
"help": "Spawn GDB Server for debugging"
35+
}
36+
)]
37+
38+
def __init__(self, pyboy, mb, pyboy_argv):
39+
super().__init__(pyboy, mb, pyboy_argv)
40+
41+
if not self.enabled():
42+
return
43+
44+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
45+
# TODO: Argv ip, port
46+
address, port = pyboy_argv.get("gdbserver").split(":", 1)
47+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
48+
# fcntl.fcntl(self.sock, fcntl.F_SETFL, os.O_NONBLOCK)
49+
self.sock.bind((address, int(port)))
50+
self.sock.listen(1)
51+
logger.critical("Waiting for GDB client to connect on %s...", self.pyboy_argv.get("gdbserver"))
52+
(self.client_socket, self.client_address) = self.sock.accept()
53+
self.buffer = b""
54+
# self.pyboy._pause()
55+
self.freeze = True
56+
self.client_socket.setblocking(False)
57+
self._message_handler()
58+
59+
def enabled(self):
60+
return self.pyboy_argv.get("gdbserver")
61+
62+
def _gdb_checksum(self, data):
63+
return sum(data) & 0xff
64+
65+
def _gdb_send(self, data):
66+
msg = b"$" + data + b"#" + f"{self._gdb_checksum(data):02x}".encode()
67+
logger.debug("Sending message: %s", msg)
68+
self.client_socket.send(msg)
69+
70+
def _gdb_ack(self):
71+
logger.debug("Sending ack")
72+
self.client_socket.send(b"+")
73+
74+
re_ack = b"([\+\-])"
75+
re_command = b"\$(.*?)#([0-9a-fA-F]+)"
76+
re_signal = b"([\x01\x02\x03\x04\x05\x06\x07\x08\x09])"
77+
78+
def _gdb_recv_packages(self):
79+
logger.debug("Receiving data...")
80+
try:
81+
data = self.client_socket.recv(4096)
82+
except BlockingIOError:
83+
return []
84+
85+
if data:
86+
logger.debug("Received data: %s", data)
87+
88+
self.buffer += data
89+
90+
matches = []
91+
while True:
92+
for t, r in [("ack", self.re_ack), ("cmd", self.re_command), ("sig", self.re_signal)]:
93+
m = re.match(b"^" + r, self.buffer)
94+
95+
if not m:
96+
continue
97+
98+
matches.append((t, m.groups()))
99+
100+
# Consume used part of buffer
101+
self.buffer = self.buffer[m.span()[1]:]
102+
else:
103+
# No more matches to make, break out of while
104+
break
105+
106+
return matches
107+
108+
def _format_little_endian(self, _in):
109+
return b"".join(f"{x:02X}".encode() for x in bytearray.fromhex(f"{_in:04X}")[::-1])
110+
111+
def post_tick(self):
112+
self._message_handler()
113+
114+
def _message_handler(self):
115+
while True:
116+
for _type, contents in self._gdb_recv_packages():
117+
if _type == "ack":
118+
logger.debug("Received Ack: %s", contents)
119+
elif _type == "sig":
120+
logger.info("Sig: %s", contents)
121+
self._gdb_ack()
122+
# self.pyboy._pause()
123+
self.freeze = True
124+
# self._gdb_send(b"OK")
125+
self._gdb_send(b"S05")
126+
elif _type == "cmd":
127+
logger.debug("Command: %s", contents)
128+
body, checksum = contents
129+
logger.info("Received message: %s", body.decode())
130+
if not self._gdb_checksum(body) == int(checksum, 16):
131+
logger.critical("Checksum on package failed: %s", contents)
132+
exit(1)
133+
134+
self._gdb_ack()
135+
136+
# Command...
137+
# \$(.*?)#([0-9a-fA-F]+)
138+
139+
if body.startswith(b"qSupported"):
140+
# _, _sub_bodies = body.split(b':', 1)
141+
# sub_bodies = _sub_bodies.split(b';')
142+
# self._gdb_send(b"swbreak+;") # vContSupported+;
143+
self._gdb_send(b"hwbreak+;")
144+
elif body == b"vMustReplyEmpty":
145+
self._gdb_send(b"")
146+
elif body.startswith(b"Hg"):
147+
self._gdb_send(b"")
148+
elif body == b"qTStatus":
149+
self._gdb_send(b"")
150+
elif body == b"qfThreadInfo":
151+
self._gdb_send(b"")
152+
elif body == b"qL1160000000000000000":
153+
self._gdb_send(b"")
154+
elif body == b"qL1200000000000000000":
155+
self._gdb_send(b"")
156+
elif body == b"Hc-1" or body == b"Hc0":
157+
self._gdb_send(b"")
158+
elif body == b"qC":
159+
self._gdb_send(b"")
160+
elif body == b"?":
161+
# Reason for pausing
162+
self._gdb_send(b"S05")
163+
elif body == b"qAttached":
164+
# Keep alive after GDB closes?
165+
self._gdb_send(b"0")
166+
elif body == b"c":
167+
self.freeze = False
168+
# self.pyboy._unpause()
169+
elif body == b"g":
170+
# Registers as 16-bit little endian padded with x to number of registers in Z80
171+
# AF, BC, DE, HL, SP, PC
172+
msg = (
173+
f"{self.pyboy.mb.cpu.F:02x}"
174+
f"{self.pyboy.mb.cpu.A:02x}"
175+
f"{self.pyboy.mb.cpu.C:02x}"
176+
f"{self.pyboy.mb.cpu.B:02x}"
177+
f"{self.pyboy.mb.cpu.E:02x}"
178+
f"{self.pyboy.mb.cpu.D:02x}"
179+
).encode()
180+
181+
msg += ( \
182+
self._format_little_endian(self.pyboy.mb.cpu.HL) + \
183+
self._format_little_endian(self.pyboy.mb.cpu.SP) + \
184+
self._format_little_endian(self.pyboy.mb.cpu.PC) \
185+
)
186+
187+
msg += b"xx" * 14
188+
189+
self._gdb_send(msg)
190+
elif body.startswith(b"m"):
191+
# Memory
192+
_addr, _length = body[1:].split(b",", 1)
193+
addr = int(_addr, 16)
194+
length = int(_length, 16)
195+
if addr > 0xFFFF:
196+
self._gdb_send(b"E 01")
197+
else:
198+
# From GDB docs:
199+
# "The reply may contain fewer addressable memory units than requested if the server was able
200+
# to read only part of the region of memory."
201+
self._gdb_send(
202+
"".join(
203+
f"{self.pyboy.get_memory_value(a):02x}"
204+
for a in range(addr, min(addr + length, 0x10000))
205+
).encode()
206+
)
207+
elif body.startswith(b"Z"):
208+
# Add breakpoint
209+
_type, _addr, kind = body.split(b",", 2)
210+
addr = int(_addr, 16)
211+
bank = -1
212+
self.pyboy.mb.breakpoint_add(bank, addr)
213+
self._gdb_send(b"OK")
214+
elif body.startswith(b"z"):
215+
# Remove breakpoint
216+
_type, _addr, kind = body.split(b",", 2)
217+
addr = int(_addr, 16)
218+
bank = -1
219+
brk_index = self.pyboy.mb.breakpoint_find(bank, addr)
220+
if brk_index < 0:
221+
breakpoint()
222+
self._gdb_send(b"E 01")
223+
else:
224+
self.pyboy.mb.breakpoint_remove(brk_index)
225+
self._gdb_send(b"OK")
226+
elif body.startswith(b"vCont?"):
227+
# self.pyboy._unpause()
228+
# self.pyboy.mb.breakpoint_singlestep = 0
229+
# self.freeze = False
230+
# self.pyboy.mb.breakpoint_singlestep_latch = 0
231+
self._gdb_send(b"vCont;c;s;t")
232+
# return True
233+
elif body.startswith(b"vCont"):
234+
# self.pyboy._unpause()
235+
# self.pyboy.mb.breakpoint_singlestep = 0
236+
self.freeze = False
237+
self.pyboy.mb.breakpoint_singlestep_latch = 0
238+
self._gdb_send(b"OK")
239+
# return True
240+
# elif body == b"vContC":
241+
# # self.pyboy._pause()
242+
# self.freeze = True
243+
# self._gdb_send(b"OK")
244+
elif body.startswith(b"vKill"):
245+
self.pyboy.stop()
246+
self._gdb_send(b"OK")
247+
elif body == b"qSymbol::":
248+
self._gdb_send(b"OK")
249+
# return True
250+
else:
251+
breakpoint()
252+
else:
253+
breakpoint()
254+
255+
if not self.freeze:
256+
break
257+
258+
def handle_breakpoint(self):
259+
# if not self.pyboy.paused:
260+
logger.critical(
261+
f"GDB server handle_breakpoint HL: {self.pyboy.mb.cpu.HL:04X}, SP: {self.pyboy.mb.cpu.SP:04X}, PC: {self.pyboy.mb.cpu.PC:04X}"
262+
)
263+
# self.pyboy._pause()
264+
self._gdb_send(b"S05")
265+
266+
# self.client_socket.setblocking(True)
267+
self.freeze = True
268+
self._message_handler()
269+
270+
# breakpoint()
271+
pass

pyboy/plugins/manager.pxd

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ from pyboy.plugins.rewind cimport Rewind
1818
from pyboy.plugins.screen_recorder cimport ScreenRecorder
1919
from pyboy.plugins.screenshot_recorder cimport ScreenshotRecorder
2020
from pyboy.plugins.debug_prompt cimport DebugPrompt
21+
from pyboy.plugins.gdb_server cimport GdbServer
2122
from pyboy.plugins.game_wrapper_super_mario_land cimport GameWrapperSuperMarioLand
2223
from pyboy.plugins.game_wrapper_tetris cimport GameWrapperTetris
2324
from pyboy.plugins.game_wrapper_kirby_dream_land cimport GameWrapperKirbyDreamLand
@@ -42,6 +43,7 @@ cdef class PluginManager:
4243
cdef public ScreenRecorder screen_recorder
4344
cdef public ScreenshotRecorder screenshot_recorder
4445
cdef public DebugPrompt debug_prompt
46+
cdef public GdbServer gdb_server
4547
cdef public GameWrapperSuperMarioLand game_wrapper_super_mario_land
4648
cdef public GameWrapperTetris game_wrapper_tetris
4749
cdef public GameWrapperKirbyDreamLand game_wrapper_kirby_dream_land
@@ -57,6 +59,7 @@ cdef class PluginManager:
5759
cdef bint screen_recorder_enabled
5860
cdef bint screenshot_recorder_enabled
5961
cdef bint debug_prompt_enabled
62+
cdef bint gdb_server_enabled
6063
cdef bint game_wrapper_super_mario_land_enabled
6164
cdef bint game_wrapper_tetris_enabled
6265
cdef bint game_wrapper_kirby_dream_land_enabled

0 commit comments

Comments
 (0)