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