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/video.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DOS video memory access utilities.
|
|
3
|
+
|
|
4
|
+
Provides screen capture and video memory inspection for DOS text mode.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from .gdb import GDBClient
|
|
10
|
+
|
|
11
|
+
# DOS video memory addresses
|
|
12
|
+
DOS_VIDEO_PAGE_ONE = "0xB800:0000"
|
|
13
|
+
DOS_VIDEO_PAGE_TWO = "0xB800:1000"
|
|
14
|
+
DOS_VIDEO_MEMORY_SIZE = 0xFA0 # 4000 bytes (80 * 25 * 2)
|
|
15
|
+
|
|
16
|
+
# BIOS Data Area addresses
|
|
17
|
+
BDA_MODE = "0x0040:0049"
|
|
18
|
+
BDA_COLUMN_COUNT = "0x0040:004A"
|
|
19
|
+
BDA_ROW_COUNT = "0x0040:0084"
|
|
20
|
+
BDA_TIMER_TICK = "0x0040:006C" # 4-byte tick counter, 18.2065 Hz
|
|
21
|
+
|
|
22
|
+
# Timer constants
|
|
23
|
+
TIMER_FREQUENCY = 18.2065 # Hz
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DOSVideoTools:
|
|
27
|
+
"""Tools for analyzing DOS program screen output."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, host: str = "localhost", port: int = GDBClient.DEFAULT_PORT):
|
|
30
|
+
"""
|
|
31
|
+
Initialize video tools with GDB connection.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
host: GDB server hostname
|
|
35
|
+
port: GDB server port
|
|
36
|
+
"""
|
|
37
|
+
self.gdb = GDBClient(host, port)
|
|
38
|
+
|
|
39
|
+
def close(self) -> None:
|
|
40
|
+
"""Close the GDB connection."""
|
|
41
|
+
self.gdb.close()
|
|
42
|
+
|
|
43
|
+
def __enter__(self) -> "DOSVideoTools":
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
47
|
+
self.close()
|
|
48
|
+
|
|
49
|
+
def read_timer_ticks(self) -> int | None:
|
|
50
|
+
"""
|
|
51
|
+
Read the BIOS timer tick counter (18.2065 Hz).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Tick count or None on error
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
data = self.gdb.read_memory(BDA_TIMER_TICK, 4)
|
|
58
|
+
return int.from_bytes(data, "little")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.exception(e)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def read_video_mode(self) -> int | None:
|
|
64
|
+
"""
|
|
65
|
+
Read the current video mode.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Video mode number or None on error
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
data = self.gdb.read_memory(BDA_MODE, 1)
|
|
72
|
+
return data[0]
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.exception(e)
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def screen_dump(self, page: int = 1) -> list[str] | None:
|
|
78
|
+
"""
|
|
79
|
+
Dump the DOS text screen as a list of strings.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
page: Video page number (1 or 2)
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
List of 25 strings (80 chars each) or None on error
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
addr = DOS_VIDEO_PAGE_ONE if page == 1 else DOS_VIDEO_PAGE_TWO
|
|
89
|
+
memory = self.gdb.read_memory(addr, DOS_VIDEO_MEMORY_SIZE)
|
|
90
|
+
|
|
91
|
+
lines = []
|
|
92
|
+
for row in range(25):
|
|
93
|
+
line_text = ""
|
|
94
|
+
for col in range(80):
|
|
95
|
+
char_index = (row * 80 + col) * 2
|
|
96
|
+
if char_index < len(memory):
|
|
97
|
+
char = memory[char_index]
|
|
98
|
+
if char == 0:
|
|
99
|
+
line_text += " "
|
|
100
|
+
elif 32 <= char <= 126:
|
|
101
|
+
line_text += chr(char)
|
|
102
|
+
else:
|
|
103
|
+
line_text += chr(char)
|
|
104
|
+
else:
|
|
105
|
+
line_text += " "
|
|
106
|
+
lines.append(line_text)
|
|
107
|
+
return lines
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.exception(e)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def screen_dump_with_ticks(self) -> tuple[list[str] | None, int | None]:
|
|
113
|
+
"""
|
|
114
|
+
Dump screen and timer ticks together for timing correlation.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Tuple of (lines, ticks) or (None, None) on error
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
# Read timer first (small read, fast)
|
|
121
|
+
tick_data = self.gdb.read_memory(BDA_TIMER_TICK, 4)
|
|
122
|
+
ticks = int.from_bytes(tick_data, "little")
|
|
123
|
+
|
|
124
|
+
# Then read screen
|
|
125
|
+
memory = self.gdb.read_memory(DOS_VIDEO_PAGE_ONE, DOS_VIDEO_MEMORY_SIZE)
|
|
126
|
+
|
|
127
|
+
lines = []
|
|
128
|
+
for row in range(25):
|
|
129
|
+
line_text = ""
|
|
130
|
+
for col in range(80):
|
|
131
|
+
char_index = (row * 80 + col) * 2
|
|
132
|
+
if char_index < len(memory):
|
|
133
|
+
char = memory[char_index]
|
|
134
|
+
if char == 0 or char == 32:
|
|
135
|
+
line_text += " "
|
|
136
|
+
elif 32 <= char <= 126:
|
|
137
|
+
line_text += chr(char)
|
|
138
|
+
else:
|
|
139
|
+
line_text += chr(char)
|
|
140
|
+
else:
|
|
141
|
+
line_text += " "
|
|
142
|
+
lines.append(line_text)
|
|
143
|
+
return (lines, ticks)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.exception(e)
|
|
146
|
+
return (None, None)
|
|
147
|
+
|
|
148
|
+
def screen_raw(self, page: int = 1) -> bytes | None:
|
|
149
|
+
"""
|
|
150
|
+
Read raw video memory (characters and attributes).
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
page: Video page number (1 or 2)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Raw bytes or None on error
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
addr = DOS_VIDEO_PAGE_ONE if page == 1 else DOS_VIDEO_PAGE_TWO
|
|
160
|
+
return self.gdb.read_memory(addr, DOS_VIDEO_MEMORY_SIZE)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.exception(e)
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def screen_debug(self) -> list[bytes] | None:
|
|
166
|
+
"""
|
|
167
|
+
Read raw video memory from both pages.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of [page1_bytes, page2_bytes] or None on error
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
return [
|
|
174
|
+
self.gdb.read_memory(DOS_VIDEO_PAGE_ONE, DOS_VIDEO_MEMORY_SIZE),
|
|
175
|
+
self.gdb.read_memory(DOS_VIDEO_PAGE_TWO, DOS_VIDEO_MEMORY_SIZE),
|
|
176
|
+
]
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.exception(e)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def decode_vga_attribute(attr_byte: int) -> dict:
|
|
183
|
+
"""
|
|
184
|
+
Decode a VGA text mode attribute byte.
|
|
185
|
+
|
|
186
|
+
Attribute format: IRGB irgb
|
|
187
|
+
- Upper 4 bits: background color (IRGB)
|
|
188
|
+
- Lower 4 bits: foreground color (irgb)
|
|
189
|
+
- Bit 7: blink flag
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
attr_byte: Attribute byte value
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Dict with foreground, background, colors, and blink flag
|
|
196
|
+
"""
|
|
197
|
+
color_names = [
|
|
198
|
+
"Black",
|
|
199
|
+
"Blue",
|
|
200
|
+
"Green",
|
|
201
|
+
"Cyan",
|
|
202
|
+
"Red",
|
|
203
|
+
"Magenta",
|
|
204
|
+
"Brown",
|
|
205
|
+
"Light Gray",
|
|
206
|
+
"Dark Gray",
|
|
207
|
+
"Light Blue",
|
|
208
|
+
"Light Green",
|
|
209
|
+
"Light Cyan",
|
|
210
|
+
"Light Red",
|
|
211
|
+
"Light Magenta",
|
|
212
|
+
"Yellow",
|
|
213
|
+
"White",
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
foreground = attr_byte & 0x0F
|
|
217
|
+
background = (attr_byte & 0x70) >> 4
|
|
218
|
+
blink = (attr_byte & 0x80) != 0
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
"foreground": foreground,
|
|
222
|
+
"background": background,
|
|
223
|
+
"fg_color": color_names[foreground],
|
|
224
|
+
"bg_color": color_names[background],
|
|
225
|
+
"blink": blink,
|
|
226
|
+
"raw_value": attr_byte,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def format_attribute_info(attr_byte: int) -> str:
|
|
231
|
+
"""Format attribute information as a readable string."""
|
|
232
|
+
info = decode_vga_attribute(attr_byte)
|
|
233
|
+
|
|
234
|
+
result = f"Attribute 0x{attr_byte:02X}:\n"
|
|
235
|
+
result += f" Foreground: {info['fg_color']} (0x{info['foreground']:X})\n"
|
|
236
|
+
result += f" Background: {info['bg_color']} (0x{info['background']:X})\n"
|
|
237
|
+
result += f" Blinking: {'Yes' if info['blink'] else 'No'}\n"
|
|
238
|
+
|
|
239
|
+
return result
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dbxdebug
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Client library and CLI for DOSBox-X remote debug protocols (GDB and QMP)
|
|
5
|
+
Author: lokkju
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: debugging,dosbox,dosbox-x,emulator,gdb,qmp
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: POSIX
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
16
|
+
Classifier: Topic :: System :: Emulators
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: click>=8.0
|
|
19
|
+
Requires-Dist: loguru>=0.7
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# dbxdebug
|
|
23
|
+
|
|
24
|
+
Client library and CLI for DOSBox-X remote debug protocols.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **GDB Client**: Remote debugging via GDB Remote Serial Protocol
|
|
29
|
+
- Memory read/write
|
|
30
|
+
- Register inspection
|
|
31
|
+
- Breakpoint management
|
|
32
|
+
- Execution control (step, continue, halt)
|
|
33
|
+
|
|
34
|
+
- **QMP Client**: Keyboard input via QEMU Monitor Protocol
|
|
35
|
+
- Key press/release with timing control
|
|
36
|
+
- Text typing with shift handling
|
|
37
|
+
- Key combinations (Ctrl+C, Alt+F4, etc.)
|
|
38
|
+
|
|
39
|
+
- **Video Tools**: DOS text mode screen capture
|
|
40
|
+
- Screen capture (text, raw, HTML)
|
|
41
|
+
- Timed multi-frame recording
|
|
42
|
+
- Color analysis
|
|
43
|
+
- BIOS timer correlation
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv sync
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## CLI Usage
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
dbxdebug
|
|
55
|
+
├── mem # Memory operations (alias: gdb)
|
|
56
|
+
│ ├── read # Read memory
|
|
57
|
+
│ └── write # Write hex bytes
|
|
58
|
+
│
|
|
59
|
+
├── cpu # CPU and execution control
|
|
60
|
+
│ ├── regs # Display registers
|
|
61
|
+
│ ├── break # Set breakpoint
|
|
62
|
+
│ ├── delete # Remove breakpoint
|
|
63
|
+
│ ├── step # Single step
|
|
64
|
+
│ ├── cont # Continue execution
|
|
65
|
+
│ └── halt # Stop execution
|
|
66
|
+
│
|
|
67
|
+
├── key # Keyboard input (alias: qmp)
|
|
68
|
+
│ ├── send # Key chord (e.g., ctrl c)
|
|
69
|
+
│ ├── type # Type text string
|
|
70
|
+
│ ├── down # Press and hold key
|
|
71
|
+
│ ├── up # Release key
|
|
72
|
+
│ └── list # List QMP commands
|
|
73
|
+
│
|
|
74
|
+
└── screen # Screen capture
|
|
75
|
+
├── show # Display text to stdout
|
|
76
|
+
├── capture # Save frame to file (-f raw|html|text)
|
|
77
|
+
├── record # Multi-frame timed capture
|
|
78
|
+
├── watch # Real-time display
|
|
79
|
+
├── info # Video mode, BIOS ticks
|
|
80
|
+
└── colors # Analyze color palette
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Examples
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Memory
|
|
87
|
+
dbxdebug mem read b800:0000 4000 --hex
|
|
88
|
+
dbxdebug mem write 0x1000 90909090
|
|
89
|
+
|
|
90
|
+
# CPU / Debugging
|
|
91
|
+
dbxdebug cpu regs
|
|
92
|
+
dbxdebug cpu break 0x1000
|
|
93
|
+
dbxdebug cpu step
|
|
94
|
+
dbxdebug cpu cont
|
|
95
|
+
|
|
96
|
+
# Keyboard
|
|
97
|
+
dbxdebug key send a
|
|
98
|
+
dbxdebug key send ctrl c
|
|
99
|
+
dbxdebug key send ctrl alt delete
|
|
100
|
+
dbxdebug key type "Hello World!"
|
|
101
|
+
|
|
102
|
+
# Screen
|
|
103
|
+
dbxdebug screen show
|
|
104
|
+
dbxdebug screen capture -f html -o snapshot
|
|
105
|
+
dbxdebug screen record -d 60 -r 30 -o session.capture.gz
|
|
106
|
+
dbxdebug screen watch
|
|
107
|
+
dbxdebug screen colors
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Library Usage
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from dbxdebug import (
|
|
114
|
+
GDBClient, QMPClient, DOSVideoTools,
|
|
115
|
+
ScreenRecorder, load_capture,
|
|
116
|
+
ctrl_key, CTRL_C, DBX_KEY,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Memory and debugging
|
|
120
|
+
with GDBClient() as gdb:
|
|
121
|
+
regs = gdb.read_registers()
|
|
122
|
+
mem = gdb.read_memory("b800:0000", 4000)
|
|
123
|
+
gdb.write_memory(0x1000, b"\x90\x90")
|
|
124
|
+
gdb.set_breakpoint(0x1000)
|
|
125
|
+
gdb.step()
|
|
126
|
+
gdb.continue_execution()
|
|
127
|
+
|
|
128
|
+
# Keyboard input
|
|
129
|
+
with QMPClient() as qmp:
|
|
130
|
+
qmp.send_key(["ctrl", "c"]) # Key chord
|
|
131
|
+
qmp.send_key(CTRL_C) # Using constant
|
|
132
|
+
qmp.type_text("Hello World!") # Type string
|
|
133
|
+
qmp.key_down("shift") # Hold key
|
|
134
|
+
qmp.key_up("shift") # Release key
|
|
135
|
+
|
|
136
|
+
# Screen capture
|
|
137
|
+
with DOSVideoTools() as video:
|
|
138
|
+
lines = video.screen_dump() # Text lines
|
|
139
|
+
raw = video.screen_raw() # Raw bytes with attrs
|
|
140
|
+
lines, ticks = video.screen_dump_with_ticks()
|
|
141
|
+
|
|
142
|
+
# Timed recording
|
|
143
|
+
with DOSVideoTools() as video:
|
|
144
|
+
recorder = ScreenRecorder()
|
|
145
|
+
recorder.record(video, duration=10.0, sample_rate=50)
|
|
146
|
+
recorder.save("session.capture.gz")
|
|
147
|
+
|
|
148
|
+
# Load and analyze capture
|
|
149
|
+
data = load_capture("session.capture.gz")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Ports
|
|
153
|
+
|
|
154
|
+
| Protocol | Default Port | Purpose |
|
|
155
|
+
|----------|--------------|---------|
|
|
156
|
+
| GDB | 2159 | Debugging, memory access |
|
|
157
|
+
| QMP | 4444 | Keyboard input |
|
|
158
|
+
|
|
159
|
+
Enable in DOSBox-X config:
|
|
160
|
+
```ini
|
|
161
|
+
[dosbox]
|
|
162
|
+
gdbserver=true
|
|
163
|
+
qmpserver=true
|
|
164
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
dbxdebug/__init__.py,sha256=9VE7gig7l6WQMdXdYa2_NFrYkBDldAIjw2lRlgESu1M,2650
|
|
2
|
+
dbxdebug/capture_io.py,sha256=fxHHLnXky29_K8r0j2FpeOP4nx9BtmLtjJ8NT4zgkNc,5681
|
|
3
|
+
dbxdebug/cli.py,sha256=kIfWbLntUgHj_xGq19hoerEwU4J-lL1yxZx8fSzX8CM,21269
|
|
4
|
+
dbxdebug/dbx_kbd.py,sha256=nVE9Br9G-FajK0ZX3wrlm-sngtQ2aQilNGTlhsT3smY,9409
|
|
5
|
+
dbxdebug/gdb.py,sha256=RzJM0cekLk6-v6-x0ldLBIoKHLR4iFYnXc_dRMEOQyo,8730
|
|
6
|
+
dbxdebug/html.py,sha256=AQLSsRU5gQ-O7UO0vgqSE7L6D40yZja_jH0Yz9Ce4lQ,15098
|
|
7
|
+
dbxdebug/keyboard.py,sha256=CdS5i7g4PYLCsjtJkopS3mY5DE6zz1d-BmAK5grDOZc,4025
|
|
8
|
+
dbxdebug/qmp.py,sha256=hkNiMyg_VBHX8RH4SKIXY36T4BqLbmvcHlErTI8lUTo,7212
|
|
9
|
+
dbxdebug/utils.py,sha256=bEai_WicXvnGTTaTf0DJtNJ78vI0g-m0KfnuegZ7fXY,2703
|
|
10
|
+
dbxdebug/video.py,sha256=knQdG0hmtLzufjrA_MqWAmh-lMWyKQ4ZWf7uKMMVIC0,6981
|
|
11
|
+
dbxdebug-0.2.1.dist-info/METADATA,sha256=Li4B-9M91UQqPeRl20F4qXxjSoQ3L01McxJ6aZVZ15E,4562
|
|
12
|
+
dbxdebug-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
13
|
+
dbxdebug-0.2.1.dist-info/entry_points.txt,sha256=lzVCl-Qns7sa1y8IO6Jil3LeQPKupF7vTESe7LeUICg,47
|
|
14
|
+
dbxdebug-0.2.1.dist-info/RECORD,,
|