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,124 @@
|
|
|
1
|
+
import gdb
|
|
2
|
+
import rbasic
|
|
3
|
+
import constants
|
|
4
|
+
import format
|
|
5
|
+
|
|
6
|
+
class RArrayBase:
|
|
7
|
+
"""Base class for RArray variants."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, value):
|
|
10
|
+
"""value is a VALUE pointing to a T_ARRAY object."""
|
|
11
|
+
self.value = value
|
|
12
|
+
self.rarray = value.cast(constants.get_type('struct RArray').pointer())
|
|
13
|
+
self.basic = value.cast(constants.get_type('struct RBasic').pointer())
|
|
14
|
+
self.flags = int(self.basic.dereference()['flags'])
|
|
15
|
+
|
|
16
|
+
def length(self):
|
|
17
|
+
"""Get array length. Must be implemented by subclasses."""
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
def items_ptr(self):
|
|
21
|
+
"""Get pointer to array items. Must be implemented by subclasses."""
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
def get_item(self, index):
|
|
25
|
+
"""Get item at index."""
|
|
26
|
+
if index < 0 or index >= self.length():
|
|
27
|
+
raise IndexError(f"Index {index} out of range")
|
|
28
|
+
items = self.items_ptr()
|
|
29
|
+
return items[index]
|
|
30
|
+
|
|
31
|
+
def __len__(self):
|
|
32
|
+
"""Support len() function."""
|
|
33
|
+
return self.length()
|
|
34
|
+
|
|
35
|
+
def __getitem__(self, index):
|
|
36
|
+
"""Support indexing."""
|
|
37
|
+
return self.get_item(index)
|
|
38
|
+
|
|
39
|
+
def print_recursive(self, printer, depth):
|
|
40
|
+
"""Print this array recursively."""
|
|
41
|
+
# Print the array header
|
|
42
|
+
printer.print(self)
|
|
43
|
+
|
|
44
|
+
# If depth is 0, don't recurse into elements
|
|
45
|
+
if depth <= 0:
|
|
46
|
+
if len(self) > 0:
|
|
47
|
+
printer.print_with_indent(printer.max_depth - depth, " ...")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
# Print each element
|
|
51
|
+
for i in range(len(self)):
|
|
52
|
+
printer.print_item_label(printer.max_depth - depth, i)
|
|
53
|
+
try:
|
|
54
|
+
element = self[i]
|
|
55
|
+
printer.print_value(element, depth - 1)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"Error accessing element {i}: {e}")
|
|
58
|
+
|
|
59
|
+
class RArrayEmbedded(RArrayBase):
|
|
60
|
+
"""Embedded array (small arrays stored directly in struct)."""
|
|
61
|
+
|
|
62
|
+
def length(self):
|
|
63
|
+
# Extract length from flags using Ruby's encoding
|
|
64
|
+
# Length is stored in RUBY_FL_USER3|RUBY_FL_USER4 bits, shifted by RUBY_FL_USHIFT+3
|
|
65
|
+
mask = constants.get("RUBY_FL_USER3") | constants.get("RUBY_FL_USER4")
|
|
66
|
+
shift = constants.get("RUBY_FL_USHIFT") + 3
|
|
67
|
+
return (self.flags & mask) >> shift
|
|
68
|
+
|
|
69
|
+
def items_ptr(self):
|
|
70
|
+
return self.rarray.dereference()['as']['ary']
|
|
71
|
+
|
|
72
|
+
def __str__(self):
|
|
73
|
+
"""Return string representation of array."""
|
|
74
|
+
addr = int(self.value)
|
|
75
|
+
return f"<T_ARRAY@0x{addr:x} embedded length={len(self)}>"
|
|
76
|
+
|
|
77
|
+
def print_to(self, terminal):
|
|
78
|
+
"""Print this array with formatting."""
|
|
79
|
+
addr = int(self.value)
|
|
80
|
+
return terminal.print(
|
|
81
|
+
format.metadata, '<',
|
|
82
|
+
format.type, 'T_ARRAY',
|
|
83
|
+
format.metadata, f'@0x{addr:x} embedded length={len(self)}>',
|
|
84
|
+
format.reset
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
class RArrayHeap(RArrayBase):
|
|
88
|
+
"""Heap array (larger arrays with separate memory allocation)."""
|
|
89
|
+
|
|
90
|
+
def length(self):
|
|
91
|
+
return int(self.rarray.dereference()['as']['heap']['len'])
|
|
92
|
+
|
|
93
|
+
def items_ptr(self):
|
|
94
|
+
return self.rarray.dereference()['as']['heap']['ptr']
|
|
95
|
+
|
|
96
|
+
def __str__(self):
|
|
97
|
+
"""Return string representation of array."""
|
|
98
|
+
addr = int(self.value)
|
|
99
|
+
return f"<T_ARRAY@0x{addr:x} heap length={len(self)}>"
|
|
100
|
+
|
|
101
|
+
def print_to(self, terminal):
|
|
102
|
+
"""Print this array with formatting."""
|
|
103
|
+
addr = int(self.value)
|
|
104
|
+
return terminal.print(
|
|
105
|
+
format.metadata, '<',
|
|
106
|
+
format.type, 'T_ARRAY',
|
|
107
|
+
format.metadata, f'@0x{addr:x} heap length={len(self)}>',
|
|
108
|
+
format.reset
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def RArray(value):
|
|
112
|
+
"""Factory function that returns the appropriate RArray variant.
|
|
113
|
+
|
|
114
|
+
Caller should ensure value is a RUBY_T_ARRAY before calling this function.
|
|
115
|
+
"""
|
|
116
|
+
# Get flags to determine embedded vs heap
|
|
117
|
+
basic = value.cast(constants.get_type('struct RBasic').pointer())
|
|
118
|
+
flags = int(basic.dereference()['flags'])
|
|
119
|
+
|
|
120
|
+
# Check if array is embedded or heap-allocated using flags
|
|
121
|
+
if (flags & constants.get("RARRAY_EMBED_FLAG")) != 0:
|
|
122
|
+
return RArrayEmbedded(value)
|
|
123
|
+
else:
|
|
124
|
+
return RArrayHeap(value)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import gdb
|
|
2
|
+
import constants
|
|
3
|
+
import format
|
|
4
|
+
|
|
5
|
+
def type_of(value):
|
|
6
|
+
"""Get the Ruby type of a VALUE.
|
|
7
|
+
|
|
8
|
+
Returns the RUBY_T_* constant value (e.g., RUBY_T_STRING, RUBY_T_ARRAY),
|
|
9
|
+
or None if the type cannot be determined.
|
|
10
|
+
"""
|
|
11
|
+
basic = value.cast(constants.get_type('struct RBasic').pointer())
|
|
12
|
+
flags = int(basic.dereference()['flags'])
|
|
13
|
+
RUBY_T_MASK = constants.get('RUBY_T_MASK')
|
|
14
|
+
return flags & RUBY_T_MASK
|
|
15
|
+
|
|
16
|
+
def is_type(value, ruby_type_constant):
|
|
17
|
+
"""Check if a VALUE is of a specific Ruby type.
|
|
18
|
+
|
|
19
|
+
Arguments:
|
|
20
|
+
value: The GDB value to check
|
|
21
|
+
ruby_type_constant: String name of the constant (e.g., 'RUBY_T_STRING')
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
True if the value is of the specified type, False otherwise
|
|
25
|
+
"""
|
|
26
|
+
type_flag = type_of(value)
|
|
27
|
+
expected_type = constants.get(ruby_type_constant)
|
|
28
|
+
return type_flag == expected_type
|
|
29
|
+
|
|
30
|
+
# Map of type constants to their names for display
|
|
31
|
+
TYPE_NAMES = {
|
|
32
|
+
'RUBY_T_NONE': 'None',
|
|
33
|
+
'RUBY_T_OBJECT': 'Object',
|
|
34
|
+
'RUBY_T_CLASS': 'Class',
|
|
35
|
+
'RUBY_T_MODULE': 'Module',
|
|
36
|
+
'RUBY_T_FLOAT': 'Float',
|
|
37
|
+
'RUBY_T_STRING': 'String',
|
|
38
|
+
'RUBY_T_REGEXP': 'Regexp',
|
|
39
|
+
'RUBY_T_ARRAY': 'Array',
|
|
40
|
+
'RUBY_T_HASH': 'Hash',
|
|
41
|
+
'RUBY_T_STRUCT': 'Struct',
|
|
42
|
+
'RUBY_T_BIGNUM': 'Bignum',
|
|
43
|
+
'RUBY_T_FILE': 'File',
|
|
44
|
+
'RUBY_T_DATA': 'Data',
|
|
45
|
+
'RUBY_T_MATCH': 'Match',
|
|
46
|
+
'RUBY_T_COMPLEX': 'Complex',
|
|
47
|
+
'RUBY_T_RATIONAL': 'Rational',
|
|
48
|
+
'RUBY_T_NIL': 'Nil',
|
|
49
|
+
'RUBY_T_TRUE': 'True',
|
|
50
|
+
'RUBY_T_FALSE': 'False',
|
|
51
|
+
'RUBY_T_SYMBOL': 'Symbol',
|
|
52
|
+
'RUBY_T_FIXNUM': 'Fixnum',
|
|
53
|
+
'RUBY_T_UNDEF': 'Undef',
|
|
54
|
+
'RUBY_T_IMEMO': 'IMemo',
|
|
55
|
+
'RUBY_T_NODE': 'Node',
|
|
56
|
+
'RUBY_T_ICLASS': 'IClass',
|
|
57
|
+
'RUBY_T_ZOMBIE': 'Zombie',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def type_name(value):
|
|
61
|
+
"""Get the human-readable type name for a VALUE.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
String like 'String', 'Array', 'Hash', etc., or 'Unknown'
|
|
65
|
+
"""
|
|
66
|
+
type_flag = type_of(value)
|
|
67
|
+
|
|
68
|
+
# Try to find matching type name
|
|
69
|
+
for const_name, display_name in TYPE_NAMES.items():
|
|
70
|
+
if constants.get(const_name) == type_flag:
|
|
71
|
+
return display_name
|
|
72
|
+
|
|
73
|
+
return f'Unknown(0x{type_flag:x})'
|
|
74
|
+
|
|
75
|
+
class RBasic:
|
|
76
|
+
"""Generic Ruby object wrapper for unhandled types.
|
|
77
|
+
|
|
78
|
+
This provides a fallback for types that don't have specialized handlers.
|
|
79
|
+
"""
|
|
80
|
+
def __init__(self, value):
|
|
81
|
+
self.value = value
|
|
82
|
+
self.basic = value.cast(constants.get_type('struct RBasic').pointer())
|
|
83
|
+
self.flags = int(self.basic.dereference()['flags'])
|
|
84
|
+
self.type_flag = self.flags & constants.get('RUBY_T_MASK')
|
|
85
|
+
|
|
86
|
+
def __str__(self):
|
|
87
|
+
type_str = type_name(self.value)
|
|
88
|
+
return f"<{type_str}:0x{int(self.value):x}>"
|
|
89
|
+
|
|
90
|
+
def print_to(self, terminal):
|
|
91
|
+
"""Return formatted basic object representation."""
|
|
92
|
+
type_str = type_name(self.value)
|
|
93
|
+
addr = int(self.value)
|
|
94
|
+
return terminal.print(
|
|
95
|
+
format.metadata, '<',
|
|
96
|
+
format.type, type_str,
|
|
97
|
+
format.metadata, f':0x{addr:x}>',
|
|
98
|
+
format.reset
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def print_recursive(self, printer, depth):
|
|
102
|
+
"""Print this basic object (no recursion)."""
|
|
103
|
+
printer.print(self)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import gdb
|
|
2
|
+
import constants
|
|
3
|
+
import rbasic
|
|
4
|
+
import format
|
|
5
|
+
|
|
6
|
+
class RBignumObject:
|
|
7
|
+
def __init__(self, value):
|
|
8
|
+
self.value = value
|
|
9
|
+
self.rbignum = value.cast(constants.get_type('struct RBignum').pointer())
|
|
10
|
+
self.basic = value.cast(constants.get_type('struct RBasic').pointer())
|
|
11
|
+
self.flags = int(self.basic.dereference()['flags'])
|
|
12
|
+
|
|
13
|
+
def is_embedded(self):
|
|
14
|
+
# Check if FL_USER1 flag is set (RBIGNUM_EMBED_FLAG)
|
|
15
|
+
FL_USER1 = 1 << (constants.get('RUBY_FL_USHIFT', 12) + 1)
|
|
16
|
+
return bool(self.flags & FL_USER1)
|
|
17
|
+
|
|
18
|
+
def __len__(self):
|
|
19
|
+
if self.is_embedded():
|
|
20
|
+
# Embedded length is stored in flags
|
|
21
|
+
# Extract length from FL_USER2 onwards
|
|
22
|
+
return (self.flags >> (constants.get('RUBY_FL_USHIFT', 12) + 2)) & 0x1F
|
|
23
|
+
else:
|
|
24
|
+
return int(self.rbignum.dereference()['as']['heap']['len'])
|
|
25
|
+
|
|
26
|
+
def __str__(self):
|
|
27
|
+
addr = int(self.value)
|
|
28
|
+
if self.is_embedded():
|
|
29
|
+
return f"<T_BIGNUM@0x{addr:x} embedded length={len(self)}>"
|
|
30
|
+
else:
|
|
31
|
+
return f"<T_BIGNUM@0x{addr:x} heap length={len(self)}>"
|
|
32
|
+
|
|
33
|
+
def print_to(self, terminal):
|
|
34
|
+
"""Return formatted bignum representation."""
|
|
35
|
+
addr = int(self.value)
|
|
36
|
+
storage = "embedded" if self.is_embedded() else "heap"
|
|
37
|
+
return terminal.print(
|
|
38
|
+
format.metadata, '<',
|
|
39
|
+
format.type, 'T_BIGNUM',
|
|
40
|
+
format.metadata, f'@0x{addr:x} {storage} length={len(self)}>',
|
|
41
|
+
format.reset
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def print_recursive(self, printer, depth):
|
|
45
|
+
"""Print this bignum (no recursion needed)."""
|
|
46
|
+
printer.print(self)
|
|
47
|
+
|
|
48
|
+
def RBignum(value):
|
|
49
|
+
if rbasic.is_type(value, 'RUBY_T_BIGNUM'):
|
|
50
|
+
return RBignumObject(value)
|
|
51
|
+
|
|
52
|
+
return None
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import gdb
|
|
2
|
+
import constants
|
|
3
|
+
import value
|
|
4
|
+
import rstring
|
|
5
|
+
|
|
6
|
+
class RClass:
|
|
7
|
+
"""Wrapper for Ruby class objects (klass pointers)."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, klass_value):
|
|
10
|
+
"""Initialize with a klass VALUE.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
klass_value: A GDB value representing a Ruby class (klass pointer)
|
|
14
|
+
"""
|
|
15
|
+
self.klass = klass_value
|
|
16
|
+
self._name = None
|
|
17
|
+
|
|
18
|
+
def name(self):
|
|
19
|
+
"""Get the class name as a string.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Class name string, or formatted anonymous class representation
|
|
23
|
+
"""
|
|
24
|
+
if self._name is None:
|
|
25
|
+
self._name = self._get_class_name()
|
|
26
|
+
return self._name
|
|
27
|
+
|
|
28
|
+
def _get_class_name(self):
|
|
29
|
+
"""Extract class name from klass pointer.
|
|
30
|
+
|
|
31
|
+
Tries multiple strategies across Ruby versions:
|
|
32
|
+
1. Check against well-known global class pointers (rb_eStandardError, etc.)
|
|
33
|
+
2. Try rb_classext_struct.classpath (Ruby 3.4+)
|
|
34
|
+
3. Fall back to anonymous class format
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Class name string
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
# Strategy 1: Check well-known exception classes
|
|
41
|
+
# This works in core dumps since we're just comparing pointers
|
|
42
|
+
well_known = [
|
|
43
|
+
('rb_eException', 'Exception'),
|
|
44
|
+
('rb_eStandardError', 'StandardError'),
|
|
45
|
+
('rb_eSystemExit', 'SystemExit'),
|
|
46
|
+
('rb_eInterrupt', 'Interrupt'),
|
|
47
|
+
('rb_eSignal', 'SignalException'),
|
|
48
|
+
('rb_eFatal', 'fatal'),
|
|
49
|
+
('rb_eScriptError', 'ScriptError'),
|
|
50
|
+
('rb_eLoadError', 'LoadError'),
|
|
51
|
+
('rb_eNotImpError', 'NotImplementedError'),
|
|
52
|
+
('rb_eSyntaxError', 'SyntaxError'),
|
|
53
|
+
('rb_eSecurityError', 'SecurityError'),
|
|
54
|
+
('rb_eNoMemError', 'NoMemoryError'),
|
|
55
|
+
('rb_eTypeError', 'TypeError'),
|
|
56
|
+
('rb_eArgError', 'ArgumentError'),
|
|
57
|
+
('rb_eIndexError', 'IndexError'),
|
|
58
|
+
('rb_eKeyError', 'KeyError'),
|
|
59
|
+
('rb_eRangeError', 'RangeError'),
|
|
60
|
+
('rb_eNameError', 'NameError'),
|
|
61
|
+
('rb_eNoMethodError', 'NoMethodError'),
|
|
62
|
+
('rb_eRuntimeError', 'RuntimeError'),
|
|
63
|
+
('rb_eFrozenError', 'FrozenError'),
|
|
64
|
+
('rb_eIOError', 'IOError'),
|
|
65
|
+
('rb_eEOFError', 'EOFError'),
|
|
66
|
+
('rb_eLocalJumpError', 'LocalJumpError'),
|
|
67
|
+
('rb_eSysStackError', 'SystemStackError'),
|
|
68
|
+
('rb_eRegexpError', 'RegexpError'),
|
|
69
|
+
('rb_eThreadError', 'ThreadError'),
|
|
70
|
+
('rb_eZeroDivError', 'ZeroDivisionError'),
|
|
71
|
+
('rb_eFloatDomainError', 'FloatDomainError'),
|
|
72
|
+
('rb_eStopIteration', 'StopIteration'),
|
|
73
|
+
('rb_eMathDomainError', 'Math::DomainError'),
|
|
74
|
+
('rb_eEncCompatError', 'Encoding::CompatibilityError'),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
klass_addr = int(self.klass)
|
|
78
|
+
for var_name, class_name in well_known:
|
|
79
|
+
try:
|
|
80
|
+
known_klass = gdb.parse_and_eval(var_name)
|
|
81
|
+
if int(known_klass) == klass_addr:
|
|
82
|
+
return class_name
|
|
83
|
+
except:
|
|
84
|
+
# Variable might not exist in this Ruby version
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Strategy 2: Try modern rb_classext_struct.classpath (Ruby 3.4+)
|
|
88
|
+
try:
|
|
89
|
+
rclass = self.klass.cast(gdb.lookup_type('struct RClass').pointer())
|
|
90
|
+
# Try to access classext.classpath
|
|
91
|
+
try:
|
|
92
|
+
# Try embedded classext (RCLASS_EXT_EMBEDDED)
|
|
93
|
+
classext_ptr = gdb.parse_and_eval(f"(rb_classext_t *)((char *){int(self.klass)} + sizeof(struct RClass))")
|
|
94
|
+
classpath_val = classext_ptr['classpath']
|
|
95
|
+
except:
|
|
96
|
+
# Try pointer-based classext
|
|
97
|
+
try:
|
|
98
|
+
classext_ptr = rclass['ptr']
|
|
99
|
+
classpath_val = classext_ptr['classpath']
|
|
100
|
+
except:
|
|
101
|
+
classpath_val = None
|
|
102
|
+
|
|
103
|
+
if classpath_val and int(classpath_val) != 0 and not value.is_nil(classpath_val):
|
|
104
|
+
# Decode the classpath string
|
|
105
|
+
class_name_obj = value.interpret(classpath_val)
|
|
106
|
+
if hasattr(class_name_obj, 'to_str'):
|
|
107
|
+
class_name = class_name_obj.to_str()
|
|
108
|
+
if class_name and not class_name.startswith('<'):
|
|
109
|
+
return class_name
|
|
110
|
+
except:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
# Strategy 3: Fall back to anonymous class format
|
|
114
|
+
return f"#<Class:0x{int(self.klass):x}>"
|
|
115
|
+
except Exception as e:
|
|
116
|
+
# Ultimate fallback
|
|
117
|
+
return f"#<Class:0x{int(self.klass):x}>"
|
|
118
|
+
|
|
119
|
+
def __str__(self):
|
|
120
|
+
"""Return the class name."""
|
|
121
|
+
return self.name()
|
|
122
|
+
|
|
123
|
+
def get_class_name(klass_value):
|
|
124
|
+
"""Get the name of a class from its klass pointer.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
klass_value: A GDB value representing a Ruby class (klass pointer)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Class name string
|
|
131
|
+
"""
|
|
132
|
+
rc = RClass(klass_value)
|
|
133
|
+
return rc.name()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import gdb
|
|
2
|
+
import constants
|
|
3
|
+
import format
|
|
4
|
+
import value
|
|
5
|
+
import rstring
|
|
6
|
+
import rclass
|
|
7
|
+
|
|
8
|
+
class RException:
|
|
9
|
+
"""Wrapper for Ruby exception objects."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, exception_value):
|
|
12
|
+
"""Initialize with a Ruby exception VALUE.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
exception_value: A GDB value representing a Ruby exception object
|
|
16
|
+
"""
|
|
17
|
+
self.value = exception_value
|
|
18
|
+
self._basic = None
|
|
19
|
+
self._klass = None
|
|
20
|
+
self._class_name = None
|
|
21
|
+
self._message = None
|
|
22
|
+
|
|
23
|
+
# Validate it's an object
|
|
24
|
+
if value.is_immediate(exception_value):
|
|
25
|
+
raise ValueError("Exception VALUE cannot be an immediate value")
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def klass(self):
|
|
29
|
+
"""Get the exception's class (klass pointer)."""
|
|
30
|
+
if self._klass is None:
|
|
31
|
+
if self._basic is None:
|
|
32
|
+
self._basic = self.value.cast(constants.get_type('struct RBasic').pointer())
|
|
33
|
+
self._klass = self._basic['klass']
|
|
34
|
+
return self._klass
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def class_name(self):
|
|
38
|
+
"""Get the exception class name as a string."""
|
|
39
|
+
if self._class_name is None:
|
|
40
|
+
self._class_name = self._get_class_name()
|
|
41
|
+
return self._class_name
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def message(self):
|
|
45
|
+
"""Get the exception message as a string (None if unavailable)."""
|
|
46
|
+
if self._message is None:
|
|
47
|
+
self._message = self._get_message()
|
|
48
|
+
return self._message
|
|
49
|
+
|
|
50
|
+
def _get_class_name(self):
|
|
51
|
+
"""Extract class name from klass pointer.
|
|
52
|
+
|
|
53
|
+
Uses the rclass module to get the class name.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
return rclass.get_class_name(self.klass)
|
|
57
|
+
except Exception:
|
|
58
|
+
# Fallback if we can't read the class
|
|
59
|
+
return f"Exception(klass=0x{int(self.klass):x})"
|
|
60
|
+
|
|
61
|
+
def _get_message(self):
|
|
62
|
+
"""Extract message from exception object.
|
|
63
|
+
|
|
64
|
+
Walks instance variables to find 'mesg' ivar.
|
|
65
|
+
Returns None if message is unavailable (common in core dumps).
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
# Exception objects store message in 'mesg' instance variable
|
|
69
|
+
# For now, return None as full ivar walking is complex
|
|
70
|
+
# TODO: Implement full instance variable walking for core dumps
|
|
71
|
+
return None
|
|
72
|
+
except Exception:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def __str__(self):
|
|
76
|
+
"""Return formatted string 'ClassName: message' or just 'ClassName'."""
|
|
77
|
+
class_name = self.class_name
|
|
78
|
+
msg = self.message
|
|
79
|
+
|
|
80
|
+
if msg:
|
|
81
|
+
return f"{class_name}: {msg}"
|
|
82
|
+
else:
|
|
83
|
+
return class_name
|
|
84
|
+
|
|
85
|
+
def print_to(self, terminal):
|
|
86
|
+
"""Print this exception with formatting to the given terminal."""
|
|
87
|
+
class_name = self.class_name
|
|
88
|
+
msg = self.message
|
|
89
|
+
|
|
90
|
+
# Print class name with type formatting
|
|
91
|
+
class_output = terminal.print(
|
|
92
|
+
format.type, class_name,
|
|
93
|
+
format.reset
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if msg:
|
|
97
|
+
# Print message with string formatting
|
|
98
|
+
msg_output = terminal.print(
|
|
99
|
+
format.string, f': {msg}',
|
|
100
|
+
format.reset
|
|
101
|
+
)
|
|
102
|
+
return f"{class_output}{msg_output}"
|
|
103
|
+
else:
|
|
104
|
+
return class_output
|
|
105
|
+
|
|
106
|
+
def print_recursive(self, printer, depth):
|
|
107
|
+
"""Print this exception (no recursion needed for exceptions)."""
|
|
108
|
+
printer.print(self)
|
|
109
|
+
|
|
110
|
+
def is_exception(val):
|
|
111
|
+
"""Check if a VALUE is an exception object.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
val: A GDB value representing a Ruby VALUE
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
True if the value appears to be an exception object, False otherwise
|
|
118
|
+
"""
|
|
119
|
+
if not value.is_object(val):
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Check if it's a T_OBJECT or T_DATA (exceptions can be either)
|
|
124
|
+
basic = val.cast(constants.get_type("struct RBasic").pointer())
|
|
125
|
+
flags = int(basic['flags'])
|
|
126
|
+
type_flag = flags & constants.get("RUBY_T_MASK")
|
|
127
|
+
|
|
128
|
+
# Exceptions are typically T_OBJECT, but could also be T_DATA
|
|
129
|
+
t_object = constants.get("RUBY_T_OBJECT")
|
|
130
|
+
t_data = constants.get("RUBY_T_DATA")
|
|
131
|
+
|
|
132
|
+
return type_flag == t_object or type_flag == t_data
|
|
133
|
+
except Exception:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def RExceptionFactory(val):
|
|
137
|
+
"""Factory function to create RException or return None.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
val: A GDB value representing a Ruby VALUE
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
RException instance if val is an exception, None otherwise
|
|
144
|
+
"""
|
|
145
|
+
if is_exception(val):
|
|
146
|
+
try:
|
|
147
|
+
return RException(val)
|
|
148
|
+
except Exception:
|
|
149
|
+
return None
|
|
150
|
+
return None
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import gdb
|
|
2
|
+
import constants
|
|
3
|
+
import rbasic
|
|
4
|
+
import struct
|
|
5
|
+
import format
|
|
6
|
+
|
|
7
|
+
class RFloatImmediate:
|
|
8
|
+
"""Flonum - immediate float encoding (Ruby 3.4+)"""
|
|
9
|
+
def __init__(self, value):
|
|
10
|
+
self.value = int(value)
|
|
11
|
+
|
|
12
|
+
def float_value(self):
|
|
13
|
+
# Flonum decoding from Ruby's rb_float_flonum_value
|
|
14
|
+
# Special case for 0.0
|
|
15
|
+
if self.value == 0x8000000000000002:
|
|
16
|
+
return 0.0
|
|
17
|
+
|
|
18
|
+
v = self.value
|
|
19
|
+
b63 = (v >> 63) & 1
|
|
20
|
+
# e: xx1... -> 011...
|
|
21
|
+
# xx0... -> 100...
|
|
22
|
+
# ^b63
|
|
23
|
+
adjusted = (2 - b63) | (v & ~0x03)
|
|
24
|
+
# RUBY_BIT_ROTR(x, 3) rotates right by 3 bits
|
|
25
|
+
# Which is: (x >> 3) | (x << (64 - 3))
|
|
26
|
+
rotated = ((adjusted >> 3) | (adjusted << 61)) & 0xFFFFFFFFFFFFFFFF
|
|
27
|
+
|
|
28
|
+
# Pack as unsigned long long, then unpack as double
|
|
29
|
+
return struct.unpack('d', struct.pack('Q', rotated))[0]
|
|
30
|
+
|
|
31
|
+
def __str__(self):
|
|
32
|
+
return f"<T_FLOAT> {self.float_value()}"
|
|
33
|
+
|
|
34
|
+
def print_to(self, terminal):
|
|
35
|
+
"""Return formatted float representation."""
|
|
36
|
+
tag = terminal.print(
|
|
37
|
+
format.metadata, '<',
|
|
38
|
+
format.type, 'T_FLOAT',
|
|
39
|
+
format.metadata, '>',
|
|
40
|
+
format.reset
|
|
41
|
+
)
|
|
42
|
+
num_val = terminal.print(format.number, str(self.float_value()), format.reset)
|
|
43
|
+
return f"{tag} {num_val}"
|
|
44
|
+
|
|
45
|
+
def print_recursive(self, printer, depth):
|
|
46
|
+
"""Print this float (no recursion needed)."""
|
|
47
|
+
printer.print(self)
|
|
48
|
+
|
|
49
|
+
class RFloatObject:
|
|
50
|
+
"""Heap-allocated float object"""
|
|
51
|
+
def __init__(self, value):
|
|
52
|
+
self.value = value.cast(constants.get_type('struct RFloat'))
|
|
53
|
+
|
|
54
|
+
def float_value(self):
|
|
55
|
+
return float(self.value['float_value'])
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
addr = int(self.value.address)
|
|
59
|
+
return f"<T_FLOAT@0x{addr:x}> {self.float_value()}"
|
|
60
|
+
|
|
61
|
+
def print_to(self, terminal):
|
|
62
|
+
"""Return formatted float representation."""
|
|
63
|
+
addr = int(self.value.address)
|
|
64
|
+
tag = terminal.print(
|
|
65
|
+
format.metadata, '<',
|
|
66
|
+
format.type, 'T_FLOAT',
|
|
67
|
+
format.metadata, f'@0x{addr:x}>',
|
|
68
|
+
format.reset
|
|
69
|
+
)
|
|
70
|
+
num_val = terminal.print(format.number, str(self.float_value()), format.reset)
|
|
71
|
+
return f"{tag} {num_val}"
|
|
72
|
+
|
|
73
|
+
def print_recursive(self, printer, depth):
|
|
74
|
+
"""Print this float (no recursion needed)."""
|
|
75
|
+
printer.print(self)
|
|
76
|
+
|
|
77
|
+
def is_flonum(value):
|
|
78
|
+
"""Check if value is an immediate flonum"""
|
|
79
|
+
val_int = int(value)
|
|
80
|
+
# FLONUM_MASK = 0x03, FLONUM_FLAG = 0x02
|
|
81
|
+
FLONUM_MASK = constants.get('RUBY_FLONUM_MASK', 0x03)
|
|
82
|
+
FLONUM_FLAG = constants.get('RUBY_FLONUM_FLAG', 0x02)
|
|
83
|
+
return (val_int & FLONUM_MASK) == FLONUM_FLAG
|
|
84
|
+
|
|
85
|
+
def RFloat(value):
|
|
86
|
+
"""Factory function for float values - handles both flonums and heap objects"""
|
|
87
|
+
# Check for immediate flonum first
|
|
88
|
+
if is_flonum(value):
|
|
89
|
+
return RFloatImmediate(value)
|
|
90
|
+
|
|
91
|
+
# Check for heap-allocated T_FLOAT
|
|
92
|
+
if rbasic.is_type(value, 'RUBY_T_FLOAT'):
|
|
93
|
+
return RFloatObject(value)
|
|
94
|
+
|
|
95
|
+
return None
|