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,357 @@
1
+ # Stack Inspection
2
+
3
+ This guide explains how to inspect both Ruby VM stacks and native C stacks when debugging Ruby programs.
4
+
5
+ ## Understanding Ruby's Dual Stack System
6
+
7
+ Ruby programs operate with two distinct stacks that serve different purposes. Understanding both is crucial for effective debugging, especially when tracking down segfaults, stack overflows, or unexpected behavior in C extensions.
8
+
9
+ Use stack inspection when you need:
10
+
11
+ - **Trace execution flow**: Understand the sequence of method calls that led to the current state
12
+ - **Debug C extensions**: See both Ruby and native frames when extensions are involved
13
+ - **Find stack overflows**: Identify deep recursion in either Ruby or C code
14
+ - **Understand fiber switches**: See where fibers yield and resume
15
+
16
+ ## The Two Stack Types
17
+
18
+ ### VM Stack (Ruby Level)
19
+
20
+ The VM stack holds:
21
+ - Ruby method call frames (control frames)
22
+ - Local variables and temporaries
23
+ - Method arguments
24
+ - Block parameters
25
+ - Return values
26
+
27
+ This is what you see with Ruby's `caller` method at runtime.
28
+
29
+ ### C Stack (Native Level)
30
+
31
+ The C/machine stack holds:
32
+ - Native function call frames
33
+ - C local variables
34
+ - Saved registers
35
+ - Return addresses
36
+
37
+ This is what GDB's `bt` command shows by default.
38
+
39
+ ## Inspecting VM Stacks
40
+
41
+ ### Current Frame Information
42
+
43
+ See the current Ruby control frame:
44
+
45
+ ~~~
46
+ (gdb) set $ec = ruby_current_execution_context_ptr
47
+ (gdb) set $cfp = $ec->cfp
48
+ (gdb) p $cfp->pc # Program counter
49
+ (gdb) p $cfp->sp # Stack pointer
50
+ (gdb) p $cfp->iseq # Instruction sequence
51
+ (gdb) p $cfp->ep # Environment pointer
52
+ ~~~
53
+
54
+ ### VM Stack for a Fiber
55
+
56
+ Show detailed VM stack information:
57
+
58
+ ~~~
59
+ (gdb) rb-fiber-vm-stack 5
60
+ VM Stack for Fiber #5:
61
+ Base: 0x7f8a1c950000
62
+ Size: 4096 VALUEs (32768 bytes)
63
+ CFP: 0x7f8a1c951000
64
+
65
+ Use GDB commands to examine:
66
+ x/4096gx 0x7f8a1c950000 # Examine as hex values
67
+ p *((VALUE *)0x7f8a1c950000)@100 # Print first 100 values
68
+ ~~~
69
+
70
+ ### Walking Control Frames
71
+
72
+ See detailed information about each Ruby method call:
73
+
74
+ ~~~
75
+ (gdb) rb-fiber-vm-frames 5
76
+ VM Control Frames for Fiber #5:
77
+ Fiber: 0x7f8a1c800500 (status: SUSPENDED)
78
+
79
+ Frame #0 (depth 45):
80
+ CFP Address: 0x7f8a1c951000
81
+ PC: 0x7f8a1c234500
82
+ SP: 0x7f8a1c950100
83
+ EP: 0x7f8a1c950200
84
+ Self: 0x7f8a1c888888
85
+ Location: /app/lib/connection.rb:123
86
+ Method: read
87
+ Frame Type: VM_FRAME_MAGIC_METHOD
88
+ Stack Depth: 256 slots
89
+
90
+ Frame #1 (depth 44):
91
+ ...
92
+ ~~~
93
+
94
+ This shows the complete Ruby call chain.
95
+
96
+ ### Values on Stack Top
97
+
98
+ Inspect the current values being computed:
99
+
100
+ ~~~
101
+ (gdb) rb-fiber-stack-top 5 10
102
+ VM Stack Top for Fiber #5:
103
+
104
+ Top 10 VALUE(s) on stack (newest first):
105
+
106
+ [ -1] 0x00007f8a1c888888 T_STRING "data"
107
+ [ -2] 0x0000000000000015 Fixnum(10) Fixnum: 10
108
+ [ -3] 0x00007f8a1c999999 T_HASH <hash:len=3>
109
+ [ -4] 0x0000000000000000 Qfalse Qfalse
110
+ ...
111
+ ~~~
112
+
113
+ This is useful for understanding what values are being passed between methods.
114
+
115
+ ## Inspecting C Stacks
116
+
117
+ ### C Stack Information
118
+
119
+ View native stack details for a fiber:
120
+
121
+ ~~~
122
+ (gdb) rb-fiber-c-stack 5
123
+ C/Machine Stack for Fiber #5:
124
+ Fiber: 0x7f8a1c800500
125
+ Stack Base: 0x7f8a1e000000
126
+ Stack Size: 1048576 bytes
127
+ Stack Start: 0x7f8a1e000000
128
+ Stack End: 0x7f8a1e0f0000
129
+ Stack Used: 983040 bytes (grows down)
130
+ ~~~
131
+
132
+ ### Walking C Frames
133
+
134
+ Attempt to walk the native call stack:
135
+
136
+ ~~~
137
+ (gdb) rb-fiber-c-frames 5
138
+ C/Native Stack Frames for Fiber #5:
139
+ ...
140
+
141
+ Frame #0: (initial context)
142
+ SP: 0x7f8a1e0f0000
143
+
144
+ Frame #1:
145
+ FP: 0x7f8a1e0f0010
146
+ Return Address: 0x7f8a1ab12345 - fiber_setcontext
147
+
148
+ Frame #2:
149
+ FP: 0x7f8a1e0f0080
150
+ Return Address: 0x7f8a1ab12400 - rb_fiber_yield
151
+ ...
152
+ ~~~
153
+
154
+ Note: For suspended fibers, this may be incomplete. Use `rb-fiber-switch` for accurate C backtraces.
155
+
156
+ ## Practical Examples
157
+
158
+ ### Finding Where Execution Stopped
159
+
160
+ Identify the exact location in both Ruby and C:
161
+
162
+ ~~~
163
+ (gdb) rb-fiber-bt 5 # Ruby backtrace
164
+ 45: /app/lib/connection.rb:123:in `read'
165
+
166
+ (gdb) rb-fiber-switch 5 # Switch to fiber context
167
+ (gdb) bt # C backtrace
168
+ #0 fiber_setcontext
169
+ #1 rb_fiber_yield
170
+ #2 rb_io_wait_readable
171
+ #3 rb_io_read
172
+ ~~~
173
+
174
+ This shows the fiber is suspended in `read`, waiting for I/O.
175
+
176
+ ### Debugging Deep Recursion
177
+
178
+ Detect excessive call depth:
179
+
180
+ ~~~
181
+ (gdb) rb-fiber-vm-frames 5 | grep "Frame #" | wc -l
182
+ 134 # 134 Ruby frames!
183
+
184
+ (gdb) rb-fiber-vm-frames 5 | grep "Location"
185
+ Location: /app/lib/parser.rb:45
186
+ Location: /app/lib/parser.rb:45
187
+ Location: /app/lib/parser.rb:45
188
+ ... # Same method recursing
189
+ ~~~
190
+
191
+ Identifies a recursion issue in the parser.
192
+
193
+ ### Examining Method Arguments
194
+
195
+ See what was passed to a method:
196
+
197
+ ~~~
198
+ (gdb) rb-fiber-vm-frames 5
199
+ Frame #0:
200
+ Stack Depth: 3 slots # Method has 3 values on stack
201
+
202
+ (gdb) rb-fiber-stack-top 5 3
203
+ [ -1] 0x00007f8a1c888888 T_STRING "filename.txt"
204
+ [ -2] 0x00000000000000b5 Fixnum(90) Fixnum: 90
205
+ [ -3] 0x00007f8a1c777777 T_HASH <options>
206
+
207
+ (gdb) rb-object-print 0x00007f8a1c777777 --depth 2
208
+ AR Table (options hash with mode, encoding, etc.)
209
+ ~~~
210
+
211
+ ### Tracking Fiber Switches
212
+
213
+ See the call stack across fiber boundaries:
214
+
215
+ ~~~
216
+ (gdb) rb-fiber-switch 5
217
+ (gdb) bt
218
+ #0 fiber_setcontext
219
+ #1 rb_fiber_yield # Fiber yielded here
220
+ #2 rb_io_wait_readable # Waiting for I/O
221
+ #3 some_io_operation
222
+
223
+ (gdb) frame 3
224
+ (gdb) info locals # C variables at I/O call
225
+ ~~~
226
+
227
+ ## Combining VM and C Stacks
228
+
229
+ For the complete picture, inspect both:
230
+
231
+ ~~~
232
+ (gdb) rb-fiber-bt 5 # Ruby perspective
233
+ 45: /app/lib/connection.rb:123:in `read'
234
+ 44: /app/lib/connection.rb:89:in `receive'
235
+
236
+ (gdb) rb-fiber-switch 5 # Switch context
237
+ (gdb) bt # C perspective
238
+ #0 fiber_setcontext
239
+ #1 rb_fiber_yield
240
+ #2 rb_io_wait_readable
241
+ #3 rb_io_read
242
+ #4 rb_io_sysread_internal
243
+ ~~~
244
+
245
+ This shows:
246
+ - Ruby level: `read` method in connection.rb
247
+ - C level: Suspended in `rb_io_wait_readable`
248
+
249
+ The combination reveals the full execution path.
250
+
251
+ ## Best Practices
252
+
253
+ ### Start with Ruby Backtraces
254
+
255
+ Always check Ruby-level backtraces first:
256
+
257
+ ~~~
258
+ (gdb) rb-all-fiber-bt # Overview of all fibers
259
+ (gdb) rb-fiber-bt 5 # Detailed Ruby backtrace
260
+ ~~~
261
+
262
+ This gives context before diving into C-level details.
263
+
264
+ ### Use Fiber Switching for Accuracy
265
+
266
+ For the most accurate C stack views, switch to the fiber:
267
+
268
+ ~~~
269
+ (gdb) rb-fiber-switch 5
270
+ (gdb) bt # Accurate backtrace
271
+ (gdb) frame 2 # Navigate frames
272
+ (gdb) info args # See C function arguments
273
+ ~~~
274
+
275
+ Walking frames manually (`rb-fiber-c-frames`) is best-effort only.
276
+
277
+ ### Check Frame Types
278
+
279
+ Different frame types have different data available:
280
+
281
+ ~~~
282
+ Frame Type: VM_FRAME_MAGIC_METHOD # Regular Ruby method
283
+ Frame Type: VM_FRAME_MAGIC_CFUNC # C function called from Ruby
284
+ Frame Type: VM_FRAME_MAGIC_BLOCK # Block/lambda
285
+ Frame Type: VM_FRAME_MAGIC_IFUNC # Internal function
286
+ ~~~
287
+
288
+ C function frames don't have ISEQ (instruction sequence) data.
289
+
290
+ ## Common Pitfalls
291
+
292
+ ### Confusing Stack Direction
293
+
294
+ Ruby's VM stack grows up (toward higher addresses):
295
+
296
+ ~~~
297
+ VM Stack: 0x7f8a1c950000 - 0x7f8a1c960000
298
+ Current SP: 0x7f8a1c950100 # Near the base
299
+ ~~~
300
+
301
+ But the C stack typically grows down:
302
+
303
+ ~~~
304
+ Stack Start: 0x7f8a1e100000
305
+ Stack End: 0x7f8a1e000000 # Lower address
306
+ ~~~
307
+
308
+ Pay attention to which stack you're examining.
309
+
310
+ ### Accessing Out-of-Bounds
311
+
312
+ Don't access stack beyond valid range:
313
+
314
+ ~~~
315
+ (gdb) rb-fiber-stack-top 5 10000 # Might read invalid memory
316
+ ~~~
317
+
318
+ Check the stack depth first.
319
+
320
+ ### Terminated Fiber Stacks
321
+
322
+ Terminated fibers don't have valid saved contexts:
323
+
324
+ ~~~
325
+ (gdb) rb-fiber 8
326
+ Status: TERMINATED # No useful stack data
327
+ ~~~
328
+
329
+ Focus on SUSPENDED and RESUMED fibers for debugging.
330
+
331
+ ## Troubleshooting
332
+
333
+ ### Stack Appears Empty
334
+
335
+ If `rb-fiber-bt` shows no frames:
336
+
337
+ 1. Check fiber status: `rb-fiber 5`
338
+ 2. Verify it's not TERMINATED
339
+ 3. Try: `rb-fiber-vm-frames 5` for raw frame data
340
+ 4. Use: `rb-fiber-debug-unwind 5` to see saved register state
341
+
342
+ ### C Backtrace Too Short
343
+
344
+ After `rb-fiber-switch`, if `bt` shows few frames:
345
+
346
+ 1. The fiber may be newly created
347
+ 2. Check: `rb-fiber-bt 5` for Ruby-level frames
348
+ 3. Compare with: `rb-fiber-vm-frames 5` for all VM frames
349
+
350
+ The C backtrace only shows where the fiber was suspended, not the full Ruby call chain.
351
+
352
+ ## See Also
353
+
354
+ - {ruby Ruby::GDB::object-inspection Object inspection} for examining stack values
355
+ - {ruby Ruby::GDB::fiber-debugging Fiber debugging} for fiber-specific commands
356
+ - {ruby Ruby::GDB::heap-debugging Heap debugging} for finding objects on the heap
357
+
@@ -0,0 +1,254 @@
1
+ class Arguments:
2
+ """Structured result from parsing GDB command arguments.
3
+
4
+ Attributes:
5
+ expressions: List of expressions to evaluate (e.g., ["$var", "$ec->cfp->sp[-1]"])
6
+ flags: Set of boolean flags (e.g., {'debug'})
7
+ options: Dict of options with values (e.g., {'depth': 3})
8
+ """
9
+ def __init__(self, expressions, flags, options):
10
+ self.expressions = expressions
11
+ self.flags = flags
12
+ self.options = options
13
+
14
+ def has_flag(self, flag_name):
15
+ """Check if a boolean flag is present"""
16
+ return flag_name in self.flags
17
+
18
+ def get_option(self, option_name, default=None):
19
+ """Get an option value with optional default"""
20
+ return self.options.get(option_name, default)
21
+
22
+ class ArgumentParser:
23
+ """Parse GDB command arguments handling nested brackets, quotes, and flags.
24
+
25
+ This parser correctly handles:
26
+ - Nested brackets: $ec->cfp->sp[-1]
27
+ - Nested parentheses: ((struct foo*)bar)->baz
28
+ - Quotes: "string value"
29
+ - Flags: --debug, --depth 3
30
+ - Complex expressions: foo + 10, bar->ptr[idx * 2]
31
+ """
32
+
33
+ def __init__(self, argument_string):
34
+ self.argument_string = argument_string.strip()
35
+ self.position = 0
36
+ self.length = len(self.argument_string)
37
+
38
+ def parse(self):
39
+ """Parse the argument string and return (expressions, flags, options)
40
+
41
+ Returns:
42
+ tuple: (expressions: list[str], flags: set, options: dict)
43
+ - expressions: List of expressions to evaluate
44
+ - flags: Set of boolean flags (e.g., {'debug'})
45
+ - options: Dict of options with values (e.g., {'depth': 3})
46
+ """
47
+ flags = set()
48
+ options = {}
49
+ expressions = []
50
+
51
+ while self.position < self.length:
52
+ self.skip_whitespace()
53
+ if self.position >= self.length:
54
+ break
55
+
56
+ if self.peek() == '-' and self.peek(1) == '-':
57
+ # Parse a flag or option
58
+ flag_name, flag_value = self.parse_flag()
59
+ if flag_value is None:
60
+ flags.add(flag_name)
61
+ else:
62
+ options[flag_name] = flag_value
63
+ else:
64
+ # Parse an expression
65
+ expression = self.parse_expression()
66
+ if expression:
67
+ expressions.append(expression)
68
+
69
+ return expressions, flags, options
70
+
71
+ def peek(self, offset=0):
72
+ """Peek at character at current position + offset"""
73
+ position = self.position + offset
74
+ if position < self.length:
75
+ return self.argument_string[position]
76
+ return None
77
+
78
+ def consume(self, count=1):
79
+ """Consume and return count characters"""
80
+ result = self.argument_string[self.position:self.position + count]
81
+ self.position += count
82
+ return result
83
+
84
+ def skip_whitespace(self):
85
+ """Skip whitespace characters"""
86
+ while self.position < self.length and self.argument_string[self.position].isspace():
87
+ self.position += 1
88
+
89
+ def parse_flag(self):
90
+ """Parse a flag starting with --
91
+
92
+ Returns:
93
+ tuple: (flag_name: str, value: str|None)
94
+ - For boolean flags: ('debug', None)
95
+ - For valued flags: ('depth', '3')
96
+ """
97
+ # Consume '--'
98
+ self.consume(2)
99
+
100
+ # Read flag name
101
+ flag_name = ''
102
+ while self.position < self.length and self.argument_string[self.position].isalnum():
103
+ flag_name += self.consume()
104
+
105
+ # Check if flag has a value
106
+ self.skip_whitespace()
107
+
108
+ # If next character is not another flag and not end of string, it might be a value
109
+ if self.position < self.length and not (self.peek() == '-' and self.peek(1) == '-'):
110
+ # Try to parse a value - it could start with $, digit, or letter
111
+ if self.peek() and not self.peek().isspace():
112
+ value = ''
113
+ while self.position < self.length and not self.argument_string[self.position].isspace():
114
+ if self.peek() == '-' and self.peek(1) == '-':
115
+ break
116
+ value += self.consume()
117
+
118
+ # Try to convert to int if it's a number
119
+ try:
120
+ return flag_name, int(value)
121
+ except ValueError:
122
+ return flag_name, value
123
+
124
+ return flag_name, None
125
+
126
+ def parse_expression(self):
127
+ """Parse a single expression, stopping at whitespace (unless nested) or flags.
128
+
129
+ An expression can be:
130
+ - A quoted string: "foo" or 'bar'
131
+ - A parenthesized expression: (x + y)
132
+ - A variable with accessors: $ec->cfp->sp[-1]
133
+ - Any combination that doesn't contain unquoted/unnested whitespace
134
+ """
135
+ expression = ''
136
+
137
+ while self.position < self.length:
138
+ self.skip_whitespace()
139
+ if self.position >= self.length:
140
+ break
141
+
142
+ character = self.peek()
143
+
144
+ # Stop at flags
145
+ if character == '-' and self.peek(1) == '-':
146
+ break
147
+
148
+ # Handle quoted strings - these are complete expressions
149
+ if character in ('"', "'"):
150
+ quoted = self.parse_quoted_string(character)
151
+ expression += quoted
152
+ # After a quoted string, we're done with this expression
153
+ break
154
+
155
+ # Handle parentheses - collect the whole balanced expression
156
+ if character == '(':
157
+ expression += self.parse_balanced('(', ')')
158
+ continue
159
+
160
+ # Handle brackets
161
+ if character == '[':
162
+ expression += self.parse_balanced('[', ']')
163
+ continue
164
+
165
+ # Handle braces
166
+ if character == '{':
167
+ expression += self.parse_balanced('{', '}')
168
+ continue
169
+
170
+ # Stop at whitespace (this separates expressions)
171
+ if character.isspace():
172
+ break
173
+
174
+ # Regular character - part of a variable name, operator, etc.
175
+ expression += self.consume()
176
+
177
+ return expression.strip()
178
+
179
+ def parse_quoted_string(self, quote_character):
180
+ """Parse a quoted string, handling escapes"""
181
+ result = self.consume() # Opening quote
182
+
183
+ while self.position < self.length:
184
+ character = self.peek()
185
+
186
+ if character == '\\':
187
+ # Escape sequence
188
+ result += self.consume()
189
+ if self.position < self.length:
190
+ result += self.consume()
191
+ elif character == quote_character:
192
+ # Closing quote
193
+ result += self.consume()
194
+ break
195
+ else:
196
+ result += self.consume()
197
+
198
+ return result
199
+
200
+ def parse_balanced(self, open_character, close_character):
201
+ """Parse a balanced pair of delimiters (e.g., parentheses, brackets)"""
202
+ result = self.consume() # Opening delimiter
203
+ depth = 1
204
+
205
+ while self.position < self.length and depth > 0:
206
+ character = self.peek()
207
+
208
+ # Handle quotes inside balanced delimiters
209
+ if character in ('"', "'"):
210
+ result += self.parse_quoted_string(character)
211
+ continue
212
+
213
+ if character == open_character:
214
+ depth += 1
215
+ elif character == close_character:
216
+ depth -= 1
217
+
218
+ result += self.consume()
219
+
220
+ return result
221
+
222
+ def parse_arguments(input):
223
+ """Convenience function to parse argument string.
224
+
225
+ Arguments:
226
+ input: The raw argument string from GDB command
227
+
228
+ Returns:
229
+ Arguments: Structured object with expressions, flags, and options
230
+
231
+ Examples:
232
+ >>> arguments = parse_arguments("$var --debug")
233
+ >>> arguments.expressions
234
+ ['$var']
235
+ >>> arguments.has_flag('debug')
236
+ True
237
+
238
+ >>> arguments = parse_arguments('"foo" "bar" --depth 3')
239
+ >>> arguments.expressions
240
+ ['"foo"', '"bar"']
241
+ >>> arguments.get_option('depth')
242
+ 3
243
+
244
+ >>> arguments = parse_arguments("$ec->cfp->sp[-1] --debug --depth 2")
245
+ >>> arguments.expressions
246
+ ['$ec->cfp->sp[-1]']
247
+ >>> arguments.has_flag('debug')
248
+ True
249
+ >>> arguments.get_option('depth')
250
+ 2
251
+ """
252
+ parser = ArgumentParser(input)
253
+ expressions, flags, options = parser.parse()
254
+ return Arguments(expressions, flags, options)
@@ -0,0 +1,59 @@
1
+ import gdb
2
+
3
+ # Global shared cache for Ruby constants
4
+ _CACHE = {}
5
+
6
+ # Global shared cache for GDB type lookups
7
+ _TYPE_CACHE = {}
8
+
9
+ def get(name, default=None):
10
+ """Get a Ruby constant value, with caching.
11
+
12
+ Arguments:
13
+ name: The constant name (e.g., 'RUBY_T_STRING', 'RUBY_FL_USER1')
14
+ default: Default value if constant cannot be found
15
+
16
+ Returns:
17
+ The integer value of the constant, or default if not found
18
+
19
+ Raises:
20
+ Exception if constant not found and no default provided
21
+ """
22
+ if name in _CACHE:
23
+ return _CACHE[name]
24
+ try:
25
+ val = int(gdb.parse_and_eval(name))
26
+ _CACHE[name] = val
27
+ return val
28
+ except Exception:
29
+ if default is None:
30
+ raise
31
+ _CACHE[name] = default
32
+ return default
33
+
34
+ def get_type(type_name):
35
+ """Get a GDB type, with caching.
36
+
37
+ Arguments:
38
+ type_name: The type name (e.g., 'struct RString', 'struct RArray')
39
+
40
+ Returns:
41
+ The GDB type object
42
+
43
+ Raises:
44
+ Exception if type cannot be found
45
+ """
46
+ if type_name in _TYPE_CACHE:
47
+ return _TYPE_CACHE[type_name]
48
+
49
+ gdb_type = gdb.lookup_type(type_name)
50
+ _TYPE_CACHE[type_name] = gdb_type
51
+ return gdb_type
52
+
53
+ def clear():
54
+ """Clear the constants and type caches.
55
+
56
+ Useful when switching between different Ruby processes or versions.
57
+ """
58
+ _CACHE.clear()
59
+ _TYPE_CACHE.clear()