This guide explains how to inspect both Ruby VM stacks and native C stacks when debugging Ruby programs.
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 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.
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.
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
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.
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
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.
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.
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.
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-print *(VALUE*)($sp - 1) # Top of stack
(gdb) rb-print *(VALUE*)($sp - 2) # Second value
(gdb) rb-print *(VALUE*)($sp - 3) # Third value
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
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.