Stack Inspection
This guide explains how to inspect both Ruby VM stacks and native C stacks when debugging Ruby programs.
Understanding Ruby's Dual Stack System
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.
Use stack inspection when you need:
- Trace execution flow: Understand the sequence of method calls that led to the current state
- Debug C extensions: See both Ruby and native frames when extensions are involved
- Find stack overflows: Identify deep recursion in either Ruby or C code
- Understand fiber switches: See where fibers yield and resume
The Two Stack Types
VM Stack (Ruby Level)
The VM stack holds:
- Ruby method call frames (control frames)
- Local variables and temporaries
- Method arguments
- Block parameters
- Return values
This is what you see with Ruby's caller method at runtime.
C Stack (Native Level)
The C/machine stack holds:
- Native function call frames
- C local variables
- Saved registers
- Return addresses
This is what GDB's bt command shows by default.
Inspecting VM Stacks
Current Frame Information
See the current Ruby control frame:
(gdb) set $ec = ruby_current_execution_context_ptr
(gdb) set $cfp = $ec->cfp
(gdb) p $cfp->pc # Program counter
(gdb) p $cfp->sp # Stack pointer
(gdb) p $cfp->iseq # Instruction sequence
(gdb) p $cfp->ep # Environment pointer
Combined Stack Trace
The simplest way to see both Ruby and C frames:
(gdb) rb-fiber-scan-heap
(gdb) rb-fiber-scan-switch 5
(gdb) rb-stack-trace
Combined Ruby/C backtrace for Fiber #5:
[C] fiber_setcontext
[R] /app/lib/connection.rb:123:in `read'
[C] rb_io_wait_readable
[R] /app/lib/connection.rb:89:in `receive'
...
This shows both levels of the call stack in order.
Detailed Frame Information
For advanced debugging, you can inspect the raw VM frames:
(gdb) set $ec = ruby_current_execution_context_ptr
(gdb) set $cfp = $ec->cfp
# Current frame details:
(gdb) p $cfp->pc # Program counter
(gdb) p $cfp->sp # Stack pointer
(gdb) p $cfp->iseq # Instruction sequence
(gdb) p $cfp->ep # Environment pointer
Inspecting C Stacks
C Backtrace with GDB
After switching to a fiber, use standard GDB commands to inspect the C stack:
(gdb) rb-fiber-scan-switch 5
(gdb) bt # Show C backtrace
(gdb) frame 2 # Switch to specific frame
(gdb) info args # Show function arguments
(gdb) info locals # Show local variables
The fiber unwinder automatically integrates with GDB's backtrace functionality, so bt shows the correct C stack for the selected fiber.
Practical Examples
Finding Where Execution Stopped
Identify the exact location in both Ruby and C:
(gdb) rb-fiber-scan-heap
(gdb) rb-fiber-scan-switch 5 # Switch to fiber context
(gdb) rb-stack-trace # Combined backtrace
[R] /app/lib/connection.rb:123:in `read'
[C] rb_fiber_yield
[C] rb_io_wait_readable
[R] /app/lib/connection.rb:89:in `receive'
This shows the fiber is suspended in read, waiting for I/O.
Debugging Deep Recursion
Detect excessive call depth:
(gdb) rb-fiber-scan-heap
(gdb) rb-fiber-scan-switch 5
(gdb) rb-stack-trace | grep "/app/lib/parser.rb:45" | wc -l
134 # Same line appearing 134 times!
Identifies a recursion issue in the parser.
Examining Stack Values
See what values are on the current frame's stack:
(gdb) rb-fiber-scan-switch 5
(gdb) set $sp = $ec->cfp->sp
# Print values on stack
(gdb) rb-object-print *(VALUE*)($sp - 1) # Top of stack
(gdb) rb-object-print *(VALUE*)($sp - 2) # Second value
(gdb) rb-object-print *(VALUE*)($sp - 3) # Third value
Tracking Fiber Switches
See the call stack across fiber boundaries:
(gdb) rb-fiber-scan-switch 5
(gdb) bt
#0 fiber_setcontext
#1 rb_fiber_yield # Fiber yielded here
#2 rb_io_wait_readable # Waiting for I/O
#3 some_io_operation
(gdb) frame 3
(gdb) info locals # C variables at I/O call
Combining VM and C Stacks
For the complete picture, use the combined stack trace:
(gdb) rb-fiber-scan-switch 5
(gdb) rb-stack-trace
Combined Ruby/C backtrace:
[R] /app/lib/connection.rb:123:in `read'
[C] rb_io_wait_readable
[C] rb_io_read
[R] /app/lib/connection.rb:89:in `receive'
[C] rb_fiber_yield
[R] /app/lib/server.rb:56:in `handle_client'
...
Or separately:
(gdb) rb-stack-trace --values # Ruby perspective with stack values
(gdb) bt # C perspective only
The combined view reveals the full execution path through both Ruby and C code.