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.
@@ -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()