Ruby GDB Scripts
A while ago, I found a pair of really useful GDB functions for debugging live Ruby processes on ThoughtBot's blog:
define redirect_stdout
call rb_eval_string("$_old_stdout, $stdout = $stdout, File.open('/tmp/ruby-debug.' + Process.pid.to_s, 'a'); $stdout.sync = true")
end
define ruby_eval
call(rb_p(rb_eval_string_protect($arg0,(int*)0)))
end
These proved really handy but they have a couple of problems relating to garbage collection and Macs, so I made a few modifications.
NOTE: If you don't care about the explanation, scroll to the bottom of the post for the modified script.
# Problem with garbage collection
In Ruby, when the garbage collector is running, no object allocations are allowed. At all. For any reason. It's so not allowed, in fact, the process will segfault if you try. Check out this test script:
puts Process.pid
loop do
100.times { Object.new }
GC.start
end
When we set it running and GDB into it, we can try and run the redirect_stdout
helper function from ThoughtBot but will almost certainly see the following
happen in the Ruby process:
test.rb:5: [BUG] object allocation during garbage collection phase
ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-darwin12.5.0]
-- Crash Report log information --------------------------------------------
See Crash Report log file under the one of following:
...
And this in the GDB process:
Program received signal SIGABRT, Aborted.
0x00007fff915a5212 in __pthread_kill ()
The program being debugged was signaled while in a function called from GDB.
GDB remains in the frame where the signal was received.
To change this behavior use "set unwindonsignal on"
Evaluation of the expression containing the function (rb_eval_string) will be abandoned.
And it's all because of this code around line 1276 in gc.c
of the Ruby core
code base:
static VALUE
newobj_of(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3)
{
rb_objspace_t *objspace = &rb_objspace;
VALUE obj;
if (UNLIKELY(during_gc)) {
dont_gc = 1;
during_gc = 0;
rb_bug("object allocation during garbage collection phase");
}
if (UNLIKELY(ruby_gc_stress && !ruby_disable_gc_stress)) {
if (!garbage_collect(objspace, FALSE, FALSE, GPR_FLAG_NEWOBJ)) {
during_gc = 0;
rb_memerror();
}
}
// ... Continues on for a while
Notice line 10. Yup. Computer says no.
# Getting around the garbage collector problem
Ruby keeps a variable called during_gc
that should be 0 when the GC is not
running. It also provides access to this variable through the function
rb_during_gc()
(which is important seeing as we only have a symbol table to
play with and would not otherwise be able to access the variable directly).
We'll call rb_during_gc()
in our GDB script to check that we aren't going to
break anything. The next problem, though...
# Unable to call function? Try harder!
If you've tried to use the ThoughtBot .gdbinit
script on a Mac machine you may
have noticed that this happens:
Unable to call function "rb_eval_string" at 0x1019e8850: no return type
information available.
To call this function anyway, you can cast the return type explicitly (e.g.
'print (float) fabs (3.0)')
Urk. I think this is less a Mac problem and more a
binary-does-not-have-type-information problem, which could be something that's
specific with how Ruby gets compiled on a Mac (I do it through ruby-install and
Apple's gcc
binary is actually clang
, so this doesn't sound unreasonable).
To get around this, we need to specifically cast our function calls to their proper return types (it's important we get this right, otherwise weird things might happen).
The call to rb_during_gc()
returns an int
and the rb_eval_string*()
calls
can be cast to void
seeing as we don't care much about their return values.
# tl;dr: The modified script
define redirect_stdout
if (int)rb_during_gc() > 0
printf "Ruby is GCing. Object allocation will crash the process.\n"
else
call (void)rb_eval_string("$gc_was_disabled = GC.disable")
call (void)rb_eval_string("$_old_stdout = $stdout")
call (void)rb_eval_string("$stdout = File.open('/tmp/ruby-debug.' + Process.pid.to_s, 'a')")
call (void)rb_eval_string("$stdout.sync = true")
call (void)rb_eval_string("GC.enable unless $gc_was_disabled")
end
end
define ruby_eval
if (int)rb_during_gc() > 0
printf "Ruby is GCing. Object allocation will crash the process.\n"
else
call (void)rb_eval_string("$gc_was_disabled = GC.disable")
call (void)rb_eval_string_protect($arg0,(int*)0)
call (void)rb_eval_string("GC.enable unless $gc_was_disabled")
end
end
This was tested on Ruby 1.9.3 and Ruby 2.1.0 on a Mac OSX 10.8 machine and an Arch Linux virtual machine. Hopefully now there will be no more tears shed when an important process is accidentally segfaulted because you forgot to check if the GC was running :)