ruby-gdb 0.1.0
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.
- checksums.yaml +7 -0
- data/bake/ruby/gdb.rb +135 -0
- data/context/fiber-debugging.md +372 -0
- data/context/getting-started.md +167 -0
- data/context/heap-debugging.md +426 -0
- data/context/index.yaml +28 -0
- data/context/object-inspection.md +272 -0
- data/context/stack-inspection.md +357 -0
- data/data/ruby/gdb/command.py +254 -0
- data/data/ruby/gdb/constants.py +59 -0
- data/data/ruby/gdb/fiber.py +825 -0
- data/data/ruby/gdb/format.py +201 -0
- data/data/ruby/gdb/heap.py +563 -0
- data/data/ruby/gdb/init.py +25 -0
- data/data/ruby/gdb/object.py +85 -0
- data/data/ruby/gdb/rarray.py +124 -0
- data/data/ruby/gdb/rbasic.py +103 -0
- data/data/ruby/gdb/rbignum.py +52 -0
- data/data/ruby/gdb/rclass.py +133 -0
- data/data/ruby/gdb/rexception.py +150 -0
- data/data/ruby/gdb/rfloat.py +95 -0
- data/data/ruby/gdb/rhash.py +157 -0
- data/data/ruby/gdb/rstring.py +217 -0
- data/data/ruby/gdb/rstruct.py +157 -0
- data/data/ruby/gdb/rsymbol.py +291 -0
- data/data/ruby/gdb/stack.py +609 -0
- data/data/ruby/gdb/value.py +181 -0
- data/lib/ruby/gdb/version.rb +13 -0
- data/lib/ruby/gdb.rb +23 -0
- data/license.md +21 -0
- data/readme.md +42 -0
- metadata +69 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import gdb
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# Constants
|
|
5
|
+
RBASIC_FLAGS_TYPE_MASK = 0x1f
|
|
6
|
+
|
|
7
|
+
class RubyHeap:
|
|
8
|
+
"""Ruby heap scanning infrastructure.
|
|
9
|
+
|
|
10
|
+
Provides methods to iterate through the Ruby heap and find objects
|
|
11
|
+
by type. Returns VALUEs (not extracted pointers) for maximum flexibility.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
"""Initialize heap scanner (call initialize() to set up VM pointers)."""
|
|
16
|
+
self.vm_ptr = None
|
|
17
|
+
self.objspace = None
|
|
18
|
+
|
|
19
|
+
# Cached type lookups
|
|
20
|
+
self._rbasic_type = None
|
|
21
|
+
self._value_type = None
|
|
22
|
+
self._char_ptr_type = None
|
|
23
|
+
|
|
24
|
+
def initialize(self):
|
|
25
|
+
"""Initialize VM and objspace pointers.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
True if initialization successful, False otherwise
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
self.vm_ptr = gdb.parse_and_eval('ruby_current_vm_ptr')
|
|
32
|
+
if int(self.vm_ptr) == 0:
|
|
33
|
+
print("Error: ruby_current_vm_ptr is NULL")
|
|
34
|
+
print("Make sure Ruby is fully initialized and the process is running.")
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
# Ruby 3.3+ moved objspace into a gc struct
|
|
38
|
+
try:
|
|
39
|
+
self.objspace = self.vm_ptr['gc']['objspace']
|
|
40
|
+
except (gdb.error, KeyError):
|
|
41
|
+
# Ruby 3.2 and earlier have objspace directly in VM
|
|
42
|
+
self.objspace = self.vm_ptr['objspace']
|
|
43
|
+
|
|
44
|
+
if int(self.objspace) == 0:
|
|
45
|
+
print("Error: objspace is NULL")
|
|
46
|
+
print("Make sure the Ruby GC has been initialized.")
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
# Cache commonly used type lookups
|
|
50
|
+
self._rbasic_type = gdb.lookup_type('struct RBasic').pointer()
|
|
51
|
+
self._value_type = gdb.lookup_type('VALUE')
|
|
52
|
+
self._char_ptr_type = gdb.lookup_type('char').pointer()
|
|
53
|
+
|
|
54
|
+
return True
|
|
55
|
+
except gdb.error as e:
|
|
56
|
+
print(f"Error initializing: {e}")
|
|
57
|
+
print("Make sure you're debugging a Ruby process with debug symbols.")
|
|
58
|
+
return False
|
|
59
|
+
except gdb.MemoryError as e:
|
|
60
|
+
print(f"Memory error during initialization: {e}")
|
|
61
|
+
print("The Ruby VM may not be fully initialized yet.")
|
|
62
|
+
print("Try breaking at a point where Ruby is running (e.g., after rb_vm_exec).")
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def _get_page(self, page_index):
|
|
66
|
+
"""Get a heap page by index, handling Ruby version differences.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
page_index: Index of the page to retrieve
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Page object, or None on error
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
# Ruby 3.3+ uses rb_darray with 'data' field, Ruby 3.2- uses direct pointer
|
|
76
|
+
try:
|
|
77
|
+
return self.objspace['heap_pages']['sorted']['data'][page_index]
|
|
78
|
+
except (gdb.error, KeyError):
|
|
79
|
+
# Ruby 3.2 and earlier: sorted is a direct pointer array
|
|
80
|
+
return self.objspace['heap_pages']['sorted'][page_index]
|
|
81
|
+
except (gdb.MemoryError, gdb.error):
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def iterate_heap(self):
|
|
85
|
+
"""Yield all objects from the Ruby heap.
|
|
86
|
+
|
|
87
|
+
Yields:
|
|
88
|
+
Tuple of (VALUE, flags, address) for each object on the heap
|
|
89
|
+
"""
|
|
90
|
+
for obj, flags, address in self.iterate_heap_from(None):
|
|
91
|
+
yield obj, flags, address
|
|
92
|
+
|
|
93
|
+
def scan(self, type_flag=None, limit=None, from_address=None):
|
|
94
|
+
"""Scan heap for objects matching a specific Ruby type flag.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
type_flag: Ruby type constant (e.g., RUBY_T_STRING, RUBY_T_DATA), or None for all types
|
|
98
|
+
limit: Maximum number of objects to find (None for no limit)
|
|
99
|
+
from_address: Address to continue from (for pagination)
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Tuple of (objects, next_address) where:
|
|
103
|
+
- objects: List of VALUEs matching the type
|
|
104
|
+
- next_address: The next address to scan from (for pagination), or None if no more objects
|
|
105
|
+
"""
|
|
106
|
+
if from_address:
|
|
107
|
+
print(f"DEBUG: scan() called with from_address=0x{from_address:x}", file=sys.stderr)
|
|
108
|
+
else:
|
|
109
|
+
print(f"DEBUG: scan() called with from_address=None", file=sys.stderr)
|
|
110
|
+
objects = []
|
|
111
|
+
next_address = None
|
|
112
|
+
|
|
113
|
+
# Iterate heap, starting from the address if specified
|
|
114
|
+
for obj, flags, obj_address in self.iterate_heap_from(from_address):
|
|
115
|
+
# Check type (lower 5 bits of flags) if type_flag is specified
|
|
116
|
+
if type_flag is not None:
|
|
117
|
+
if (flags & RBASIC_FLAGS_TYPE_MASK) != type_flag:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# If we've already hit the limit, this is the next address to continue from
|
|
121
|
+
if limit and len(objects) >= limit:
|
|
122
|
+
next_address = obj_address
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
objects.append(obj)
|
|
126
|
+
|
|
127
|
+
# Return the next address to scan from (the first object we didn't include)
|
|
128
|
+
return objects, next_address
|
|
129
|
+
|
|
130
|
+
def _find_page_for_address(self, address):
|
|
131
|
+
"""Find which heap page contains the given address.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
address: Memory address to search for
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Page index if found, None otherwise
|
|
138
|
+
"""
|
|
139
|
+
if not self.objspace:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
allocated_pages = int(self.objspace['heap_pages']['allocated_pages'])
|
|
144
|
+
except (gdb.MemoryError, gdb.error):
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
# Linear search through pages
|
|
148
|
+
# TODO: Could use binary search since pages are sorted
|
|
149
|
+
for i in range(allocated_pages):
|
|
150
|
+
page = self._get_page(i)
|
|
151
|
+
if page is None:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
start = int(page['start'])
|
|
156
|
+
total_slots = int(page['total_slots'])
|
|
157
|
+
slot_size = int(page['slot_size'])
|
|
158
|
+
|
|
159
|
+
# Check if address falls within this page's range
|
|
160
|
+
page_end = start + (total_slots * slot_size)
|
|
161
|
+
if start <= address < page_end:
|
|
162
|
+
return i
|
|
163
|
+
except (gdb.MemoryError, gdb.error):
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def iterate_heap_from(self, from_address=None):
|
|
169
|
+
"""Yield all objects from the Ruby heap, optionally starting from a specific address.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
from_address: If specified, finds the page containing this address and starts from there.
|
|
173
|
+
If None, starts from the beginning of the heap.
|
|
174
|
+
|
|
175
|
+
Yields:
|
|
176
|
+
Tuple of (VALUE, flags, address) for each object on the heap
|
|
177
|
+
"""
|
|
178
|
+
# If we have a from_address, find which page contains it
|
|
179
|
+
start_page = 0
|
|
180
|
+
start_address = None
|
|
181
|
+
if from_address is not None:
|
|
182
|
+
print(f"DEBUG: iterate_heap_from called with from_address=0x{from_address:x}", file=sys.stderr)
|
|
183
|
+
start_page = self._find_page_for_address(from_address)
|
|
184
|
+
print(f"DEBUG: _find_page_for_address returned {start_page}", file=sys.stderr)
|
|
185
|
+
if start_page is None:
|
|
186
|
+
# Address not found in any page, start from beginning
|
|
187
|
+
print(f"Warning: Address 0x{from_address:x} not found in heap, starting from beginning", file=sys.stderr)
|
|
188
|
+
start_page = 0
|
|
189
|
+
else:
|
|
190
|
+
# Remember to skip within the page to this address
|
|
191
|
+
start_address = from_address
|
|
192
|
+
print(f"DEBUG: Will start from page {start_page}, address 0x{start_address:x}", file=sys.stderr)
|
|
193
|
+
|
|
194
|
+
# Delegate to the page-based iterator
|
|
195
|
+
for obj, flags, obj_address in self._iterate_heap_from_page(start_page, start_address):
|
|
196
|
+
yield obj, flags, obj_address
|
|
197
|
+
|
|
198
|
+
def _iterate_heap_from_page(self, start_page=0, skip_until_address=None):
|
|
199
|
+
"""Yield all objects from the Ruby heap, starting from a specific page.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
start_page: Page index to start from (default: 0)
|
|
203
|
+
skip_until_address: If specified, calculate the slot index and start from there (for first page only)
|
|
204
|
+
|
|
205
|
+
Yields:
|
|
206
|
+
Tuple of (VALUE, flags, address) for each object on the heap
|
|
207
|
+
"""
|
|
208
|
+
if not self.objspace:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
allocated_pages = int(self.objspace['heap_pages']['allocated_pages'])
|
|
213
|
+
except gdb.MemoryError as e:
|
|
214
|
+
print(f"Error reading heap_pages: {e}")
|
|
215
|
+
print("The heap may not be initialized yet.")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
for i in range(start_page, allocated_pages):
|
|
219
|
+
page = self._get_page(i)
|
|
220
|
+
if page is None:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
start = int(page['start'])
|
|
225
|
+
total_slots = int(page['total_slots'])
|
|
226
|
+
slot_size = int(page['slot_size'])
|
|
227
|
+
except (gdb.MemoryError, gdb.error) as e:
|
|
228
|
+
print(f"Error reading page {i}: {e}", file=sys.stderr)
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
# OPTIMIZATION: Create base pointer once per page
|
|
232
|
+
try:
|
|
233
|
+
base_ptr = gdb.Value(start).cast(self._rbasic_type)
|
|
234
|
+
except (gdb.error, RuntimeError):
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
# For the first page, calculate which slot to start from
|
|
238
|
+
start_slot = 0
|
|
239
|
+
if i == start_page and skip_until_address is not None:
|
|
240
|
+
# Calculate slot index from address
|
|
241
|
+
offset_from_page_start = skip_until_address - start
|
|
242
|
+
start_slot = offset_from_page_start // slot_size
|
|
243
|
+
|
|
244
|
+
# DEBUG
|
|
245
|
+
print(f"DEBUG: Resuming from address 0x{skip_until_address:x}", file=sys.stderr)
|
|
246
|
+
print(f"DEBUG: Page {i} starts at 0x{start:x}, slot_size={slot_size}", file=sys.stderr)
|
|
247
|
+
print(f"DEBUG: Starting at slot {start_slot}", file=sys.stderr)
|
|
248
|
+
|
|
249
|
+
# Ensure we don't go out of bounds
|
|
250
|
+
if start_slot >= total_slots:
|
|
251
|
+
continue # Skip this entire page
|
|
252
|
+
if start_slot < 0:
|
|
253
|
+
start_slot = 0
|
|
254
|
+
|
|
255
|
+
# Iterate through objects using pointer arithmetic (much faster!)
|
|
256
|
+
for j in range(start_slot, total_slots):
|
|
257
|
+
try:
|
|
258
|
+
# Calculate byte offset and address
|
|
259
|
+
byte_offset = j * slot_size
|
|
260
|
+
obj_address = start + byte_offset
|
|
261
|
+
|
|
262
|
+
# Use pointer arithmetic (much faster than creating new Value)
|
|
263
|
+
obj_ptr = (base_ptr.cast(self._char_ptr_type) + byte_offset).cast(self._rbasic_type)
|
|
264
|
+
|
|
265
|
+
# Read the flags
|
|
266
|
+
flags = int(obj_ptr['flags'])
|
|
267
|
+
|
|
268
|
+
# Skip free objects
|
|
269
|
+
if flags == 0:
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
# Yield the VALUE, flags, and address
|
|
273
|
+
obj = obj_ptr.cast(self._value_type)
|
|
274
|
+
yield obj, flags, obj_address
|
|
275
|
+
except (gdb.error, RuntimeError):
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
def find_typed_data(self, data_type, limit=None, progress=False):
|
|
279
|
+
"""Find RTypedData objects matching a specific type.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
data_type: Pointer to rb_data_type_struct to match
|
|
283
|
+
limit: Maximum number of objects to find (None for no limit)
|
|
284
|
+
progress: If True, print progress to stderr
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List of VALUEs (not extracted data pointers) matching the type
|
|
288
|
+
"""
|
|
289
|
+
objects = []
|
|
290
|
+
|
|
291
|
+
# T_DATA constant
|
|
292
|
+
T_DATA = 0x0c
|
|
293
|
+
|
|
294
|
+
# Get RTypedData type for casting
|
|
295
|
+
rtypeddata_type = gdb.lookup_type('struct RTypedData').pointer()
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
if progress:
|
|
299
|
+
allocated_pages = int(self.objspace['heap_pages']['allocated_pages'])
|
|
300
|
+
print(f"Scanning {allocated_pages} heap pages...", file=sys.stderr)
|
|
301
|
+
except (gdb.MemoryError, gdb.error):
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
objects_checked = 0
|
|
305
|
+
|
|
306
|
+
for obj, flags, address in self.iterate_heap():
|
|
307
|
+
# Check if we've reached the limit
|
|
308
|
+
if limit and len(objects) >= limit:
|
|
309
|
+
if progress:
|
|
310
|
+
print(f"Reached limit of {limit} object(s), stopping scan", file=sys.stderr)
|
|
311
|
+
break
|
|
312
|
+
|
|
313
|
+
objects_checked += 1
|
|
314
|
+
|
|
315
|
+
# Print progress every 10000 objects
|
|
316
|
+
if progress and objects_checked % 10000 == 0:
|
|
317
|
+
print(f" Checked {objects_checked} objects, found {len(objects)} match(es)...", file=sys.stderr)
|
|
318
|
+
|
|
319
|
+
# Check if it's T_DATA
|
|
320
|
+
if (flags & RBASIC_FLAGS_TYPE_MASK) != T_DATA:
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
# Cast to RTypedData and check type
|
|
324
|
+
try:
|
|
325
|
+
typed_data = obj.cast(rtypeddata_type)
|
|
326
|
+
|
|
327
|
+
if typed_data['type'] == data_type:
|
|
328
|
+
# Return the VALUE, not the extracted data pointer
|
|
329
|
+
objects.append(obj)
|
|
330
|
+
if progress:
|
|
331
|
+
print(f" Found object #{len(objects)} at VALUE 0x{int(obj):x}", file=sys.stderr)
|
|
332
|
+
except (gdb.error, RuntimeError):
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
if progress:
|
|
336
|
+
if limit and len(objects) >= limit:
|
|
337
|
+
print(f"Scan complete: checked {objects_checked} objects (stopped at limit)", file=sys.stderr)
|
|
338
|
+
else:
|
|
339
|
+
print(f"Scan complete: checked {objects_checked} objects", file=sys.stderr)
|
|
340
|
+
|
|
341
|
+
return objects
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class RubyHeapScanCommand(gdb.Command):
|
|
345
|
+
"""Scan the Ruby heap for objects, optionally filtered by type.
|
|
346
|
+
|
|
347
|
+
Usage: rb-heap-scan [--type TYPE] [--limit N] [--from $heap]
|
|
348
|
+
|
|
349
|
+
TYPE can be:
|
|
350
|
+
- A Ruby type constant like RUBY_T_STRING, RUBY_T_ARRAY, RUBY_T_HASH
|
|
351
|
+
- A numeric value (e.g., 0x05 for T_STRING)
|
|
352
|
+
- Omit --type to scan all objects
|
|
353
|
+
|
|
354
|
+
Options:
|
|
355
|
+
--type TYPE Filter by Ruby type (omit to scan all objects)
|
|
356
|
+
--limit N Stop after finding N objects (default: 10)
|
|
357
|
+
--from ADDR Start scanning from the given address (for pagination)
|
|
358
|
+
|
|
359
|
+
Pagination:
|
|
360
|
+
The address of the last found object is saved to $heap, allowing you to paginate:
|
|
361
|
+
rb-heap-scan --type RUBY_T_STRING --limit 10 # First page
|
|
362
|
+
rb-heap-scan --type RUBY_T_STRING --limit 10 --from $heap # Next page
|
|
363
|
+
|
|
364
|
+
The $heap variable contains the address of the last scanned object.
|
|
365
|
+
|
|
366
|
+
Examples:
|
|
367
|
+
rb-heap-scan --type RUBY_T_STRING
|
|
368
|
+
rb-heap-scan --type RUBY_T_ARRAY --limit 20
|
|
369
|
+
rb-heap-scan --type 0x05 # T_STRING
|
|
370
|
+
rb-heap-scan --limit 100 # All objects
|
|
371
|
+
rb-heap-scan --from $heap # Continue from last scan
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
def __init__(self):
|
|
375
|
+
super(RubyHeapScanCommand, self).__init__("rb-heap-scan", gdb.COMMAND_USER)
|
|
376
|
+
|
|
377
|
+
def usage(self):
|
|
378
|
+
"""Print usage information."""
|
|
379
|
+
print("Usage: rb-heap-scan [--type TYPE] [--limit N] [--from $heap]")
|
|
380
|
+
print("Examples:")
|
|
381
|
+
print(" rb-heap-scan --type RUBY_T_STRING # Find up to 10 strings")
|
|
382
|
+
print(" rb-heap-scan --type RUBY_T_ARRAY --limit 5 # Find up to 5 arrays")
|
|
383
|
+
print(" rb-heap-scan --type 0x05 --limit 100 # Find up to 100 T_STRING objects")
|
|
384
|
+
print(" rb-heap-scan --limit 20 # Scan 20 objects (any type)")
|
|
385
|
+
print(" rb-heap-scan --type RUBY_T_STRING --from $heap # Continue from last scan")
|
|
386
|
+
print()
|
|
387
|
+
print("Pagination:")
|
|
388
|
+
print(" The address of the last object is saved to $heap for pagination:")
|
|
389
|
+
print(" rb-heap-scan --type RUBY_T_STRING --limit 10 # First page")
|
|
390
|
+
print(" rb-heap-scan --type RUBY_T_STRING --from $heap # Next page")
|
|
391
|
+
|
|
392
|
+
def _parse_type(self, type_arg):
|
|
393
|
+
"""Parse a type argument and return the type value.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
type_arg: String type argument (constant name or numeric value)
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
Integer type value, or None on error
|
|
400
|
+
"""
|
|
401
|
+
import constants
|
|
402
|
+
|
|
403
|
+
# Try as a constant name first
|
|
404
|
+
type_value = constants.get(type_arg)
|
|
405
|
+
|
|
406
|
+
if type_value is None:
|
|
407
|
+
# Try parsing as a number (hex or decimal)
|
|
408
|
+
try:
|
|
409
|
+
if type_arg.startswith('0x') or type_arg.startswith('0X'):
|
|
410
|
+
type_value = int(type_arg, 16)
|
|
411
|
+
else:
|
|
412
|
+
type_value = int(type_arg)
|
|
413
|
+
except ValueError:
|
|
414
|
+
print(f"Error: Unknown type constant '{type_arg}'")
|
|
415
|
+
print("Use a constant like RUBY_T_STRING or a numeric value like 0x05")
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
# Validate type value is reasonable (0-31 for the 5-bit type field)
|
|
419
|
+
if not (0 <= type_value <= 31):
|
|
420
|
+
print(f"Warning: Type value {type_value} (0x{type_value:x}) is outside valid range 0-31")
|
|
421
|
+
|
|
422
|
+
return type_value
|
|
423
|
+
|
|
424
|
+
def invoke(self, arg, from_tty):
|
|
425
|
+
"""Execute the heap scan command."""
|
|
426
|
+
try:
|
|
427
|
+
# Parse arguments
|
|
428
|
+
import command
|
|
429
|
+
arguments = command.parse_arguments(arg if arg else "")
|
|
430
|
+
|
|
431
|
+
print(f"DEBUG: Raw arg string: '{arg}'", file=sys.stderr)
|
|
432
|
+
|
|
433
|
+
# Check if we're continuing from a previous scan
|
|
434
|
+
from_option = arguments.get_option('from')
|
|
435
|
+
print(f"DEBUG: from_option = {from_option}", file=sys.stderr)
|
|
436
|
+
if from_option is not None:
|
|
437
|
+
try:
|
|
438
|
+
# $heap should be an address (pointer value)
|
|
439
|
+
from_address = int(gdb.parse_and_eval(from_option))
|
|
440
|
+
print(f"DEBUG: Parsed from_address = 0x{from_address:x}", file=sys.stderr)
|
|
441
|
+
except (gdb.error, ValueError, TypeError) as e:
|
|
442
|
+
# If $heap doesn't exist or is void/invalid, start from the beginning
|
|
443
|
+
print(f"Note: {from_option} is not set or invalid, wrapping around to start of heap", file=sys.stderr)
|
|
444
|
+
from_address = None
|
|
445
|
+
else:
|
|
446
|
+
# New scan
|
|
447
|
+
from_address = None
|
|
448
|
+
|
|
449
|
+
# Get limit (default 10)
|
|
450
|
+
limit = 10
|
|
451
|
+
limit_value = arguments.get_option('limit')
|
|
452
|
+
if limit_value is not None:
|
|
453
|
+
try:
|
|
454
|
+
limit = int(limit_value)
|
|
455
|
+
except (ValueError, TypeError):
|
|
456
|
+
print("Error: --limit must be a number")
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
# Get type (optional)
|
|
460
|
+
type_value = None
|
|
461
|
+
type_option = arguments.get_option('type')
|
|
462
|
+
if type_option is not None:
|
|
463
|
+
type_value = self._parse_type(type_option)
|
|
464
|
+
if type_value is None:
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
# Initialize heap
|
|
468
|
+
heap = RubyHeap()
|
|
469
|
+
if not heap.initialize():
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
# Print search description
|
|
473
|
+
if type_value is not None:
|
|
474
|
+
type_desc = f"type 0x{type_value:02x}"
|
|
475
|
+
else:
|
|
476
|
+
type_desc = "all types"
|
|
477
|
+
|
|
478
|
+
if from_address:
|
|
479
|
+
print(f"Scanning heap for {type_desc}, limit={limit}, continuing from address 0x{from_address:x}...")
|
|
480
|
+
else:
|
|
481
|
+
print(f"Scanning heap for {type_desc}, limit={limit}...")
|
|
482
|
+
print()
|
|
483
|
+
|
|
484
|
+
# Find objects
|
|
485
|
+
objects, next_address = heap.scan(type_value, limit=limit, from_address=from_address)
|
|
486
|
+
|
|
487
|
+
if not objects:
|
|
488
|
+
print("No objects found")
|
|
489
|
+
if from_address:
|
|
490
|
+
print("(You may have reached the end of the heap)")
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
# Import format for terminal output
|
|
494
|
+
import format
|
|
495
|
+
terminal = format.create_terminal(from_tty)
|
|
496
|
+
|
|
497
|
+
# Import value module for interpretation
|
|
498
|
+
import value as value_module
|
|
499
|
+
|
|
500
|
+
print(f"Found {len(objects)} object(s):")
|
|
501
|
+
print()
|
|
502
|
+
|
|
503
|
+
for i, obj in enumerate(objects):
|
|
504
|
+
obj_int = int(obj)
|
|
505
|
+
|
|
506
|
+
# Set as convenience variable
|
|
507
|
+
var_name = f"heap{i}"
|
|
508
|
+
gdb.set_convenience_variable(var_name, obj)
|
|
509
|
+
|
|
510
|
+
# Try to interpret and display the object
|
|
511
|
+
try:
|
|
512
|
+
interpreted = value_module.interpret(obj)
|
|
513
|
+
|
|
514
|
+
print(terminal.print(
|
|
515
|
+
format.metadata, f" [{i}] ",
|
|
516
|
+
format.dim, f"${var_name} = ",
|
|
517
|
+
format.reset, interpreted
|
|
518
|
+
))
|
|
519
|
+
except Exception as e:
|
|
520
|
+
print(terminal.print(
|
|
521
|
+
format.metadata, f" [{i}] ",
|
|
522
|
+
format.dim, f"${var_name} = ",
|
|
523
|
+
format.error, f"<error: {e}>"
|
|
524
|
+
))
|
|
525
|
+
|
|
526
|
+
print()
|
|
527
|
+
print(terminal.print(
|
|
528
|
+
format.dim,
|
|
529
|
+
f"Objects saved in $heap0 through $heap{len(objects)-1}",
|
|
530
|
+
format.reset
|
|
531
|
+
))
|
|
532
|
+
|
|
533
|
+
# Save next address to $heap for pagination
|
|
534
|
+
if next_address is not None:
|
|
535
|
+
# Save the next address to continue from
|
|
536
|
+
gdb.set_convenience_variable('heap', gdb.Value(next_address).cast(gdb.lookup_type('void').pointer()))
|
|
537
|
+
print(terminal.print(
|
|
538
|
+
format.dim,
|
|
539
|
+
f"Next scan address saved to $heap: 0x{next_address:016x}",
|
|
540
|
+
format.reset
|
|
541
|
+
))
|
|
542
|
+
print(terminal.print(
|
|
543
|
+
format.dim,
|
|
544
|
+
f"Run 'rb-heap-scan --type {type_option if type_option else '...'} --from $heap' for next page",
|
|
545
|
+
format.reset
|
|
546
|
+
))
|
|
547
|
+
else:
|
|
548
|
+
# Reached the end of the heap - unset $heap so next scan starts fresh
|
|
549
|
+
gdb.set_convenience_variable('heap', None)
|
|
550
|
+
print(terminal.print(
|
|
551
|
+
format.dim,
|
|
552
|
+
f"Reached end of heap (no more objects to scan)",
|
|
553
|
+
format.reset
|
|
554
|
+
))
|
|
555
|
+
|
|
556
|
+
except Exception as e:
|
|
557
|
+
print(f"Error: {e}")
|
|
558
|
+
import traceback
|
|
559
|
+
traceback.print_exc()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# Register commands
|
|
563
|
+
RubyHeapScanCommand()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ruby GDB Extensions Initialization
|
|
3
|
+
|
|
4
|
+
This module loads all Ruby debugging extensions for GDB.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import gdb
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
# Get the directory containing this file
|
|
12
|
+
ruby_gdb_dir = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
|
|
14
|
+
# Add to Python path for imports
|
|
15
|
+
if ruby_gdb_dir not in sys.path:
|
|
16
|
+
sys.path.insert(0, ruby_gdb_dir)
|
|
17
|
+
|
|
18
|
+
# Load object inspection extensions:
|
|
19
|
+
import object
|
|
20
|
+
|
|
21
|
+
# Load fiber debugging extensions:
|
|
22
|
+
import fiber
|
|
23
|
+
|
|
24
|
+
# Load stack inspection extensions:
|
|
25
|
+
import stack
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import gdb
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# Import utilities
|
|
5
|
+
import command
|
|
6
|
+
import constants
|
|
7
|
+
import value
|
|
8
|
+
import rstring
|
|
9
|
+
import rarray
|
|
10
|
+
import rhash
|
|
11
|
+
import rsymbol
|
|
12
|
+
import rstruct
|
|
13
|
+
import rfloat
|
|
14
|
+
import rbignum
|
|
15
|
+
import rbasic
|
|
16
|
+
import format
|
|
17
|
+
|
|
18
|
+
class RubyObjectPrintCommand(gdb.Command):
|
|
19
|
+
"""Recursively print Ruby hash and array structures.
|
|
20
|
+
Usage: rb-object-print <expression> [max_depth] [--debug]
|
|
21
|
+
Examples:
|
|
22
|
+
rb-object-print $errinfo # Print exception object
|
|
23
|
+
rb-object-print $ec->storage # Print fiber storage
|
|
24
|
+
rb-object-print 0x7f7a12345678 # Print object at address
|
|
25
|
+
rb-object-print $var 2 # Print with max depth 2
|
|
26
|
+
|
|
27
|
+
Default max_depth is 1 if not specified.
|
|
28
|
+
Add --debug flag to enable debug output."""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super(RubyObjectPrintCommand, self).__init__("rb-object-print", gdb.COMMAND_DATA)
|
|
32
|
+
|
|
33
|
+
def usage(self):
|
|
34
|
+
"""Print usage information."""
|
|
35
|
+
print("Usage: rb-object-print <expression> [--depth N] [--debug]")
|
|
36
|
+
print("Examples:")
|
|
37
|
+
print(" rb-object-print $errinfo")
|
|
38
|
+
print(" rb-object-print $ec->storage --depth 2")
|
|
39
|
+
print(" rb-object-print foo + 10")
|
|
40
|
+
print(" rb-object-print $ec->cfp->sp[-1] --depth 3 --debug")
|
|
41
|
+
|
|
42
|
+
def invoke(self, argument, from_tty):
|
|
43
|
+
# Parse arguments using the robust parser
|
|
44
|
+
arguments = command.parse_arguments(argument if argument else "")
|
|
45
|
+
|
|
46
|
+
# Validate that we have at least one expression
|
|
47
|
+
if not arguments.expressions:
|
|
48
|
+
self.usage()
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
# Apply flags
|
|
52
|
+
debug_mode = arguments.has_flag('debug')
|
|
53
|
+
|
|
54
|
+
# Apply options
|
|
55
|
+
max_depth = arguments.get_option('depth', 1)
|
|
56
|
+
|
|
57
|
+
# Validate depth
|
|
58
|
+
if max_depth < 1:
|
|
59
|
+
print("Error: --depth must be >= 1")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
# Create terminal and printer
|
|
63
|
+
terminal = format.create_terminal(from_tty)
|
|
64
|
+
printer = format.Printer(terminal, max_depth, debug_mode)
|
|
65
|
+
|
|
66
|
+
# Process each expression
|
|
67
|
+
for expression in arguments.expressions:
|
|
68
|
+
try:
|
|
69
|
+
# Evaluate the expression
|
|
70
|
+
ruby_value = gdb.parse_and_eval(expression)
|
|
71
|
+
printer.debug(f"Evaluated '{expression}' to 0x{int(ruby_value):x}")
|
|
72
|
+
|
|
73
|
+
# Interpret the value and let it print itself recursively
|
|
74
|
+
ruby_object = value.interpret(ruby_value)
|
|
75
|
+
ruby_object.print_recursive(printer, max_depth)
|
|
76
|
+
except gdb.error as e:
|
|
77
|
+
print(f"Error evaluating expression '{expression}': {e}")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"Error processing '{expression}': {type(e).__name__}: {e}")
|
|
80
|
+
if debug_mode:
|
|
81
|
+
import traceback
|
|
82
|
+
traceback.print_exc(file=sys.stderr)
|
|
83
|
+
|
|
84
|
+
# Register command
|
|
85
|
+
RubyObjectPrintCommand()
|