Ruby GDB Scripts
samwho keyboard logo

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 :)

powered by buttondown