Are you still adding printf/puts calls and restarting your app to
figure what went wrong? Sometimes, the problem is hard to reproduce, or you
only discover it in production. You've got a process that exhibits the
bug, but you didn't run it under
ruby-debug, so there's no
choice but kill it and reproduce after adding some code to inspect your
program, right?
Sure not. Jamis Buck blogged about how to use
GDB to inspect a live Ruby process,
and showed how to get a stack-trace using Ruby's C API and some GDB scripting:
(gdb) set $ary = (int)backtrace(-1)
(gdb) set $count = *($ary+8)
(gdb) set $index = 0
(gdb) while $index < $count
> x/1s *((int)rb_ary_entry($ary, $index)+12)
> set $index = $index + 1
>end
But it gets much easier than that. How about this:
(gdb) eval "caller"
or
(gdb) eval "local_variables"
Once you've opened that door, you get a full-powered Ruby interpreter inside
GDB. Ruby's introspection capabilities do the rest. Local variables, instance
variables, classes, methods, Ruby threads, object counts... evil eval can
bring us pretty far. You can find the scripts to turn GDB into a sort of IRB
that can attach to running processes below.
A synthetic example
For the sake of illustration, let's take this trivial program:
def fooa=1bar+aenddef bara=2b=2baz+1enddef bazc=232c+gets.to_iendputs"foo returned #{foo}"
$ gdb ruby
GNU gdb 6.1-debian
...
(gdb) attach 13658
Attaching to program: /home/batsman/usr/bin/ruby, process 13525
...
0xa7ec526e in read () from /lib/tls/libc.so.6
(gdb)
I don't want to pollute gdb with lots of commands, so I've put the Ruby magic
in a separate script that can be loaded with
(gdb) session-ruby
(I also have a session-asm that imports a number of commands useful for
low-level inspection.)
At this point, I could just
(gdb) eval "caller"
but that would #p() the caller's result in the stdout from the process
being examined. So I first redirect $stdout with another command:
(gdb) redirect_stdout
$1 = 2
(gdb) eval "caller"
This way, the result from eval is put in /tmp/ruby-debug.PID, and I get:
The last result might look surprising. How come there's a (Ruby) local
variable, if we're running a method implemented in C (rb_f_gets)?
It seems ruby doesn't get out of its way to overwrite the (Ruby) frame info when it
calls a C method, so the data you get corresponds to the enclosing Ruby
method.
Running until the end of the current method (aka "finish")