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,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()
|