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,609 @@
1
+ """Stack inspection commands for Ruby processes."""
2
+
3
+ import gdb
4
+ import sys
5
+
6
+ # Import Ruby GDB modules
7
+ import format
8
+ import value
9
+ import rstring
10
+ import rexception
11
+ import rsymbol
12
+
13
+
14
+ def print_fiber_backtrace(fiber_ptr, from_tty=True):
15
+ """Print backtrace for a Ruby fiber.
16
+
17
+ Args:
18
+ fiber_ptr: Fiber struct pointer (rb_fiber_struct *)
19
+ from_tty: Whether output is to terminal (for formatting)
20
+ """
21
+ printer = RubyStackPrinter()
22
+ printer.print_fiber_backtrace(fiber_ptr, from_tty)
23
+
24
+
25
+ def print_ec_backtrace(ec, from_tty=True):
26
+ """Print backtrace for an execution context.
27
+
28
+ Args:
29
+ ec: Execution context pointer (rb_execution_context_t *)
30
+ from_tty: Whether output is to terminal (for formatting)
31
+ """
32
+ printer = RubyStackPrinter()
33
+ printer.print_backtrace(ec, from_tty)
34
+
35
+
36
+ class RubyStackPrinter:
37
+ """Helper class for printing Ruby stack traces.
38
+
39
+ This class provides the core logic for printing backtraces that can be
40
+ used both by commands and programmatically from other modules.
41
+ """
42
+
43
+ def __init__(self):
44
+ # Cached type lookups
45
+ self._rbasic_type = None
46
+ self._value_type = None
47
+ self._cfp_type = None
48
+ self._rstring_type = None
49
+ self.show_values = False
50
+ self.terminal = None
51
+
52
+ def _initialize_types(self):
53
+ """Initialize cached type lookups."""
54
+ if self._rbasic_type is None:
55
+ self._rbasic_type = gdb.lookup_type('struct RBasic').pointer()
56
+ if self._value_type is None:
57
+ self._value_type = gdb.lookup_type('VALUE')
58
+ if self._cfp_type is None:
59
+ self._cfp_type = gdb.lookup_type('rb_control_frame_t').pointer()
60
+ if self._rstring_type is None:
61
+ self._rstring_type = gdb.lookup_type('struct RString').pointer()
62
+
63
+ def print_fiber_backtrace(self, fiber_ptr, from_tty=True):
64
+ """Print backtrace for a Ruby fiber.
65
+
66
+ Args:
67
+ fiber_ptr: Fiber struct pointer (rb_fiber_struct *)
68
+ from_tty: Whether output is to terminal (for formatting)
69
+ """
70
+ try:
71
+ self._initialize_types()
72
+ self.terminal = format.create_terminal(from_tty)
73
+
74
+ # Get execution context from fiber
75
+ ec = fiber_ptr['cont']['saved_ec'].address
76
+
77
+ print(f"Backtrace for fiber {fiber_ptr}:")
78
+ self.print_backtrace(ec, from_tty)
79
+
80
+ except (gdb.error, RuntimeError) as e:
81
+ print(f"Error printing fiber backtrace: {e}")
82
+
83
+ def print_backtrace(self, ec, from_tty=True):
84
+ """Print backtrace for an execution context.
85
+
86
+ Args:
87
+ ec: Execution context pointer (rb_execution_context_t *)
88
+ from_tty: Whether output is to terminal (for formatting)
89
+ """
90
+ try:
91
+ self._initialize_types()
92
+ if self.terminal is None:
93
+ self.terminal = format.create_terminal(from_tty)
94
+
95
+ cfp = ec['cfp']
96
+ vm_stack = ec['vm_stack']
97
+ vm_stack_size = int(ec['vm_stack_size'])
98
+
99
+ # Check for exception
100
+ errinfo_val = ec['errinfo']
101
+ errinfo_int = int(errinfo_val)
102
+
103
+ # Check if it's a real exception object (not nil or other immediate/special value)
104
+ if not value.is_immediate(errinfo_val) and not value.is_nil(errinfo_val):
105
+ try:
106
+ exc_class = self._get_exception_class(errinfo_val)
107
+ exc_msg = self._get_exception_message(errinfo_val)
108
+
109
+ # Set as GDB convenience variable for manual inspection
110
+ gdb.set_convenience_variable('errinfo', errinfo_val)
111
+
112
+ if exc_msg:
113
+ print(f"Currently handling: {exc_class}: {exc_msg} (VALUE: 0x{errinfo_int:x}, $errinfo)")
114
+ else:
115
+ print(f"Currently handling: {exc_class} (VALUE: 0x{errinfo_int:x}, $errinfo)")
116
+ print()
117
+ except:
118
+ # Set convenience variable even if we can't decode
119
+ gdb.set_convenience_variable('errinfo', errinfo_val)
120
+ print(f"Currently handling exception (VALUE: 0x{errinfo_int:x}, $errinfo)")
121
+ print()
122
+
123
+ # Calculate end of control frames
124
+ cfpend = (vm_stack + vm_stack_size).cast(self._cfp_type) - 1
125
+
126
+ frame_num = 0
127
+ current_cfp = cfp
128
+
129
+ while current_cfp < cfpend:
130
+ try:
131
+ self._print_frame(current_cfp, frame_num)
132
+ frame_num += 1
133
+ current_cfp += 1
134
+ except (gdb.error, RuntimeError) as e:
135
+ print(f" #{frame_num}: [error reading frame: {e}]")
136
+ break
137
+
138
+ if frame_num == 0:
139
+ print(" (no frames)")
140
+
141
+ except (gdb.error, RuntimeError) as e:
142
+ print(f"Error printing backtrace: {e}")
143
+
144
+ def _print_frame(self, cfp, depth):
145
+ """Print a single control frame.
146
+
147
+ Args:
148
+ cfp: Control frame pointer (rb_control_frame_t *)
149
+ depth: Frame depth/number
150
+ """
151
+ iseq = cfp['iseq']
152
+
153
+ if int(iseq) == 0:
154
+ # C function frame - try to extract method info from EP
155
+ try:
156
+ ep = cfp['ep']
157
+
158
+ # Check if this is a valid C frame
159
+ if int(ep) != 0:
160
+ ep0 = int(ep[0])
161
+ if (ep0 & 0xffff0001) == 0x55550001:
162
+ # Valid C frame, try to extract method entry
163
+ env_me_cref = ep[-2]
164
+
165
+ try:
166
+ me_type = gdb.lookup_type('rb_callable_method_entry_t').pointer()
167
+ me = env_me_cref.cast(me_type)
168
+
169
+ # Get the C function pointer
170
+ cfunc = me['def']['body']['cfunc']['func']
171
+
172
+ # Get the method ID
173
+ method_id = me['def']['original_id']
174
+
175
+ # Try to get symbol for the C function
176
+ func_addr = int(cfunc)
177
+ func_name = ""
178
+ try:
179
+ symbol_info = gdb.execute(f"info symbol 0x{func_addr:x}", to_string=True)
180
+ func_name = f" ({symbol_info.split()[0]})"
181
+ except:
182
+ func_name = f" (0x{func_addr:x})"
183
+
184
+ # Print C frame with cyan/dimmed formatting
185
+ print(self.terminal.print(
186
+ format.metadata, f" #{depth}: ",
187
+ format.dim, "[C function", func_name, "]",
188
+ format.reset
189
+ ))
190
+ return
191
+ except:
192
+ pass
193
+ except:
194
+ pass
195
+
196
+ # Fallback if we couldn't extract info
197
+ print(self.terminal.print(
198
+ format.metadata, f" #{depth}: ",
199
+ format.dim, "[C function or native frame]",
200
+ format.reset
201
+ ))
202
+ return
203
+
204
+ pc = cfp['pc']
205
+
206
+ if int(pc) == 0:
207
+ print(self.terminal.print(
208
+ format.metadata, f" #{depth}: ",
209
+ format.error, "???:???:in '???'",
210
+ format.reset
211
+ ))
212
+ return
213
+
214
+ # Check if it's an ifunc (internal function)
215
+ RUBY_IMMEDIATE_MASK = 0x03
216
+ RUBY_FL_USHIFT = 12
217
+ RUBY_T_IMEMO = 0x1a
218
+ RUBY_IMEMO_MASK = 0x0f
219
+
220
+ iseq_val = int(iseq.cast(self._value_type))
221
+ if not (iseq_val & RUBY_IMMEDIATE_MASK):
222
+ try:
223
+ flags = int(iseq['flags'])
224
+ expected = ((RUBY_T_IMEMO << RUBY_FL_USHIFT) | RUBY_T_IMEMO)
225
+ mask = ((RUBY_IMEMO_MASK << RUBY_FL_USHIFT) | 0x1f)
226
+
227
+ if (flags & mask) == expected:
228
+ # It's an ifunc
229
+ print(self.terminal.print(
230
+ format.metadata, f" #{depth}: ",
231
+ format.dim, "[ifunc]",
232
+ format.reset
233
+ ))
234
+ return
235
+ except:
236
+ pass
237
+
238
+ try:
239
+ # Get location information
240
+ location = iseq['body']['location']
241
+ pathobj = location['pathobj']
242
+ label = location['label']
243
+
244
+ # Get path string - pathobj can be a string or an array [path, realpath]
245
+ path = self._extract_path_from_pathobj(pathobj)
246
+ label_str = self._value_to_string(label)
247
+
248
+ # Calculate line number
249
+ lineno = self._get_lineno(cfp)
250
+
251
+ # Print Ruby frame with highlighting
252
+ print(self.terminal.print(
253
+ format.metadata, f" #{depth}: ",
254
+ format.string, path,
255
+ format.reset, ":",
256
+ format.metadata, str(lineno),
257
+ format.reset, ":in '",
258
+ format.method, label_str,
259
+ format.reset, "'"
260
+ ))
261
+
262
+ # If --values flag is set, print stack values
263
+ if self.show_values:
264
+ self._print_stack_values(cfp, iseq)
265
+
266
+ except (gdb.error, RuntimeError) as e:
267
+ print(self.terminal.print(
268
+ format.metadata, f" #{depth}: ",
269
+ format.error, f"[error reading frame info: {e}]",
270
+ format.reset
271
+ ))
272
+
273
+ def _print_stack_values(self, cfp, iseq):
274
+ """Print Ruby VALUEs on the control frame's stack pointer.
275
+
276
+ Args:
277
+ cfp: Control frame pointer (rb_control_frame_t *)
278
+ iseq: Instruction sequence pointer
279
+ """
280
+ try:
281
+ sp = cfp['sp']
282
+ ep = cfp['ep']
283
+
284
+ if int(sp) == 0 or int(ep) == 0:
285
+ return
286
+
287
+ # Try to get local table information for better labeling
288
+ local_names = []
289
+ local_size = 0
290
+ try:
291
+ if int(iseq) != 0:
292
+ iseq_body = iseq['body']
293
+ local_table_size = int(iseq_body['local_table_size'])
294
+
295
+ if local_table_size > 0:
296
+ local_size = local_table_size
297
+ local_table = iseq_body['local_table']
298
+
299
+ # Read local variable names (they're stored as IDs/symbols)
300
+ # Local table is stored in reverse order (last local first)
301
+ for i in range(min(local_table_size, 20)): # Cap at 20
302
+ try:
303
+ local_id = local_table[local_table_size - 1 - i]
304
+ # Try to convert ID to symbol name using RubySymbol
305
+ if int(local_id) != 0:
306
+ sym = rsymbol.RubySymbol(local_id)
307
+ name = sym.to_str()
308
+ if name:
309
+ local_names.append(name)
310
+ else:
311
+ local_names.append(f"local_{i}")
312
+ else:
313
+ local_names.append(f"local_{i}")
314
+ except:
315
+ local_names.append(f"local_{i}")
316
+ except:
317
+ pass
318
+
319
+ # Environment pointer typically points to the local variable area
320
+ # Stack grows downward, so we start from sp and go down
321
+ value_ptr = sp - 1
322
+
323
+ # Print a reasonable number of stack values
324
+ max_values = 10
325
+ values_printed = 0
326
+
327
+ print(self.terminal.print(format.dim, " Stack values:", format.reset))
328
+
329
+ # Calculate offset from ep to show position
330
+ while value_ptr >= ep and values_printed < max_values:
331
+ try:
332
+ val = value_ptr[0]
333
+ val_int = int(val)
334
+
335
+ # Calculate offset from ep for labeling
336
+ offset = int(value_ptr - ep)
337
+
338
+ # Try to determine if this is a local variable
339
+ label = f"sp[-{values_printed + 1}]"
340
+ if offset < local_size and offset < len(local_names):
341
+ label = f"{local_names[offset]} (ep[{offset}])"
342
+
343
+ # Try to get a brief representation of the value
344
+ val_str = self._format_value_brief(val)
345
+
346
+ print(self.terminal.print(
347
+ format.metadata, f" {label:20s} ",
348
+ format.dim, f"= ",
349
+ format.reset, val_str,
350
+ format.reset
351
+ ))
352
+
353
+ values_printed += 1
354
+ value_ptr -= 1
355
+ except (gdb.error, gdb.MemoryError):
356
+ break
357
+
358
+ if values_printed == 0:
359
+ print(self.terminal.print(format.dim, " (empty stack)", format.reset))
360
+
361
+ except (gdb.error, RuntimeError) as e:
362
+ # Silently skip if we can't read stack values
363
+ pass
364
+
365
+ def _format_value_brief(self, val):
366
+ """Get a brief string representation of a VALUE.
367
+
368
+ Args:
369
+ val: Ruby VALUE
370
+
371
+ Returns:
372
+ Brief string description
373
+ """
374
+ try:
375
+ # Use value.py's interpret function to get the typed object
376
+ obj = value.interpret(val)
377
+
378
+ # Get string representation
379
+ obj_str = str(obj)
380
+
381
+ # Truncate if too long
382
+ if len(obj_str) > 60:
383
+ return obj_str[:57] + "..."
384
+
385
+ return obj_str
386
+
387
+ except Exception as e:
388
+ return f"<error: {e}>"
389
+
390
+ def _get_lineno(self, cfp):
391
+ """Get line number for a control frame.
392
+
393
+ Args:
394
+ cfp: Control frame pointer
395
+
396
+ Returns:
397
+ Line number as int or "???" if unavailable
398
+ """
399
+ try:
400
+ iseq = cfp['iseq']
401
+ pc = cfp['pc']
402
+
403
+ if int(pc) == 0:
404
+ return "???"
405
+
406
+ iseq_body = iseq['body']
407
+ iseq_encoded = iseq_body['iseq_encoded']
408
+ iseq_size = int(iseq_body['iseq_size'])
409
+
410
+ pc_offset = int(pc - iseq_encoded)
411
+
412
+ if pc_offset < 0 or pc_offset >= iseq_size:
413
+ return "???"
414
+
415
+ # Try to get line info
416
+ insns_info = iseq_body['insns_info']
417
+ positions = insns_info['positions']
418
+
419
+ if int(positions) != 0:
420
+ position = positions[pc_offset]
421
+ lineno = int(position['lineno'])
422
+ if lineno >= 0:
423
+ return lineno
424
+
425
+ # Fall back to first_lineno
426
+ return int(iseq_body['location']['first_lineno'])
427
+
428
+ except:
429
+ return "???"
430
+
431
+ def _get_exception_class(self, exc_value):
432
+ """Get the class name of an exception object.
433
+
434
+ Delegates to rexception.RException for proper exception handling.
435
+
436
+ Args:
437
+ exc_value: Exception VALUE
438
+
439
+ Returns:
440
+ Class name as string
441
+ """
442
+ try:
443
+ exc = rexception.RException(exc_value)
444
+ return exc.class_name
445
+ except Exception:
446
+ # Fallback if RException can't be created
447
+ try:
448
+ rbasic = exc_value.cast(self._rbasic_type)
449
+ klass = rbasic['klass']
450
+ return f"Exception(klass=0x{int(klass):x})"
451
+ except:
452
+ raise
453
+
454
+ def _get_exception_message(self, exc_value):
455
+ """Get the message from an exception object.
456
+
457
+ Delegates to rexception.RException for proper exception handling.
458
+
459
+ Args:
460
+ exc_value: Exception VALUE
461
+
462
+ Returns:
463
+ Message string or None if unavailable
464
+ """
465
+ try:
466
+ exc = rexception.RException(exc_value)
467
+ return exc.message
468
+ except Exception:
469
+ # If RException can't be created, return None
470
+ return None
471
+
472
+ def _value_to_string(self, val):
473
+ """Convert a Ruby VALUE to a Python string.
474
+
475
+ Args:
476
+ val: Ruby VALUE
477
+
478
+ Returns:
479
+ String representation
480
+ """
481
+ try:
482
+ # Use the value.interpret infrastructure for proper type handling
483
+ obj = value.interpret(val)
484
+
485
+ # For strings, get the actual content
486
+ if hasattr(obj, 'to_str'):
487
+ return obj.to_str()
488
+
489
+ # For immediates and other types, convert to string
490
+ obj_str = str(obj)
491
+
492
+ # Strip the type tag if present (e.g., "<T_FIXNUM> 42" -> "42")
493
+ if obj_str.startswith('<'):
494
+ # Find the end of the type tag
495
+ end_tag = obj_str.find('>')
496
+ if end_tag != -1 and end_tag + 2 < len(obj_str):
497
+ # Return the part after the tag and space
498
+ return obj_str[end_tag + 2:]
499
+
500
+ return obj_str
501
+
502
+ except Exception as e:
503
+ return f"<error:{e}>"
504
+
505
+ def _extract_path_from_pathobj(self, pathobj):
506
+ """Extract file path from pathobj (can be string or array).
507
+
508
+ Args:
509
+ pathobj: Ruby VALUE (either T_STRING or T_ARRAY)
510
+
511
+ Returns:
512
+ File path as string
513
+ """
514
+ try:
515
+ # Interpret the pathobj to get its type
516
+ obj = value.interpret(pathobj)
517
+
518
+ # If it's an array, get the first element (the path)
519
+ if hasattr(obj, 'length') and hasattr(obj, 'get_item'):
520
+ if obj.length() > 0:
521
+ path_value = obj.get_item(0)
522
+ return self._value_to_string(path_value)
523
+
524
+ # Otherwise, treat it as a string directly
525
+ return self._value_to_string(pathobj)
526
+
527
+ except Exception as e:
528
+ return f"<error:{e}>"
529
+
530
+
531
+ class RubyStackTraceCommand(gdb.Command):
532
+ """Print combined C and Ruby backtrace for current fiber or thread.
533
+
534
+ Usage: rb-stack-trace [--values]
535
+
536
+ Shows backtrace for:
537
+ - Currently selected fiber (if rb-fiber-switch was used)
538
+ - Current thread execution context (if no fiber selected)
539
+
540
+ Options:
541
+ --values Show all Ruby VALUEs on each frame's stack pointer
542
+
543
+ The output shows both C frames and Ruby frames intermixed,
544
+ giving a complete picture of the call stack.
545
+ """
546
+
547
+ def __init__(self):
548
+ super(RubyStackTraceCommand, self).__init__("rb-stack-trace", gdb.COMMAND_USER)
549
+ self.printer = RubyStackPrinter()
550
+
551
+ def usage(self):
552
+ """Print usage information."""
553
+ print("Usage: rb-stack-trace [--values]")
554
+ print("Examples:")
555
+ print(" rb-stack-trace # Show backtrace for current fiber/thread")
556
+ print(" rb-stack-trace --values # Show backtrace with stack VALUEs")
557
+
558
+ def invoke(self, arg, from_tty):
559
+ """Execute the stack trace command."""
560
+ try:
561
+ # Parse arguments
562
+ import command
563
+ arguments = command.parse_arguments(arg if arg else "")
564
+ self.printer.show_values = arguments.has_flag('values')
565
+
566
+ # Create terminal for formatting
567
+ self.printer.terminal = format.create_terminal(from_tty)
568
+
569
+ # Check if a fiber is currently selected
570
+ # Import here to avoid circular dependency
571
+ import fiber
572
+ current_fiber = fiber.get_current_fiber()
573
+
574
+ if current_fiber:
575
+ # Use the selected fiber's execution context
576
+ print(f"Stack trace for selected fiber:")
577
+ print(f" Fiber: ", end='')
578
+ print(self.printer.terminal.print_type_tag('T_DATA', int(current_fiber.value), None))
579
+ print()
580
+
581
+ ec = current_fiber.pointer['cont']['saved_ec'].address
582
+ self.printer.print_backtrace(ec, from_tty)
583
+ else:
584
+ # Use current thread's execution context
585
+ print("Stack trace for current thread:")
586
+ print()
587
+
588
+ try:
589
+ # Get current execution context from the running thread
590
+ ec = gdb.parse_and_eval('ruby_current_ec')
591
+ if int(ec) == 0:
592
+ print("Error: No execution context available")
593
+ print("Either select a fiber with 'rb-fiber-switch' or ensure Ruby is running")
594
+ return
595
+
596
+ self.printer.print_backtrace(ec, from_tty)
597
+ except gdb.error as e:
598
+ print(f"Error getting execution context: {e}")
599
+ print("Try selecting a fiber first with 'rb-fiber-switch'")
600
+ return
601
+
602
+ except Exception as e:
603
+ print(f"Error: {e}")
604
+ import traceback
605
+ traceback.print_exc()
606
+
607
+
608
+ # Register commands
609
+ RubyStackTraceCommand()