Skip to content

Commit 7301423

Browse files
rwstaunerclaude
andcommitted
Add vmstack debugger command for GDB and LLDB
Adds a `vmstack` command to inspect the Ruby VM operand stack for the current control frame. Supports printing all values, top N, or a specific range, with optional frame selection via -u. GDB: source misc/gdb_vmstack.py LLDB: auto-discovered via misc/lldb_rb/commands/vmstack_command.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6a4402c commit 7301423

2 files changed

Lines changed: 552 additions & 0 deletions

File tree

misc/gdb_vmstack.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"""
2+
GDB command to print Ruby VM stack values for the current control frame.
3+
4+
Usage:
5+
vmstack Print all stack values in the current frame
6+
vmstack N Print the top N stack values
7+
vmstack START END Print stack values in the range [START, END)
8+
vmstack -u UPLEVEL Print stack for a parent frame (0 = current)
9+
vmstack -r Print raw hex values without rp interpretation
10+
11+
Examples:
12+
vmstack Show the full operand stack
13+
vmstack 5 Show the top 5 values (closest to SP)
14+
vmstack 0 3 Show stack slots [0], [1], [2] from the base
15+
vmstack -u 1 Show the stack for the caller's frame
16+
vmstack -u 1 3 Show top 3 of the caller's stack
17+
18+
Source this file from GDB:
19+
source misc/gdb_vmstack.py
20+
"""
21+
22+
import argparse
23+
import textwrap
24+
25+
class VMStack(gdb.Command):
26+
"""Print Ruby VM stack values for the current control frame."""
27+
28+
def __init__(self):
29+
super(VMStack, self).__init__('vmstack', gdb.COMMAND_USER)
30+
self.parser = argparse.ArgumentParser(
31+
prog='vmstack',
32+
description='Print Ruby VM stack values',
33+
)
34+
self.parser.add_argument(
35+
'args', type=int, nargs='*',
36+
help='[count] or [start end] — items to display',
37+
)
38+
self.parser.add_argument(
39+
'-u', '--uplevel', type=int, default=0,
40+
help='CFP offset from the stack top (0 = current frame)',
41+
)
42+
self.parser.add_argument(
43+
'-r', '--raw', action='store_true',
44+
help='Print raw hex values only (skip rp pretty-print)',
45+
)
46+
47+
# ------------------------------------------------------------------ helpers
48+
49+
def get_int(self, expr):
50+
return int(gdb.execute(f'printf "%ld", ({expr})', to_string=True))
51+
52+
def get_string(self, expr):
53+
return gdb.execute(expr, to_string=True)
54+
55+
def rp(self, value):
56+
"""Pretty-print a VALUE using the .gdbinit `rp` command."""
57+
try:
58+
return self.get_string(f'rp {value}').rstrip()
59+
except gdb.error:
60+
return '<error>'
61+
62+
# ------------------------------------------------------------------ invoke
63+
64+
def invoke(self, arg_string, from_tty):
65+
try:
66+
args = self.parser.parse_args(arg_string.split() if arg_string else [])
67+
except SystemExit:
68+
return
69+
70+
cfp = f'(ruby_current_ec->cfp + ({args.uplevel}))'
71+
72+
# Make sure cfp is within bounds
73+
end_cfp = self.get_int(
74+
'ruby_current_ec->vm_stack + ruby_current_ec->vm_stack_size'
75+
)
76+
cfp_addr = self.get_int(cfp)
77+
if cfp_addr >= end_cfp:
78+
print(f'Error: uplevel {args.uplevel} is out of range')
79+
return
80+
81+
cfp_size = self.get_int('sizeof(rb_control_frame_t)')
82+
cfp_index = int((end_cfp - cfp_addr - 1) / cfp_size)
83+
84+
# We need a Ruby frame (with iseq) and it can't be the very first frame
85+
has_iseq = self.get_int(f'{cfp}->iseq') != 0
86+
if not has_iseq:
87+
# For C frames, we can still show sp relative to the previous frame's sp
88+
print(f'CFP[{cfp_index}] is a C frame (no iseq) — showing raw SP region')
89+
self._print_cfunc_stack(cfp, cfp_index, args)
90+
return
91+
92+
if cfp_index == 0:
93+
print(f'Error: cannot compute base pointer for the bottom-most frame')
94+
return
95+
96+
# Compute stack size: sp - base_ptr, in VALUE-sized slots
97+
sp = self.get_int(f'{cfp}->sp')
98+
bp = self.get_int(f'vm_base_ptr({cfp})')
99+
value_size = self.get_int('sizeof(VALUE)')
100+
total = int((sp - bp) / value_size)
101+
102+
if total < 0:
103+
print(f'Error: stack appears corrupt (sp < bp)')
104+
return
105+
106+
# Determine the range to display
107+
start, end = self._resolve_range(args.args, total)
108+
if start is None:
109+
return
110+
111+
self._print_header(cfp, cfp_index, total, start, end, bp, sp)
112+
113+
for i in range(start, end):
114+
addr = bp + i * value_size
115+
value = self.get_int(f'vm_base_ptr({cfp})[{i}]')
116+
label = self._slot_label(cfp, i, total, addr)
117+
if args.raw:
118+
desc = ''
119+
else:
120+
desc = self.rp(value)
121+
if desc:
122+
desc = f' {desc}'
123+
print(f' [{i:3d}] 0x{addr:016x} 0x{value:016x}{label}{desc}')
124+
125+
# -------------------------------------------------------- range resolution
126+
127+
def _resolve_range(self, positional, total):
128+
"""Return (start, end) from positional args, or (None, None) on error."""
129+
if len(positional) == 0:
130+
return 0, total
131+
elif len(positional) == 1:
132+
count = positional[0]
133+
if count > total:
134+
count = total
135+
if count <= 0:
136+
print('Nothing to display (count <= 0)')
137+
return None, None
138+
# "top N" means the N slots closest to SP
139+
return total - count, total
140+
elif len(positional) == 2:
141+
start, end = positional
142+
if start < 0:
143+
start = max(0, total + start)
144+
if end < 0:
145+
end = max(0, total + end)
146+
start = min(start, total)
147+
end = min(end, total)
148+
if start >= end:
149+
print(f'Nothing to display (start={start} >= end={end})')
150+
return None, None
151+
return start, end
152+
else:
153+
print('Error: too many positional arguments (expected [count] or [start end])')
154+
return None, None
155+
156+
# -------------------------------------------------------- display helpers
157+
158+
def _slot_label(self, cfp, index, total, addr):
159+
"""Return a marker string if this slot matches SP or EP."""
160+
labels = []
161+
if addr == self.get_int(f'{cfp}->sp'):
162+
labels.append('SP')
163+
if addr == self.get_int(f'{cfp}->ep'):
164+
labels.append('EP')
165+
if index == total - 1:
166+
labels.append('TOS')
167+
if labels:
168+
return ' <-- ' + ', '.join(labels)
169+
return ''
170+
171+
def _print_header(self, cfp, cfp_index, total, start, end, bp, sp):
172+
# Try to get location info
173+
location = ''
174+
try:
175+
iseq = self.get_int(f'{cfp}->iseq')
176+
if iseq:
177+
label = self.get_string(
178+
f'printf "%s", ISEQ_BODY({cfp}->iseq)->location.label->as.embed.ary'
179+
).strip()
180+
if label:
181+
location = f' ({label})'
182+
except gdb.error:
183+
pass
184+
185+
showing = f'[{start}..{end - 1}]' if end - start < total else 'all'
186+
print(f'--- CFP[{cfp_index}]{location} stack_size={total} showing {showing} ---')
187+
print(f' BP=0x{bp:x} SP=0x{sp:x}')
188+
189+
def _print_cfunc_stack(self, cfp, cfp_index, args):
190+
"""For C frames where vm_base_ptr isn't available, show the area
191+
between the previous frame's SP and this frame's SP."""
192+
sp = self.get_int(f'{cfp}->sp')
193+
# The previous frame (cfp+1) has the base we can reference
194+
prev_sp = self.get_int(f'(ruby_current_ec->cfp + ({args.uplevel} + 1))->sp')
195+
value_size = self.get_int('sizeof(VALUE)')
196+
total = int((sp - prev_sp) / value_size)
197+
if total <= 0:
198+
print(' (empty stack)')
199+
return
200+
start, end = self._resolve_range(args.args, total)
201+
if start is None:
202+
return
203+
print(f' prev_SP=0x{prev_sp:x} SP=0x{sp:x} slots={total}')
204+
for i in range(start, end):
205+
addr = prev_sp + i * value_size
206+
value = self.get_int(f'((VALUE *)0x{prev_sp:x})[{i}]')
207+
desc = ''
208+
if not args.raw:
209+
desc = self.rp(value)
210+
if desc:
211+
desc = f' {desc}'
212+
print(f' [{i:3d}] 0x{addr:016x} 0x{value:016x}{desc}')
213+
214+
215+
VMStack()

0 commit comments

Comments
 (0)