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/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()