dbxdebug 0.2.1__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.
- dbxdebug/__init__.py +134 -0
- dbxdebug/capture_io.py +210 -0
- dbxdebug/cli.py +618 -0
- dbxdebug/dbx_kbd.py +392 -0
- dbxdebug/gdb.py +297 -0
- dbxdebug/html.py +559 -0
- dbxdebug/keyboard.py +205 -0
- dbxdebug/qmp.py +239 -0
- dbxdebug/utils.py +92 -0
- dbxdebug/video.py +239 -0
- dbxdebug-0.2.1.dist-info/METADATA +164 -0
- dbxdebug-0.2.1.dist-info/RECORD +14 -0
- dbxdebug-0.2.1.dist-info/WHEEL +4 -0
- dbxdebug-0.2.1.dist-info/entry_points.txt +2 -0
dbxdebug/dbx_kbd.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DOSBox-X keyboard key codes and QMP QKeyCode mapping.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- DBX_KEY: Enum of DOSBox-X internal key codes
|
|
6
|
+
- DBX_KEY_TO_QCODE: Mapping from DBX_KEY to QMP qcode strings
|
|
7
|
+
- QCODE_TO_DBX_KEY: Reverse mapping from qcode strings to DBX_KEY
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from enum import IntEnum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DBX_KEY(IntEnum):
|
|
14
|
+
"""DOSBox-X internal keyboard key codes."""
|
|
15
|
+
|
|
16
|
+
KBD_NONE = 0x00
|
|
17
|
+
KBD_1 = 0x01
|
|
18
|
+
KBD_2 = 0x02
|
|
19
|
+
KBD_3 = 0x03
|
|
20
|
+
KBD_4 = 0x04
|
|
21
|
+
KBD_5 = 0x05
|
|
22
|
+
KBD_6 = 0x06
|
|
23
|
+
KBD_7 = 0x07
|
|
24
|
+
KBD_8 = 0x08
|
|
25
|
+
KBD_9 = 0x09
|
|
26
|
+
KBD_0 = 0x0A
|
|
27
|
+
KBD_q = 0x0B
|
|
28
|
+
KBD_w = 0x0C
|
|
29
|
+
KBD_e = 0x0D
|
|
30
|
+
KBD_r = 0x0E
|
|
31
|
+
KBD_t = 0x0F
|
|
32
|
+
KBD_y = 0x10
|
|
33
|
+
KBD_u = 0x11
|
|
34
|
+
KBD_i = 0x12
|
|
35
|
+
KBD_o = 0x13
|
|
36
|
+
KBD_p = 0x14
|
|
37
|
+
KBD_a = 0x15
|
|
38
|
+
KBD_s = 0x16
|
|
39
|
+
KBD_d = 0x17
|
|
40
|
+
KBD_f = 0x18
|
|
41
|
+
KBD_g = 0x19
|
|
42
|
+
KBD_h = 0x1A
|
|
43
|
+
KBD_j = 0x1B
|
|
44
|
+
KBD_k = 0x1C
|
|
45
|
+
KBD_l = 0x1D
|
|
46
|
+
KBD_z = 0x1E
|
|
47
|
+
KBD_x = 0x1F
|
|
48
|
+
KBD_c = 0x20
|
|
49
|
+
KBD_v = 0x21
|
|
50
|
+
KBD_b = 0x22
|
|
51
|
+
KBD_n = 0x23
|
|
52
|
+
KBD_m = 0x24
|
|
53
|
+
KBD_f1 = 0x25
|
|
54
|
+
KBD_f2 = 0x26
|
|
55
|
+
KBD_f3 = 0x27
|
|
56
|
+
KBD_f4 = 0x28
|
|
57
|
+
KBD_f5 = 0x29
|
|
58
|
+
KBD_f6 = 0x2A
|
|
59
|
+
KBD_f7 = 0x2B
|
|
60
|
+
KBD_f8 = 0x2C
|
|
61
|
+
KBD_f9 = 0x2D
|
|
62
|
+
KBD_f10 = 0x2E
|
|
63
|
+
KBD_f11 = 0x2F
|
|
64
|
+
KBD_f12 = 0x30
|
|
65
|
+
KBD_esc = 0x31
|
|
66
|
+
KBD_tab = 0x32
|
|
67
|
+
KBD_backspace = 0x33
|
|
68
|
+
KBD_enter = 0x34
|
|
69
|
+
KBD_space = 0x35
|
|
70
|
+
KBD_leftalt = 0x36
|
|
71
|
+
KBD_rightalt = 0x37
|
|
72
|
+
KBD_leftctrl = 0x38
|
|
73
|
+
KBD_rightctrl = 0x39
|
|
74
|
+
KBD_leftshift = 0x3A
|
|
75
|
+
KBD_rightshift = 0x3B
|
|
76
|
+
KBD_capslock = 0x3C
|
|
77
|
+
KBD_scrolllock = 0x3D
|
|
78
|
+
KBD_numlock = 0x3E
|
|
79
|
+
KBD_grave = 0x3F
|
|
80
|
+
KBD_minus = 0x40
|
|
81
|
+
KBD_equals = 0x41
|
|
82
|
+
KBD_backslash = 0x42
|
|
83
|
+
KBD_leftbracket = 0x43
|
|
84
|
+
KBD_rightbracket = 0x44
|
|
85
|
+
KBD_semicolon = 0x45
|
|
86
|
+
KBD_quote = 0x46
|
|
87
|
+
KBD_period = 0x47
|
|
88
|
+
KBD_comma = 0x48
|
|
89
|
+
KBD_slash = 0x49
|
|
90
|
+
KBD_extra_lt_gt = 0x4A
|
|
91
|
+
KBD_printscreen = 0x4B
|
|
92
|
+
KBD_pause = 0x4C
|
|
93
|
+
KBD_insert = 0x4D
|
|
94
|
+
KBD_home = 0x4E
|
|
95
|
+
KBD_pageup = 0x4F
|
|
96
|
+
KBD_delete = 0x50
|
|
97
|
+
KBD_end = 0x51
|
|
98
|
+
KBD_pagedown = 0x52
|
|
99
|
+
KBD_left = 0x53
|
|
100
|
+
KBD_up = 0x54
|
|
101
|
+
KBD_down = 0x55
|
|
102
|
+
KBD_right = 0x56
|
|
103
|
+
KBD_kp1 = 0x57
|
|
104
|
+
KBD_kp2 = 0x58
|
|
105
|
+
KBD_kp3 = 0x59
|
|
106
|
+
KBD_kp4 = 0x5A
|
|
107
|
+
KBD_kp5 = 0x5B
|
|
108
|
+
KBD_kp6 = 0x5C
|
|
109
|
+
KBD_kp7 = 0x5D
|
|
110
|
+
KBD_kp8 = 0x5E
|
|
111
|
+
KBD_kp9 = 0x5F
|
|
112
|
+
KBD_kp0 = 0x60
|
|
113
|
+
KBD_kpdivide = 0x61
|
|
114
|
+
KBD_kpmultiply = 0x62
|
|
115
|
+
KBD_kpminus = 0x63
|
|
116
|
+
KBD_kpplus = 0x64
|
|
117
|
+
KBD_kpenter = 0x65
|
|
118
|
+
KBD_kpperiod = 0x66
|
|
119
|
+
KBD_lwindows = 0x67
|
|
120
|
+
KBD_rwindows = 0x68
|
|
121
|
+
KBD_rwinmenu = 0x69
|
|
122
|
+
KBD_kpequals = 0x6A
|
|
123
|
+
KBD_f13 = 0x6B
|
|
124
|
+
KBD_f14 = 0x6C
|
|
125
|
+
KBD_f15 = 0x6D
|
|
126
|
+
KBD_f16 = 0x6E
|
|
127
|
+
KBD_f17 = 0x6F
|
|
128
|
+
KBD_f18 = 0x70
|
|
129
|
+
KBD_f19 = 0x71
|
|
130
|
+
KBD_f20 = 0x72
|
|
131
|
+
KBD_f21 = 0x73
|
|
132
|
+
KBD_f22 = 0x74
|
|
133
|
+
KBD_f23 = 0x75
|
|
134
|
+
KBD_f24 = 0x76
|
|
135
|
+
KBD_jp_hankaku = 0x77
|
|
136
|
+
KBD_jp_muhenkan = 0x78
|
|
137
|
+
KBD_jp_henkan = 0x79
|
|
138
|
+
KBD_jp_hiragana = 0x7A
|
|
139
|
+
KBD_yen = 0x7B
|
|
140
|
+
KBD_underscore = 0x7C
|
|
141
|
+
KBD_ax = 0x7D
|
|
142
|
+
KBD_conv = 0x7E
|
|
143
|
+
KBD_nconv = 0x7F
|
|
144
|
+
KBD_kor_hancha = 0x80
|
|
145
|
+
KBD_kor_hanyong = 0x81
|
|
146
|
+
KBD_jp_yen = 0x82
|
|
147
|
+
KBD_jp_backslash = 0x83
|
|
148
|
+
KBD_colon = 0x84
|
|
149
|
+
KBD_caret = 0x85
|
|
150
|
+
KBD_atsign = 0x86
|
|
151
|
+
KBD_jp_ro = 0x87
|
|
152
|
+
KBD_help = 0x88
|
|
153
|
+
KBD_kpcomma = 0x89
|
|
154
|
+
KBD_stop = 0x8A
|
|
155
|
+
KBD_copy = 0x8B
|
|
156
|
+
KBD_vf1 = 0x8C
|
|
157
|
+
KBD_vf2 = 0x8D
|
|
158
|
+
KBD_vf3 = 0x8E
|
|
159
|
+
KBD_vf4 = 0x8F
|
|
160
|
+
KBD_vf5 = 0x90
|
|
161
|
+
KBD_kana = 0x91
|
|
162
|
+
KBD_nfer = 0x92
|
|
163
|
+
KBD_xfer = 0x93
|
|
164
|
+
KBD_LAST = 0x94
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Mapping from DBX_KEY to QMP qcode strings
|
|
168
|
+
# Based on DOSBox-X qmp.cpp qkeyname_to_keycode()
|
|
169
|
+
DBX_KEY_TO_QCODE: dict[DBX_KEY, str] = {
|
|
170
|
+
# Letters
|
|
171
|
+
DBX_KEY.KBD_a: "a",
|
|
172
|
+
DBX_KEY.KBD_b: "b",
|
|
173
|
+
DBX_KEY.KBD_c: "c",
|
|
174
|
+
DBX_KEY.KBD_d: "d",
|
|
175
|
+
DBX_KEY.KBD_e: "e",
|
|
176
|
+
DBX_KEY.KBD_f: "f",
|
|
177
|
+
DBX_KEY.KBD_g: "g",
|
|
178
|
+
DBX_KEY.KBD_h: "h",
|
|
179
|
+
DBX_KEY.KBD_i: "i",
|
|
180
|
+
DBX_KEY.KBD_j: "j",
|
|
181
|
+
DBX_KEY.KBD_k: "k",
|
|
182
|
+
DBX_KEY.KBD_l: "l",
|
|
183
|
+
DBX_KEY.KBD_m: "m",
|
|
184
|
+
DBX_KEY.KBD_n: "n",
|
|
185
|
+
DBX_KEY.KBD_o: "o",
|
|
186
|
+
DBX_KEY.KBD_p: "p",
|
|
187
|
+
DBX_KEY.KBD_q: "q",
|
|
188
|
+
DBX_KEY.KBD_r: "r",
|
|
189
|
+
DBX_KEY.KBD_s: "s",
|
|
190
|
+
DBX_KEY.KBD_t: "t",
|
|
191
|
+
DBX_KEY.KBD_u: "u",
|
|
192
|
+
DBX_KEY.KBD_v: "v",
|
|
193
|
+
DBX_KEY.KBD_w: "w",
|
|
194
|
+
DBX_KEY.KBD_x: "x",
|
|
195
|
+
DBX_KEY.KBD_y: "y",
|
|
196
|
+
DBX_KEY.KBD_z: "z",
|
|
197
|
+
# Numbers
|
|
198
|
+
DBX_KEY.KBD_0: "0",
|
|
199
|
+
DBX_KEY.KBD_1: "1",
|
|
200
|
+
DBX_KEY.KBD_2: "2",
|
|
201
|
+
DBX_KEY.KBD_3: "3",
|
|
202
|
+
DBX_KEY.KBD_4: "4",
|
|
203
|
+
DBX_KEY.KBD_5: "5",
|
|
204
|
+
DBX_KEY.KBD_6: "6",
|
|
205
|
+
DBX_KEY.KBD_7: "7",
|
|
206
|
+
DBX_KEY.KBD_8: "8",
|
|
207
|
+
DBX_KEY.KBD_9: "9",
|
|
208
|
+
# Function keys
|
|
209
|
+
DBX_KEY.KBD_f1: "f1",
|
|
210
|
+
DBX_KEY.KBD_f2: "f2",
|
|
211
|
+
DBX_KEY.KBD_f3: "f3",
|
|
212
|
+
DBX_KEY.KBD_f4: "f4",
|
|
213
|
+
DBX_KEY.KBD_f5: "f5",
|
|
214
|
+
DBX_KEY.KBD_f6: "f6",
|
|
215
|
+
DBX_KEY.KBD_f7: "f7",
|
|
216
|
+
DBX_KEY.KBD_f8: "f8",
|
|
217
|
+
DBX_KEY.KBD_f9: "f9",
|
|
218
|
+
DBX_KEY.KBD_f10: "f10",
|
|
219
|
+
DBX_KEY.KBD_f11: "f11",
|
|
220
|
+
DBX_KEY.KBD_f12: "f12",
|
|
221
|
+
DBX_KEY.KBD_f13: "f13",
|
|
222
|
+
DBX_KEY.KBD_f14: "f14",
|
|
223
|
+
DBX_KEY.KBD_f15: "f15",
|
|
224
|
+
DBX_KEY.KBD_f16: "f16",
|
|
225
|
+
DBX_KEY.KBD_f17: "f17",
|
|
226
|
+
DBX_KEY.KBD_f18: "f18",
|
|
227
|
+
DBX_KEY.KBD_f19: "f19",
|
|
228
|
+
DBX_KEY.KBD_f20: "f20",
|
|
229
|
+
DBX_KEY.KBD_f21: "f21",
|
|
230
|
+
DBX_KEY.KBD_f22: "f22",
|
|
231
|
+
DBX_KEY.KBD_f23: "f23",
|
|
232
|
+
DBX_KEY.KBD_f24: "f24",
|
|
233
|
+
# Modifiers
|
|
234
|
+
DBX_KEY.KBD_leftshift: "shift",
|
|
235
|
+
DBX_KEY.KBD_rightshift: "shift_r",
|
|
236
|
+
DBX_KEY.KBD_leftctrl: "ctrl",
|
|
237
|
+
DBX_KEY.KBD_rightctrl: "ctrl_r",
|
|
238
|
+
DBX_KEY.KBD_leftalt: "alt",
|
|
239
|
+
DBX_KEY.KBD_rightalt: "alt_r",
|
|
240
|
+
DBX_KEY.KBD_lwindows: "meta_l",
|
|
241
|
+
DBX_KEY.KBD_rwindows: "meta_r",
|
|
242
|
+
DBX_KEY.KBD_rwinmenu: "menu",
|
|
243
|
+
# Special keys
|
|
244
|
+
DBX_KEY.KBD_esc: "esc",
|
|
245
|
+
DBX_KEY.KBD_tab: "tab",
|
|
246
|
+
DBX_KEY.KBD_backspace: "backspace",
|
|
247
|
+
DBX_KEY.KBD_enter: "ret",
|
|
248
|
+
DBX_KEY.KBD_space: "spc",
|
|
249
|
+
DBX_KEY.KBD_capslock: "caps_lock",
|
|
250
|
+
DBX_KEY.KBD_numlock: "num_lock",
|
|
251
|
+
DBX_KEY.KBD_scrolllock: "scroll_lock",
|
|
252
|
+
# Navigation
|
|
253
|
+
DBX_KEY.KBD_insert: "insert",
|
|
254
|
+
DBX_KEY.KBD_delete: "delete",
|
|
255
|
+
DBX_KEY.KBD_home: "home",
|
|
256
|
+
DBX_KEY.KBD_end: "end",
|
|
257
|
+
DBX_KEY.KBD_pageup: "pgup",
|
|
258
|
+
DBX_KEY.KBD_pagedown: "pgdn",
|
|
259
|
+
DBX_KEY.KBD_left: "left",
|
|
260
|
+
DBX_KEY.KBD_right: "right",
|
|
261
|
+
DBX_KEY.KBD_up: "up",
|
|
262
|
+
DBX_KEY.KBD_down: "down",
|
|
263
|
+
# Punctuation
|
|
264
|
+
DBX_KEY.KBD_grave: "grave_accent",
|
|
265
|
+
DBX_KEY.KBD_minus: "minus",
|
|
266
|
+
DBX_KEY.KBD_equals: "equal",
|
|
267
|
+
DBX_KEY.KBD_backslash: "backslash",
|
|
268
|
+
DBX_KEY.KBD_leftbracket: "bracket_left",
|
|
269
|
+
DBX_KEY.KBD_rightbracket: "bracket_right",
|
|
270
|
+
DBX_KEY.KBD_semicolon: "semicolon",
|
|
271
|
+
DBX_KEY.KBD_quote: "apostrophe",
|
|
272
|
+
DBX_KEY.KBD_comma: "comma",
|
|
273
|
+
DBX_KEY.KBD_period: "dot",
|
|
274
|
+
DBX_KEY.KBD_slash: "slash",
|
|
275
|
+
DBX_KEY.KBD_extra_lt_gt: "less",
|
|
276
|
+
# System keys
|
|
277
|
+
DBX_KEY.KBD_printscreen: "print",
|
|
278
|
+
DBX_KEY.KBD_pause: "pause",
|
|
279
|
+
# Keypad
|
|
280
|
+
DBX_KEY.KBD_kp0: "kp_0",
|
|
281
|
+
DBX_KEY.KBD_kp1: "kp_1",
|
|
282
|
+
DBX_KEY.KBD_kp2: "kp_2",
|
|
283
|
+
DBX_KEY.KBD_kp3: "kp_3",
|
|
284
|
+
DBX_KEY.KBD_kp4: "kp_4",
|
|
285
|
+
DBX_KEY.KBD_kp5: "kp_5",
|
|
286
|
+
DBX_KEY.KBD_kp6: "kp_6",
|
|
287
|
+
DBX_KEY.KBD_kp7: "kp_7",
|
|
288
|
+
DBX_KEY.KBD_kp8: "kp_8",
|
|
289
|
+
DBX_KEY.KBD_kp9: "kp_9",
|
|
290
|
+
DBX_KEY.KBD_kpdivide: "kp_divide",
|
|
291
|
+
DBX_KEY.KBD_kpmultiply: "kp_multiply",
|
|
292
|
+
DBX_KEY.KBD_kpminus: "kp_subtract",
|
|
293
|
+
DBX_KEY.KBD_kpplus: "kp_add",
|
|
294
|
+
DBX_KEY.KBD_kpenter: "kp_enter",
|
|
295
|
+
DBX_KEY.KBD_kpperiod: "kp_decimal",
|
|
296
|
+
DBX_KEY.KBD_kpequals: "kp_equals",
|
|
297
|
+
DBX_KEY.KBD_kpcomma: "kp_comma",
|
|
298
|
+
# Japanese keys
|
|
299
|
+
DBX_KEY.KBD_jp_henkan: "henkan",
|
|
300
|
+
DBX_KEY.KBD_jp_muhenkan: "muhenkan",
|
|
301
|
+
DBX_KEY.KBD_jp_hiragana: "hiragana",
|
|
302
|
+
DBX_KEY.KBD_yen: "yen",
|
|
303
|
+
DBX_KEY.KBD_jp_ro: "ro",
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Reverse mapping from qcode string to DBX_KEY
|
|
307
|
+
QCODE_TO_DBX_KEY: dict[str, DBX_KEY] = {v: k for k, v in DBX_KEY_TO_QCODE.items()}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def dbx_key_to_qcode(key: DBX_KEY) -> str | None:
|
|
311
|
+
"""
|
|
312
|
+
Convert DBX_KEY to QMP qcode string.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
key: DOSBox-X key code
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
QMP qcode string or None if not mapped
|
|
319
|
+
"""
|
|
320
|
+
return DBX_KEY_TO_QCODE.get(key)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def qcode_to_dbx_key(qcode: str) -> DBX_KEY | None:
|
|
324
|
+
"""
|
|
325
|
+
Convert QMP qcode string to DBX_KEY.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
qcode: QMP qcode string (e.g., "a", "ctrl", "ret")
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
DBX_KEY or None if not mapped
|
|
332
|
+
"""
|
|
333
|
+
return QCODE_TO_DBX_KEY.get(qcode)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def char_to_qcode(char: str) -> str | None:
|
|
337
|
+
"""
|
|
338
|
+
Convert a single character to its QMP qcode.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
char: Single character (a-z, 0-9, or punctuation)
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
QMP qcode string or None if not mapped
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
>>> char_to_qcode('a')
|
|
348
|
+
'a'
|
|
349
|
+
>>> char_to_qcode('A') # Same key, needs shift
|
|
350
|
+
'a'
|
|
351
|
+
>>> char_to_qcode(' ')
|
|
352
|
+
'spc'
|
|
353
|
+
"""
|
|
354
|
+
char = char.lower()
|
|
355
|
+
if char == " ":
|
|
356
|
+
return "spc"
|
|
357
|
+
elif char == "\n" or char == "\r":
|
|
358
|
+
return "ret"
|
|
359
|
+
elif char == "\t":
|
|
360
|
+
return "tab"
|
|
361
|
+
elif char.isalnum():
|
|
362
|
+
return char
|
|
363
|
+
else:
|
|
364
|
+
# Punctuation mapping
|
|
365
|
+
punct_map = {
|
|
366
|
+
"`": "grave_accent",
|
|
367
|
+
"-": "minus",
|
|
368
|
+
"=": "equal",
|
|
369
|
+
"[": "bracket_left",
|
|
370
|
+
"]": "bracket_right",
|
|
371
|
+
"\\": "backslash",
|
|
372
|
+
";": "semicolon",
|
|
373
|
+
"'": "apostrophe",
|
|
374
|
+
",": "comma",
|
|
375
|
+
".": "dot",
|
|
376
|
+
"/": "slash",
|
|
377
|
+
}
|
|
378
|
+
return punct_map.get(char)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def char_needs_shift(char: str) -> bool:
|
|
382
|
+
"""
|
|
383
|
+
Check if a character requires shift to type.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
char: Single character
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
True if shift is needed
|
|
390
|
+
"""
|
|
391
|
+
shift_chars = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&*()_+{}|:"<>?')
|
|
392
|
+
return char in shift_chars
|
dbxdebug/gdb.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GDB Remote Serial Protocol client for DOSBox-X.
|
|
3
|
+
|
|
4
|
+
Provides debugging capabilities:
|
|
5
|
+
- Memory read/write
|
|
6
|
+
- Register read/write
|
|
7
|
+
- Breakpoint management
|
|
8
|
+
- Execution control (step, continue)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import binascii
|
|
12
|
+
import socket
|
|
13
|
+
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
from .utils import parse_x86_address
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GDBClient:
|
|
20
|
+
"""GDB Remote Serial Protocol client for DOSBox-X debugging."""
|
|
21
|
+
|
|
22
|
+
DEFAULT_PORT = 2159
|
|
23
|
+
|
|
24
|
+
def __init__(self, host: str = "localhost", port: int = DEFAULT_PORT):
|
|
25
|
+
"""
|
|
26
|
+
Connect to DOSBox-X GDB server.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
host: Server hostname
|
|
30
|
+
port: Server port (default 2159)
|
|
31
|
+
"""
|
|
32
|
+
logger.debug(f"Connecting to GDB server at {host}:{port}")
|
|
33
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
34
|
+
self.sock.connect((host, port))
|
|
35
|
+
self.buffer = b""
|
|
36
|
+
self._no_ack_mode = False
|
|
37
|
+
|
|
38
|
+
# Initial handshake
|
|
39
|
+
self._send_packet(
|
|
40
|
+
b"qSupported:multiprocess+;swbreak+;hwbreak+;qRelocInsn+;"
|
|
41
|
+
b"fork-events+;vfork-events+;exec-events+;vContSupported+;"
|
|
42
|
+
b"QThreadEvents+;no-resumed+"
|
|
43
|
+
)
|
|
44
|
+
response = self._read_packet()
|
|
45
|
+
logger.debug(f"Handshake response: {response}")
|
|
46
|
+
|
|
47
|
+
def _calculate_checksum(self, data: bytes) -> int:
|
|
48
|
+
"""Calculate GDB packet checksum."""
|
|
49
|
+
checksum = 0
|
|
50
|
+
for b in data:
|
|
51
|
+
checksum = (checksum + b) & 0xFF
|
|
52
|
+
return checksum
|
|
53
|
+
|
|
54
|
+
def _send_packet(self, packet: bytes) -> None:
|
|
55
|
+
"""Send a GDB packet with checksum."""
|
|
56
|
+
checksum = self._calculate_checksum(packet)
|
|
57
|
+
packet_with_checksum = b"$" + packet + b"#" + f"{checksum:02x}".encode()
|
|
58
|
+
|
|
59
|
+
if self.sock is None:
|
|
60
|
+
raise ConnectionError("Socket not initialized")
|
|
61
|
+
|
|
62
|
+
self.sock.sendall(packet_with_checksum)
|
|
63
|
+
|
|
64
|
+
if not self._no_ack_mode:
|
|
65
|
+
ack = self.sock.recv(1)
|
|
66
|
+
if ack != b"+":
|
|
67
|
+
raise ConnectionError(f"Failed to receive ACK. Got: {ack}")
|
|
68
|
+
|
|
69
|
+
def _read_packet(self) -> bytes:
|
|
70
|
+
"""Read a GDB packet and verify checksum."""
|
|
71
|
+
while True:
|
|
72
|
+
if self.sock is None:
|
|
73
|
+
raise ConnectionError("Socket not initialized")
|
|
74
|
+
|
|
75
|
+
if not self.buffer:
|
|
76
|
+
self.buffer = self.sock.recv(4096)
|
|
77
|
+
if not self.buffer:
|
|
78
|
+
raise ConnectionError("Connection closed")
|
|
79
|
+
|
|
80
|
+
# Find packet start
|
|
81
|
+
if self.buffer[0:1] != b"$":
|
|
82
|
+
self.buffer = self.buffer[1:]
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Find packet end
|
|
86
|
+
hash_pos = self.buffer.find(b"#")
|
|
87
|
+
if hash_pos == -1:
|
|
88
|
+
more_data = self.sock.recv(4096)
|
|
89
|
+
if not more_data:
|
|
90
|
+
raise ConnectionError("Connection closed while waiting for packet end")
|
|
91
|
+
self.buffer += more_data
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Need checksum bytes
|
|
95
|
+
if len(self.buffer) < hash_pos + 3:
|
|
96
|
+
more_data = self.sock.recv(4096)
|
|
97
|
+
if not more_data:
|
|
98
|
+
raise ConnectionError("Connection closed while waiting for checksum")
|
|
99
|
+
self.buffer += more_data
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
packet_data = self.buffer[1:hash_pos]
|
|
103
|
+
checksum_bytes = self.buffer[hash_pos + 1 : hash_pos + 3]
|
|
104
|
+
|
|
105
|
+
calculated_checksum = self._calculate_checksum(packet_data)
|
|
106
|
+
received_checksum = int(checksum_bytes, 16)
|
|
107
|
+
|
|
108
|
+
if calculated_checksum == received_checksum:
|
|
109
|
+
if not self._no_ack_mode:
|
|
110
|
+
self.sock.sendall(b"+")
|
|
111
|
+
self.buffer = self.buffer[hash_pos + 3 :]
|
|
112
|
+
return packet_data
|
|
113
|
+
else:
|
|
114
|
+
if not self._no_ack_mode:
|
|
115
|
+
self.sock.sendall(b"-")
|
|
116
|
+
self.buffer = self.buffer[hash_pos + 3 :]
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
def enable_no_ack_mode(self) -> bool:
|
|
120
|
+
"""Enable no-ACK mode for faster communication."""
|
|
121
|
+
self._send_packet(b"QStartNoAckMode")
|
|
122
|
+
response = self._read_packet()
|
|
123
|
+
if response == b"OK":
|
|
124
|
+
self._no_ack_mode = True
|
|
125
|
+
return True
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def read_memory(self, address: str | int, length: int) -> bytes:
|
|
129
|
+
"""
|
|
130
|
+
Read memory from the target.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
address: Linear address or segmented address (e.g., "b800:0000")
|
|
134
|
+
length: Number of bytes to read
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Raw bytes from memory
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
MemoryError: If read fails
|
|
141
|
+
"""
|
|
142
|
+
linear_addr = parse_x86_address(address)
|
|
143
|
+
cmd = f"m{linear_addr:x},{length:x}".encode()
|
|
144
|
+
self._send_packet(cmd)
|
|
145
|
+
response = self._read_packet()
|
|
146
|
+
|
|
147
|
+
if response.startswith(b"E"):
|
|
148
|
+
error_code = response[1:].decode()
|
|
149
|
+
raise MemoryError(f"Error reading memory at 0x{linear_addr:x}: {error_code}")
|
|
150
|
+
|
|
151
|
+
return binascii.unhexlify(response)
|
|
152
|
+
|
|
153
|
+
def write_memory(self, address: str | int, data: bytes) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Write memory to the target.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
address: Linear address or segmented address
|
|
159
|
+
data: Bytes to write
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
MemoryError: If write fails
|
|
163
|
+
"""
|
|
164
|
+
linear_addr = parse_x86_address(address)
|
|
165
|
+
hex_data = binascii.hexlify(data).decode()
|
|
166
|
+
cmd = f"M{linear_addr:x},{len(data):x}:{hex_data}".encode()
|
|
167
|
+
self._send_packet(cmd)
|
|
168
|
+
response = self._read_packet()
|
|
169
|
+
|
|
170
|
+
if response != b"OK":
|
|
171
|
+
raise MemoryError(f"Error writing memory at 0x{linear_addr:x}: {response.decode()}")
|
|
172
|
+
|
|
173
|
+
def read_registers(self) -> dict[str, int]:
|
|
174
|
+
"""
|
|
175
|
+
Read all CPU registers.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Dict mapping register names to values
|
|
179
|
+
"""
|
|
180
|
+
self._send_packet(b"g")
|
|
181
|
+
response = self._read_packet()
|
|
182
|
+
|
|
183
|
+
# Response is 16 registers, 8 hex chars each (little-endian)
|
|
184
|
+
reg_names = [
|
|
185
|
+
"eax",
|
|
186
|
+
"ecx",
|
|
187
|
+
"edx",
|
|
188
|
+
"ebx",
|
|
189
|
+
"esp",
|
|
190
|
+
"ebp",
|
|
191
|
+
"esi",
|
|
192
|
+
"edi",
|
|
193
|
+
"eip",
|
|
194
|
+
"eflags",
|
|
195
|
+
"cs",
|
|
196
|
+
"ss",
|
|
197
|
+
"ds",
|
|
198
|
+
"es",
|
|
199
|
+
"fs",
|
|
200
|
+
"gs",
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
registers = {}
|
|
204
|
+
for i, name in enumerate(reg_names):
|
|
205
|
+
hex_val = response[i * 8 : (i + 1) * 8]
|
|
206
|
+
# Convert from little-endian
|
|
207
|
+
val_bytes = binascii.unhexlify(hex_val)
|
|
208
|
+
registers[name] = int.from_bytes(val_bytes, "little")
|
|
209
|
+
|
|
210
|
+
return registers
|
|
211
|
+
|
|
212
|
+
def read_register(self, reg_num: int) -> int:
|
|
213
|
+
"""
|
|
214
|
+
Read a single register.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
reg_num: Register number (0-15)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Register value
|
|
221
|
+
"""
|
|
222
|
+
self._send_packet(f"p{reg_num:x}".encode())
|
|
223
|
+
response = self._read_packet()
|
|
224
|
+
val_bytes = binascii.unhexlify(response)
|
|
225
|
+
return int.from_bytes(val_bytes, "little")
|
|
226
|
+
|
|
227
|
+
def set_breakpoint(self, address: str | int) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Set a software breakpoint.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
address: Linear or segmented address
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
True if successful
|
|
236
|
+
"""
|
|
237
|
+
linear_addr = parse_x86_address(address)
|
|
238
|
+
self._send_packet(f"Z0,{linear_addr:x},1".encode())
|
|
239
|
+
response = self._read_packet()
|
|
240
|
+
return response == b"OK"
|
|
241
|
+
|
|
242
|
+
def remove_breakpoint(self, address: str | int) -> bool:
|
|
243
|
+
"""
|
|
244
|
+
Remove a breakpoint.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
address: Linear or segmented address
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
True if successful
|
|
251
|
+
"""
|
|
252
|
+
linear_addr = parse_x86_address(address)
|
|
253
|
+
self._send_packet(f"z0,{linear_addr:x},1".encode())
|
|
254
|
+
response = self._read_packet()
|
|
255
|
+
return response == b"OK"
|
|
256
|
+
|
|
257
|
+
def step(self) -> bytes:
|
|
258
|
+
"""
|
|
259
|
+
Single-step one instruction.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Stop reason response
|
|
263
|
+
"""
|
|
264
|
+
self._send_packet(b"s")
|
|
265
|
+
return self._read_packet()
|
|
266
|
+
|
|
267
|
+
def continue_execution(self) -> bytes:
|
|
268
|
+
"""
|
|
269
|
+
Continue execution until breakpoint or stop.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Stop reason response
|
|
273
|
+
"""
|
|
274
|
+
self._send_packet(b"c")
|
|
275
|
+
return self._read_packet()
|
|
276
|
+
|
|
277
|
+
def halt(self) -> bytes:
|
|
278
|
+
"""
|
|
279
|
+
Request halt/break into debugger.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Stop reason response
|
|
283
|
+
"""
|
|
284
|
+
self._send_packet(b"?")
|
|
285
|
+
return self._read_packet()
|
|
286
|
+
|
|
287
|
+
def close(self) -> None:
|
|
288
|
+
"""Close the connection."""
|
|
289
|
+
if self.sock:
|
|
290
|
+
self.sock.close()
|
|
291
|
+
self.sock = None # type: ignore
|
|
292
|
+
|
|
293
|
+
def __enter__(self) -> "GDBClient":
|
|
294
|
+
return self
|
|
295
|
+
|
|
296
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
297
|
+
self.close()
|