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,825 @@
|
|
|
1
|
+
import gdb
|
|
2
|
+
import gdb.unwinder
|
|
3
|
+
import re
|
|
4
|
+
import struct
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
# Import command parser
|
|
10
|
+
import command
|
|
11
|
+
import value
|
|
12
|
+
import format
|
|
13
|
+
import heap
|
|
14
|
+
import rexception
|
|
15
|
+
|
|
16
|
+
# Global cache of fibers
|
|
17
|
+
_fiber_cache = []
|
|
18
|
+
|
|
19
|
+
# Global fiber unwinder instance
|
|
20
|
+
_fiber_unwinder = None
|
|
21
|
+
|
|
22
|
+
# Global current fiber (managed by rb-fiber-switch command)
|
|
23
|
+
_current_fiber = None
|
|
24
|
+
|
|
25
|
+
def parse_fiber_index(arg):
|
|
26
|
+
"""Parse fiber index from argument string.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
(index, error_message) - index is None if parsing failed
|
|
30
|
+
"""
|
|
31
|
+
if not arg or not arg.strip():
|
|
32
|
+
return None, "Usage: provide <index>"
|
|
33
|
+
|
|
34
|
+
arguments = command.parse_arguments(arg)
|
|
35
|
+
|
|
36
|
+
if not arguments.expressions:
|
|
37
|
+
return None, "Error: No index provided"
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
index = int(arguments.expressions[0])
|
|
41
|
+
return index, None
|
|
42
|
+
except ValueError:
|
|
43
|
+
return None, f"Error: invalid index '{arguments.expressions[0]}'"
|
|
44
|
+
|
|
45
|
+
def get_current_fiber():
|
|
46
|
+
"""Get the currently selected fiber (if any).
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
RubyFiber instance or None
|
|
50
|
+
"""
|
|
51
|
+
return _current_fiber
|
|
52
|
+
|
|
53
|
+
def set_current_fiber(fiber):
|
|
54
|
+
"""Set the currently selected fiber.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
fiber: RubyFiber instance or None
|
|
58
|
+
|
|
59
|
+
Note: Should only be called by rb-fiber-switch command.
|
|
60
|
+
"""
|
|
61
|
+
global _current_fiber
|
|
62
|
+
_current_fiber = fiber
|
|
63
|
+
|
|
64
|
+
class RubyFiber:
|
|
65
|
+
"""Wrapper for Ruby Fiber objects.
|
|
66
|
+
|
|
67
|
+
Wraps a Fiber VALUE and provides high-level interface for fiber introspection.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Fiber status constants
|
|
71
|
+
FIBER_STATUS = {
|
|
72
|
+
0: "CREATED",
|
|
73
|
+
1: "RESUMED",
|
|
74
|
+
2: "SUSPENDED",
|
|
75
|
+
3: "TERMINATED"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def __init__(self, fiber_value):
|
|
79
|
+
"""Initialize with a Fiber VALUE.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
fiber_value: A GDB value representing a Ruby Fiber object (VALUE)
|
|
83
|
+
"""
|
|
84
|
+
self.value = fiber_value
|
|
85
|
+
self._pointer = None
|
|
86
|
+
self._ec = None
|
|
87
|
+
self._exception = None
|
|
88
|
+
|
|
89
|
+
def _extract_fiber_pointer(self):
|
|
90
|
+
"""Extract struct rb_fiber_struct* from the Fiber VALUE."""
|
|
91
|
+
if self._pointer is None:
|
|
92
|
+
# Cast to RTypedData and extract the data pointer
|
|
93
|
+
rtypeddata_type = gdb.lookup_type('struct RTypedData').pointer()
|
|
94
|
+
typed_data = self.value.cast(rtypeddata_type)
|
|
95
|
+
|
|
96
|
+
rb_fiber_struct_type = gdb.lookup_type('struct rb_fiber_struct').pointer()
|
|
97
|
+
self._pointer = typed_data['data'].cast(rb_fiber_struct_type)
|
|
98
|
+
|
|
99
|
+
return self._pointer
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def pointer(self):
|
|
103
|
+
"""Get the struct rb_fiber_struct* pointer."""
|
|
104
|
+
return self._extract_fiber_pointer()
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def address(self):
|
|
108
|
+
"""Get the raw address of the fiber struct."""
|
|
109
|
+
return int(self.pointer)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def status(self):
|
|
113
|
+
"""Get fiber status as string (CREATED, RESUMED, etc.)."""
|
|
114
|
+
status_code = int(self.pointer['status'])
|
|
115
|
+
return self.FIBER_STATUS.get(status_code, f"UNKNOWN({status_code})")
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def stack_base(self):
|
|
119
|
+
"""Get fiber stack base pointer."""
|
|
120
|
+
return self.pointer['stack']['base']
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def stack_size(self):
|
|
124
|
+
"""Get fiber stack size."""
|
|
125
|
+
return int(self.pointer['stack']['size'])
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def ec(self):
|
|
129
|
+
"""Get execution context (rb_execution_context_t*)."""
|
|
130
|
+
if self._ec is None:
|
|
131
|
+
self._ec = self.pointer['cont']['saved_ec'].address
|
|
132
|
+
return self._ec
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def vm_stack(self):
|
|
136
|
+
"""Get VM stack pointer."""
|
|
137
|
+
return self.ec['vm_stack']
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def vm_stack_size(self):
|
|
141
|
+
"""Get VM stack size."""
|
|
142
|
+
return int(self.ec['vm_stack_size'])
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def cfp(self):
|
|
146
|
+
"""Get control frame pointer."""
|
|
147
|
+
return self.ec['cfp']
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def exception(self):
|
|
151
|
+
"""Get current exception RException object (if any).
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
RException instance or None
|
|
155
|
+
"""
|
|
156
|
+
if self._exception is None:
|
|
157
|
+
try:
|
|
158
|
+
errinfo_val = self.ec['errinfo']
|
|
159
|
+
|
|
160
|
+
# Only process if it's a real object (not nil or other immediate value)
|
|
161
|
+
if value.is_object(errinfo_val) and not value.is_nil(errinfo_val):
|
|
162
|
+
try:
|
|
163
|
+
self._exception = rexception.RException(errinfo_val)
|
|
164
|
+
except Exception:
|
|
165
|
+
# If we can't create RException, return None
|
|
166
|
+
pass
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
return self._exception
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def exception_info(self):
|
|
174
|
+
"""Get formatted exception string (if any).
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Formatted exception string or None
|
|
178
|
+
"""
|
|
179
|
+
exc = self.exception
|
|
180
|
+
if exc:
|
|
181
|
+
return str(exc)
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def print_info(self, terminal):
|
|
185
|
+
"""Print summary information about this fiber.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
terminal: Terminal instance for formatted output
|
|
189
|
+
"""
|
|
190
|
+
# Print fiber VALUE and address
|
|
191
|
+
print(f"Fiber VALUE: ", end='')
|
|
192
|
+
print(terminal.print_type_tag('T_DATA', int(self.value), None))
|
|
193
|
+
print(f" Address: ", end='')
|
|
194
|
+
print(terminal.print_type_tag('struct rb_fiber_struct', self.address, None))
|
|
195
|
+
|
|
196
|
+
# Print status
|
|
197
|
+
print(f" Status: {self.status}")
|
|
198
|
+
|
|
199
|
+
# Print exception if present
|
|
200
|
+
exc_info = self.exception_info
|
|
201
|
+
if exc_info:
|
|
202
|
+
print(f" Exception: {exc_info}")
|
|
203
|
+
|
|
204
|
+
# Print Stack with formatted pointer
|
|
205
|
+
stack_type = str(self.stack_base.type)
|
|
206
|
+
print(f" Stack: ", end='')
|
|
207
|
+
print(terminal.print_type_tag(stack_type, int(self.stack_base), f'size={self.stack_size}'))
|
|
208
|
+
|
|
209
|
+
# Print VM Stack with formatted pointer
|
|
210
|
+
vm_stack_type = str(self.vm_stack.type)
|
|
211
|
+
print(f" VM Stack: ", end='')
|
|
212
|
+
print(terminal.print_type_tag(vm_stack_type, int(self.vm_stack), f'size={self.vm_stack_size}'))
|
|
213
|
+
|
|
214
|
+
# Print CFP
|
|
215
|
+
print(f" CFP: ", end='')
|
|
216
|
+
print(terminal.print_type_tag('rb_control_frame_t', int(self.cfp), None))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class RubyFiberScanHeapCommand(gdb.Command):
|
|
220
|
+
"""Scan heap and list all Ruby fibers.
|
|
221
|
+
|
|
222
|
+
Usage: rb-fiber-scan-heap [limit] [--cache [filename]]
|
|
223
|
+
Examples: rb-fiber-scan-heap # Find all fibers
|
|
224
|
+
rb-fiber-scan-heap 10 # Find first 10 fibers
|
|
225
|
+
rb-fiber-scan-heap --cache # Use fibers.json cache
|
|
226
|
+
rb-fiber-scan-heap --cache my.json # Use custom cache file
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(self):
|
|
230
|
+
super(RubyFiberScanHeapCommand, self).__init__("rb-fiber-scan-heap", gdb.COMMAND_USER)
|
|
231
|
+
self.heap = heap.RubyHeap()
|
|
232
|
+
|
|
233
|
+
def usage(self):
|
|
234
|
+
"""Print usage information."""
|
|
235
|
+
print("Usage: rb-fiber-scan-heap [limit] [--cache [filename]]")
|
|
236
|
+
print("Examples:")
|
|
237
|
+
print(" rb-fiber-scan-heap # Find all fibers")
|
|
238
|
+
print(" rb-fiber-scan-heap 10 # Find first 10 fibers")
|
|
239
|
+
print(" rb-fiber-scan-heap --cache # Use fibers.json cache")
|
|
240
|
+
print(" rb-fiber-scan-heap --cache my.json # Use custom cache file")
|
|
241
|
+
|
|
242
|
+
def save_cache(self, fiber_values, filename):
|
|
243
|
+
"""Save fiber VALUE addresses to cache file.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
fiber_values: List of Fiber VALUEs
|
|
247
|
+
filename: Path to cache file
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
data = {
|
|
251
|
+
'version': 1,
|
|
252
|
+
'fiber_count': len(fiber_values),
|
|
253
|
+
'fibers': [int(f) for f in fiber_values] # Store VALUE addresses
|
|
254
|
+
}
|
|
255
|
+
with open(filename, 'w') as f:
|
|
256
|
+
json.dump(data, f, indent=2)
|
|
257
|
+
print(f"Saved {len(fiber_values)} fiber VALUE(s) to {filename}")
|
|
258
|
+
return True
|
|
259
|
+
except Exception as e:
|
|
260
|
+
print(f"Warning: Failed to save cache: {e}")
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
def load_cache(self, filename):
|
|
264
|
+
"""Load fiber VALUE addresses from cache file.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
filename: Path to cache file
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of VALUEs or None if loading failed
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
with open(filename, 'r') as f:
|
|
274
|
+
data = json.load(f)
|
|
275
|
+
|
|
276
|
+
if data.get('version') != 1:
|
|
277
|
+
print(f"Warning: Unknown cache version, ignoring cache")
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
fiber_addrs = data.get('fibers', [])
|
|
281
|
+
print(f"Loaded {len(fiber_addrs)} fiber VALUE address(es) from {filename}")
|
|
282
|
+
|
|
283
|
+
# Initialize heap to ensure we have type information
|
|
284
|
+
if not self.heap.initialize():
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
# Reconstruct VALUEs from addresses
|
|
288
|
+
value_type = gdb.lookup_type('VALUE')
|
|
289
|
+
fibers = []
|
|
290
|
+
for addr in fiber_addrs:
|
|
291
|
+
try:
|
|
292
|
+
fiber_val = gdb.Value(addr).cast(value_type)
|
|
293
|
+
fibers.append(fiber_val)
|
|
294
|
+
except (gdb.error, gdb.MemoryError):
|
|
295
|
+
print(f"Warning: Could not access VALUE at 0x{addr:x}")
|
|
296
|
+
|
|
297
|
+
print(f"Successfully reconstructed {len(fibers)} fiber VALUE(s)")
|
|
298
|
+
return fibers
|
|
299
|
+
|
|
300
|
+
except FileNotFoundError:
|
|
301
|
+
return None
|
|
302
|
+
except Exception as e:
|
|
303
|
+
print(f"Warning: Failed to load cache: {e}")
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
def invoke(self, arg, from_tty):
|
|
307
|
+
global _fiber_cache
|
|
308
|
+
|
|
309
|
+
# Create terminal for formatting
|
|
310
|
+
terminal = format.create_terminal(from_tty)
|
|
311
|
+
|
|
312
|
+
# Parse arguments using the robust parser
|
|
313
|
+
arguments = command.parse_arguments(arg if arg else "")
|
|
314
|
+
|
|
315
|
+
# Get limit from first expression (positional argument)
|
|
316
|
+
limit = None
|
|
317
|
+
if arguments.expressions:
|
|
318
|
+
try:
|
|
319
|
+
limit = int(arguments.expressions[0])
|
|
320
|
+
if limit <= 0:
|
|
321
|
+
print("Error: limit must be positive")
|
|
322
|
+
self.usage()
|
|
323
|
+
return
|
|
324
|
+
except ValueError:
|
|
325
|
+
print(f"Error: invalid limit '{arguments.expressions[0]}'")
|
|
326
|
+
self.usage()
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# Check for --cache flag
|
|
330
|
+
use_cache = arguments.has_flag('cache')
|
|
331
|
+
cache_file = arguments.get_option('cache', 'fibers.json')
|
|
332
|
+
|
|
333
|
+
# Try to load from cache if requested
|
|
334
|
+
if use_cache:
|
|
335
|
+
loaded_fibers = self.load_cache(cache_file)
|
|
336
|
+
if loaded_fibers is not None:
|
|
337
|
+
# Successfully loaded from cache
|
|
338
|
+
_fiber_cache = loaded_fibers
|
|
339
|
+
|
|
340
|
+
print(f"\nLoaded {len(loaded_fibers)} fiber(s) from cache:\n")
|
|
341
|
+
|
|
342
|
+
for i, fiber_val in enumerate(loaded_fibers):
|
|
343
|
+
try:
|
|
344
|
+
fiber_obj = RubyFiber(fiber_val)
|
|
345
|
+
self._print_fiber_info(terminal, i, fiber_obj)
|
|
346
|
+
except:
|
|
347
|
+
print(f"Fiber #{i}: VALUE 0x{int(fiber_val):x}")
|
|
348
|
+
print(f" (error creating RubyFiber)")
|
|
349
|
+
print()
|
|
350
|
+
|
|
351
|
+
print(f"Fibers cached. Use 'rb-fiber-scan-switch <index>' to switch to a fiber.")
|
|
352
|
+
return
|
|
353
|
+
else:
|
|
354
|
+
print(f"Cache file '{cache_file}' not found, proceeding with scan...")
|
|
355
|
+
print()
|
|
356
|
+
|
|
357
|
+
# Initialize heap scanner
|
|
358
|
+
if not self.heap.initialize():
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
# Get fiber_data_type for matching
|
|
362
|
+
fiber_data_type = gdb.parse_and_eval('&fiber_data_type')
|
|
363
|
+
|
|
364
|
+
if limit:
|
|
365
|
+
print(f"Scanning heap for first {limit} Fiber object(s)...", file=sys.stderr)
|
|
366
|
+
else:
|
|
367
|
+
print("Scanning heap for Fiber objects...", file=sys.stderr)
|
|
368
|
+
|
|
369
|
+
# Use RubyHeap to find fibers (returns VALUEs)
|
|
370
|
+
fiber_values = self.heap.find_typed_data(fiber_data_type, limit=limit, progress=True)
|
|
371
|
+
|
|
372
|
+
# Cache the VALUEs for later use
|
|
373
|
+
_fiber_cache = fiber_values
|
|
374
|
+
|
|
375
|
+
if limit and len(fiber_values) >= limit:
|
|
376
|
+
print(f"\nFound {len(fiber_values)} fiber(s) (limit reached):\n")
|
|
377
|
+
else:
|
|
378
|
+
print(f"\nFound {len(fiber_values)} fiber(s):\n")
|
|
379
|
+
|
|
380
|
+
for i, fiber_val in enumerate(fiber_values):
|
|
381
|
+
fiber_obj = RubyFiber(fiber_val)
|
|
382
|
+
self._print_fiber_info(terminal, i, fiber_obj)
|
|
383
|
+
|
|
384
|
+
# Save to cache if requested
|
|
385
|
+
if use_cache and fiber_values:
|
|
386
|
+
self.save_cache(fiber_values, cache_file)
|
|
387
|
+
print()
|
|
388
|
+
|
|
389
|
+
print(f"Fibers cached. Use 'rb-fiber-scan-switch <index>' to switch to a fiber.")
|
|
390
|
+
|
|
391
|
+
def _print_fiber_info(self, terminal, index, fiber_obj):
|
|
392
|
+
"""Print formatted fiber information.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
terminal: Terminal instance for formatting
|
|
396
|
+
index: Fiber index in cache
|
|
397
|
+
fiber_obj: RubyFiber instance
|
|
398
|
+
"""
|
|
399
|
+
# Print fiber index with VALUE and pointer
|
|
400
|
+
print(f"Fiber #{index}: ", end='')
|
|
401
|
+
print(terminal.print_type_tag('T_DATA', int(fiber_obj.value)), end='')
|
|
402
|
+
print(' → ', end='')
|
|
403
|
+
print(terminal.print_type_tag('struct rb_fiber_struct', fiber_obj.address))
|
|
404
|
+
|
|
405
|
+
# Print status
|
|
406
|
+
print(f" Status: {fiber_obj.status}")
|
|
407
|
+
|
|
408
|
+
# Print exception if present
|
|
409
|
+
exc_info = fiber_obj.exception_info
|
|
410
|
+
if exc_info:
|
|
411
|
+
print(f" Exception: {exc_info}")
|
|
412
|
+
|
|
413
|
+
# Print Stack with formatted pointer
|
|
414
|
+
stack_type = str(fiber_obj.stack_base.type)
|
|
415
|
+
print(f" Stack: ", end='')
|
|
416
|
+
print(terminal.print_type_tag(stack_type, int(fiber_obj.stack_base), f'size={fiber_obj.stack_size}'))
|
|
417
|
+
|
|
418
|
+
# Print VM Stack with formatted pointer
|
|
419
|
+
vm_stack_type = str(fiber_obj.vm_stack.type)
|
|
420
|
+
print(f" VM Stack: ", end='')
|
|
421
|
+
print(terminal.print_type_tag(vm_stack_type, int(fiber_obj.vm_stack), f'size={fiber_obj.vm_stack_size}'))
|
|
422
|
+
|
|
423
|
+
# Print CFP
|
|
424
|
+
cfp_type = str(fiber_obj.cfp.type).replace(' *', '') # Remove pointer marker for display
|
|
425
|
+
print(f" CFP: ", end='')
|
|
426
|
+
print(terminal.print_type_tag(cfp_type, int(fiber_obj.cfp)))
|
|
427
|
+
print()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class RubyFiberScanSwitchCommand(gdb.Command):
|
|
431
|
+
"""Switch to a fiber from the scan heap cache.
|
|
432
|
+
|
|
433
|
+
Usage: rb-fiber-scan-switch <index>
|
|
434
|
+
Example: rb-fiber-scan-switch 0
|
|
435
|
+
rb-fiber-scan-switch 2
|
|
436
|
+
|
|
437
|
+
Note: This command requires a fiber cache populated by 'rb-fiber-scan-heap'.
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
def __init__(self):
|
|
441
|
+
super(RubyFiberScanSwitchCommand, self).__init__("rb-fiber-scan-switch", gdb.COMMAND_USER)
|
|
442
|
+
|
|
443
|
+
def usage(self):
|
|
444
|
+
"""Print usage information."""
|
|
445
|
+
print("Usage: rb-fiber-scan-switch <index>")
|
|
446
|
+
print("Examples:")
|
|
447
|
+
print(" rb-fiber-scan-switch 0 # Switch to fiber #0")
|
|
448
|
+
print(" rb-fiber-scan-switch 2 # Switch to fiber #2")
|
|
449
|
+
print()
|
|
450
|
+
print("Note: Run 'rb-fiber-scan-heap' first to populate the fiber cache.")
|
|
451
|
+
|
|
452
|
+
def invoke(self, arg, from_tty):
|
|
453
|
+
global _fiber_cache
|
|
454
|
+
|
|
455
|
+
if not arg or not arg.strip():
|
|
456
|
+
self.usage()
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
# Check if cache is populated
|
|
460
|
+
if not _fiber_cache:
|
|
461
|
+
print("Error: No fibers in cache. Run 'rb-fiber-scan-heap' first.")
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
# Parse index
|
|
465
|
+
try:
|
|
466
|
+
index = int(arg.strip())
|
|
467
|
+
except ValueError:
|
|
468
|
+
print(f"Error: Invalid index '{arg}'. Must be an integer.")
|
|
469
|
+
self.usage()
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
# Validate index
|
|
473
|
+
if index < 0 or index >= len(_fiber_cache):
|
|
474
|
+
print(f"Error: Index {index} out of range [0, {len(_fiber_cache)-1}]")
|
|
475
|
+
print(f"\nRun 'rb-fiber-scan-heap' to see available fibers.")
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
# Get fiber VALUE from cache
|
|
479
|
+
fiber_value = _fiber_cache[index]
|
|
480
|
+
|
|
481
|
+
print(f"Switching to Fiber #{index}: VALUE 0x{int(fiber_value):x}")
|
|
482
|
+
|
|
483
|
+
# Delegate to rb-fiber-switch command
|
|
484
|
+
# This command manages the global _current_fiber state
|
|
485
|
+
gdb.execute(f"rb-fiber-switch 0x{int(fiber_value):x}", from_tty=from_tty)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class RubyFiberUnwinder(gdb.unwinder.Unwinder):
|
|
489
|
+
"""Custom unwinder for Ruby fibers.
|
|
490
|
+
|
|
491
|
+
This allows GDB to unwind a fiber's stack even in a core dump,
|
|
492
|
+
by extracting saved register state from the fiber's jmp_buf.
|
|
493
|
+
|
|
494
|
+
Based on similar technique from Facebook Folly:
|
|
495
|
+
https://github.com/facebook/folly/blob/main/folly/fibers/scripts/gdb.py
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
def __init__(self):
|
|
499
|
+
super(RubyFiberUnwinder, self).__init__("Ruby Fiber Unwinder")
|
|
500
|
+
self.active_fiber = None
|
|
501
|
+
self.unwound_first_frame = False
|
|
502
|
+
|
|
503
|
+
def __call__(self, pending_frame):
|
|
504
|
+
"""Called by GDB when unwinding frames."""
|
|
505
|
+
# Only unwind if we have an active fiber set
|
|
506
|
+
if not self.active_fiber:
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
# Only unwind the first frame, then let GDB continue normally
|
|
510
|
+
if self.unwound_first_frame:
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
# Ruby uses its own coroutine implementation, not setjmp/longjmp!
|
|
515
|
+
# Registers are saved in fiber->context.stack_pointer
|
|
516
|
+
# See coroutine/amd64/Context.S for the layout
|
|
517
|
+
|
|
518
|
+
coroutine_ctx = self.active_fiber['context']
|
|
519
|
+
stack_ptr = coroutine_ctx['stack_pointer']
|
|
520
|
+
|
|
521
|
+
# The stack_pointer points to the saved register area
|
|
522
|
+
# From Context.S (x86-64):
|
|
523
|
+
# [stack_pointer + 0] = R15
|
|
524
|
+
# [stack_pointer + 8] = R14
|
|
525
|
+
# [stack_pointer + 16] = R13
|
|
526
|
+
# [stack_pointer + 24] = R12
|
|
527
|
+
# [stack_pointer + 32] = RBX
|
|
528
|
+
# [stack_pointer + 40] = RBP
|
|
529
|
+
# [stack_pointer + 48] = Return address (RIP)
|
|
530
|
+
|
|
531
|
+
if int(stack_ptr) == 0:
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
# Cast to uint64 pointer to read saved registers
|
|
535
|
+
uint64_ptr = stack_ptr.cast(gdb.lookup_type('uint64_t').pointer())
|
|
536
|
+
|
|
537
|
+
# Read saved registers (keep as gdb.Value)
|
|
538
|
+
r15 = uint64_ptr[0]
|
|
539
|
+
r14 = uint64_ptr[1]
|
|
540
|
+
r13 = uint64_ptr[2]
|
|
541
|
+
r12 = uint64_ptr[3]
|
|
542
|
+
rbx = uint64_ptr[4]
|
|
543
|
+
rbp = uint64_ptr[5]
|
|
544
|
+
|
|
545
|
+
# After coroutine_transfer executes 'addq $48, %rsp', RSP points to the return address
|
|
546
|
+
# After 'ret' pops the return address, RSP = stack_ptr + 48 + 8
|
|
547
|
+
# We want to create an unwind frame AS IF we're in the caller of coroutine_transfer
|
|
548
|
+
# So RSP should be pointing AFTER the return address was popped
|
|
549
|
+
rsp_value = int(stack_ptr) + 48 + 8
|
|
550
|
+
rsp = gdb.Value(rsp_value).cast(gdb.lookup_type('uint64_t'))
|
|
551
|
+
|
|
552
|
+
# The return address (RIP) is at [stack_ptr + 48]
|
|
553
|
+
# This is what 'ret' will pop and jump to
|
|
554
|
+
rip_ptr = gdb.Value(int(stack_ptr) + 48).cast(gdb.lookup_type('uint64_t').pointer())
|
|
555
|
+
rip = rip_ptr.dereference()
|
|
556
|
+
|
|
557
|
+
# Sanity check
|
|
558
|
+
if int(rsp) == 0 or int(rip) == 0:
|
|
559
|
+
return None
|
|
560
|
+
|
|
561
|
+
# Create frame ID
|
|
562
|
+
frame_id = gdb.unwinder.FrameId(int(rsp), int(rip))
|
|
563
|
+
|
|
564
|
+
# Create unwind info
|
|
565
|
+
unwind_info = pending_frame.create_unwind_info(frame_id)
|
|
566
|
+
|
|
567
|
+
# Add saved registers
|
|
568
|
+
unwind_info.add_saved_register("rip", rip)
|
|
569
|
+
unwind_info.add_saved_register("rsp", rsp)
|
|
570
|
+
unwind_info.add_saved_register("rbp", rbp)
|
|
571
|
+
unwind_info.add_saved_register("rbx", rbx)
|
|
572
|
+
unwind_info.add_saved_register("r12", r12)
|
|
573
|
+
unwind_info.add_saved_register("r13", r13)
|
|
574
|
+
unwind_info.add_saved_register("r14", r14)
|
|
575
|
+
unwind_info.add_saved_register("r15", r15)
|
|
576
|
+
|
|
577
|
+
# Mark that we've unwound the first frame
|
|
578
|
+
self.unwound_first_frame = True
|
|
579
|
+
|
|
580
|
+
return unwind_info
|
|
581
|
+
|
|
582
|
+
except (gdb.error, gdb.MemoryError) as e:
|
|
583
|
+
# If we can't read the fiber context, bail
|
|
584
|
+
return None
|
|
585
|
+
|
|
586
|
+
def activate_fiber(self, fiber):
|
|
587
|
+
"""Activate unwinding for a specific fiber."""
|
|
588
|
+
self.active_fiber = fiber
|
|
589
|
+
self.unwound_first_frame = False
|
|
590
|
+
gdb.invalidate_cached_frames()
|
|
591
|
+
|
|
592
|
+
def deactivate(self):
|
|
593
|
+
"""Deactivate fiber unwinding."""
|
|
594
|
+
self.active_fiber = None
|
|
595
|
+
self.unwound_first_frame = False
|
|
596
|
+
gdb.invalidate_cached_frames()
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class RubyFiberSwitchCommand(gdb.Command):
|
|
600
|
+
"""Switch GDB's stack view to a specific fiber.
|
|
601
|
+
|
|
602
|
+
Usage: rb-fiber-switch <fiber_value_or_address>
|
|
603
|
+
rb-fiber-switch off
|
|
604
|
+
|
|
605
|
+
Examples:
|
|
606
|
+
rb-fiber-switch 0x7fffdc409ca8 # VALUE address
|
|
607
|
+
rb-fiber-switch $fiber_val # GDB variable
|
|
608
|
+
rb-fiber-switch off # Deactivate unwinder
|
|
609
|
+
|
|
610
|
+
This uses a custom unwinder to make GDB follow the fiber's saved
|
|
611
|
+
stack, allowing you to use 'bt', 'up', 'down', 'frame', etc.
|
|
612
|
+
Works even with core dumps!
|
|
613
|
+
|
|
614
|
+
Based on technique from Facebook Folly fibers.
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
def __init__(self):
|
|
618
|
+
super(RubyFiberSwitchCommand, self).__init__("rb-fiber-switch", gdb.COMMAND_USER)
|
|
619
|
+
self._ensure_unwinder()
|
|
620
|
+
|
|
621
|
+
def usage(self):
|
|
622
|
+
"""Print usage information."""
|
|
623
|
+
print("Usage: rb-fiber-switch <fiber_value_or_address>")
|
|
624
|
+
print(" rb-fiber-switch off")
|
|
625
|
+
print("Examples:")
|
|
626
|
+
print(" rb-fiber-switch 0x7fffdc409ca8 # VALUE address")
|
|
627
|
+
print(" rb-fiber-switch $fiber # GDB variable")
|
|
628
|
+
print(" rb-fiber-switch off # Deactivate unwinder")
|
|
629
|
+
print()
|
|
630
|
+
print("After switching, you can use: bt, up, down, frame, info locals, etc.")
|
|
631
|
+
|
|
632
|
+
def _ensure_unwinder(self):
|
|
633
|
+
"""Ensure the fiber unwinder is registered."""
|
|
634
|
+
global _fiber_unwinder
|
|
635
|
+
if _fiber_unwinder is None:
|
|
636
|
+
_fiber_unwinder = RubyFiberUnwinder()
|
|
637
|
+
gdb.unwinder.register_unwinder(None, _fiber_unwinder, replace=True)
|
|
638
|
+
|
|
639
|
+
def invoke(self, arg, from_tty):
|
|
640
|
+
global _fiber_unwinder
|
|
641
|
+
|
|
642
|
+
if not arg:
|
|
643
|
+
self.usage()
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
# Check for deactivate
|
|
647
|
+
if arg and arg.lower() in ('off', 'none', 'deactivate'):
|
|
648
|
+
_fiber_unwinder.deactivate()
|
|
649
|
+
set_current_fiber(None)
|
|
650
|
+
print("Fiber unwinder deactivated. Switched back to normal stack view.")
|
|
651
|
+
print("Try: bt")
|
|
652
|
+
return
|
|
653
|
+
|
|
654
|
+
# Parse the argument as a VALUE
|
|
655
|
+
try:
|
|
656
|
+
# Evaluate the expression to get a VALUE
|
|
657
|
+
fiber_value = gdb.parse_and_eval(arg)
|
|
658
|
+
|
|
659
|
+
# Ensure it's cast to VALUE type
|
|
660
|
+
value_type = gdb.lookup_type('VALUE')
|
|
661
|
+
fiber_value = fiber_value.cast(value_type)
|
|
662
|
+
|
|
663
|
+
except (gdb.error, RuntimeError) as e:
|
|
664
|
+
print(f"Error: Could not evaluate '{arg}' as a VALUE")
|
|
665
|
+
print(f"Details: {e}")
|
|
666
|
+
print()
|
|
667
|
+
self.usage()
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
# Create RubyFiber wrapper
|
|
671
|
+
try:
|
|
672
|
+
fiber_obj = RubyFiber(fiber_value)
|
|
673
|
+
except Exception as e:
|
|
674
|
+
print(f"Error: Could not create RubyFiber from VALUE 0x{int(fiber_value):x}")
|
|
675
|
+
print(f"Details: {e}")
|
|
676
|
+
return
|
|
677
|
+
|
|
678
|
+
# Check if fiber is in a switchable state
|
|
679
|
+
if fiber_obj.status in ('CREATED', 'TERMINATED'):
|
|
680
|
+
print(f"Warning: Fiber is {fiber_obj.status}, may not have valid saved context")
|
|
681
|
+
print()
|
|
682
|
+
|
|
683
|
+
# Update global current fiber state
|
|
684
|
+
set_current_fiber(fiber_obj)
|
|
685
|
+
|
|
686
|
+
# Get the fiber pointer for unwinder
|
|
687
|
+
fiber_ptr = fiber_obj.pointer
|
|
688
|
+
|
|
689
|
+
# Activate the unwinder for this fiber
|
|
690
|
+
_fiber_unwinder.activate_fiber(fiber_ptr)
|
|
691
|
+
|
|
692
|
+
# Set convenience variables for the fiber context
|
|
693
|
+
ec = fiber_ptr['cont']['saved_ec'].address
|
|
694
|
+
gdb.set_convenience_variable('fiber', fiber_value)
|
|
695
|
+
gdb.set_convenience_variable('fiber_ptr', fiber_ptr)
|
|
696
|
+
gdb.set_convenience_variable('ec', ec)
|
|
697
|
+
|
|
698
|
+
# Set errinfo if present (check for real object, not special constant)
|
|
699
|
+
errinfo_val = ec['errinfo']
|
|
700
|
+
errinfo_int = int(errinfo_val)
|
|
701
|
+
is_special = (errinfo_int & 0x03) != 0 or errinfo_int == 0
|
|
702
|
+
if not is_special:
|
|
703
|
+
gdb.set_convenience_variable('errinfo', errinfo_val)
|
|
704
|
+
|
|
705
|
+
# Create terminal for formatting
|
|
706
|
+
terminal = format.create_terminal(from_tty)
|
|
707
|
+
|
|
708
|
+
# Print switch confirmation
|
|
709
|
+
print(f"Switched to Fiber: ", end='')
|
|
710
|
+
print(terminal.print_type_tag('T_DATA', int(fiber_value), None), end='')
|
|
711
|
+
print(' → ', end='')
|
|
712
|
+
print(terminal.print_type_tag('struct rb_fiber_struct', fiber_obj.address, None))
|
|
713
|
+
print(f" Status: {fiber_obj.status}")
|
|
714
|
+
|
|
715
|
+
# Print exception if present
|
|
716
|
+
exc_info = fiber_obj.exception_info
|
|
717
|
+
if exc_info:
|
|
718
|
+
print(f" Exception: {exc_info}")
|
|
719
|
+
print()
|
|
720
|
+
|
|
721
|
+
# Set tag retval if present
|
|
722
|
+
try:
|
|
723
|
+
tag = ec['tag']
|
|
724
|
+
if int(tag) != 0:
|
|
725
|
+
tag_retval = tag['retval']
|
|
726
|
+
tag_state = int(tag['state'])
|
|
727
|
+
retval_int = int(tag_retval)
|
|
728
|
+
is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0
|
|
729
|
+
if not is_retval_special:
|
|
730
|
+
gdb.set_convenience_variable('retval', tag_retval)
|
|
731
|
+
except:
|
|
732
|
+
tag = None
|
|
733
|
+
is_retval_special = True
|
|
734
|
+
|
|
735
|
+
print("Convenience variables set:")
|
|
736
|
+
print(f" $fiber = Current fiber VALUE")
|
|
737
|
+
print(f" $fiber_ptr = Current fiber pointer (struct rb_fiber_struct *)")
|
|
738
|
+
print(f" $ec = Execution context (rb_execution_context_t *)")
|
|
739
|
+
if not is_special:
|
|
740
|
+
print(f" $errinfo = Exception being handled (VALUE)")
|
|
741
|
+
if tag and not is_retval_special:
|
|
742
|
+
print(f" $retval = Return value from 'return' (VALUE)")
|
|
743
|
+
print()
|
|
744
|
+
print("Now try:")
|
|
745
|
+
print(" bt # Show C backtrace of fiber")
|
|
746
|
+
print(" frame <n> # Switch to frame N")
|
|
747
|
+
print(" up/down # Move up/down frames")
|
|
748
|
+
print(" info locals # Show local variables")
|
|
749
|
+
if not is_special:
|
|
750
|
+
print(" rp $errinfo # Pretty print exception")
|
|
751
|
+
if tag and not is_retval_special:
|
|
752
|
+
print(" rp $retval # Pretty print return value (in ensure blocks)")
|
|
753
|
+
print()
|
|
754
|
+
print("Useful VALUES to inspect:")
|
|
755
|
+
print(" $ec->tag->retval # Return value (in ensure after 'return')")
|
|
756
|
+
print(" $ec->cfp->sp[-1] # Top of VM stack")
|
|
757
|
+
print(" $fiber_ptr->cont.value # Fiber yield/return value")
|
|
758
|
+
print()
|
|
759
|
+
print("NOTE: Frame #0 is synthetic (created by the unwinder) and may look odd.")
|
|
760
|
+
print(" The real fiber context starts at frame #1.")
|
|
761
|
+
print(" Use 'frame 1' to skip to the actual fiber_setcontext frame.")
|
|
762
|
+
print()
|
|
763
|
+
print("To switch back:")
|
|
764
|
+
print(" rb-fiber-switch off")
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class RubyFiberScanStackTraceAllCommand(gdb.Command):
|
|
768
|
+
"""Print stack traces for all fibers in the scan cache.
|
|
769
|
+
|
|
770
|
+
Usage: rb-fiber-scan-stack-trace-all
|
|
771
|
+
|
|
772
|
+
This command prints the Ruby stack trace for each fiber that was
|
|
773
|
+
found by 'rb-fiber-scan-heap'. Run that command first to populate
|
|
774
|
+
the fiber cache.
|
|
775
|
+
"""
|
|
776
|
+
|
|
777
|
+
def __init__(self):
|
|
778
|
+
super(RubyFiberScanStackTraceAllCommand, self).__init__("rb-fiber-scan-stack-trace-all", gdb.COMMAND_USER)
|
|
779
|
+
|
|
780
|
+
def usage(self):
|
|
781
|
+
"""Print usage information."""
|
|
782
|
+
print("Usage: rb-fiber-scan-stack-trace-all")
|
|
783
|
+
print()
|
|
784
|
+
print("Note: Run 'rb-fiber-scan-heap' first to populate the fiber cache.")
|
|
785
|
+
|
|
786
|
+
def invoke(self, arg, from_tty):
|
|
787
|
+
global _fiber_cache
|
|
788
|
+
|
|
789
|
+
# Check if cache is populated
|
|
790
|
+
if not _fiber_cache:
|
|
791
|
+
print("Error: No fibers in cache. Run 'rb-fiber-scan-heap' first.")
|
|
792
|
+
return
|
|
793
|
+
|
|
794
|
+
# Import stack module to use print_fiber_backtrace
|
|
795
|
+
import stack
|
|
796
|
+
|
|
797
|
+
print(f"Printing stack traces for {len(_fiber_cache)} fiber(s)\n")
|
|
798
|
+
print("=" * 80)
|
|
799
|
+
|
|
800
|
+
for i, fiber_value in enumerate(_fiber_cache):
|
|
801
|
+
try:
|
|
802
|
+
# Create RubyFiber wrapper to get fiber info
|
|
803
|
+
fiber_obj = RubyFiber(fiber_value)
|
|
804
|
+
|
|
805
|
+
print(f"\nFiber #{i}: VALUE 0x{int(fiber_value):x} → {fiber_obj.status}")
|
|
806
|
+
print("-" * 80)
|
|
807
|
+
|
|
808
|
+
# Use stack.print_fiber_backtrace with the fiber pointer
|
|
809
|
+
stack.print_fiber_backtrace(fiber_obj.pointer, from_tty=from_tty)
|
|
810
|
+
|
|
811
|
+
except Exception as e:
|
|
812
|
+
print(f"\nFiber #{i}: VALUE 0x{int(fiber_value):x}")
|
|
813
|
+
print("-" * 80)
|
|
814
|
+
print(f"Error printing backtrace: {e}")
|
|
815
|
+
import traceback
|
|
816
|
+
traceback.print_exc()
|
|
817
|
+
|
|
818
|
+
print()
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
# Register commands
|
|
822
|
+
RubyFiberScanHeapCommand()
|
|
823
|
+
RubyFiberScanSwitchCommand()
|
|
824
|
+
RubyFiberSwitchCommand()
|
|
825
|
+
RubyFiberScanStackTraceAllCommand()
|