Debugging Ruby programs on OSX with lldb
Out of frustration with a stuck test on a Rails project I recently explored the possibilities of debugging Ruby scripts on OSX. There are various built-in ways how to debug (e.g. via
puts or more gems like
pry-byebug), but because I didn't want to deep-dive into the project I wanted to stay with built-in tools that ship with my operating system.
The first thing I could remember about system debugging was
gdb, or also known as the GNU debugger. OSX no longer ships with gdb or any other tools of the gcc toolchain, but instead uses the LLVM toolchain. The substitute there is called
lldb. Getting started is actually quite easy, as you've probably installed XCode already and so you also have
Let's get started - first of all we need the PID of the Ruby process we want to debug, so we run:
> ps | grep ruby 17400 ttys000 0:00.28 ruby bin/rspec 14807 ttys002 0:00.00 grep --color=auto --exclude-dir=.git --exclude-dir=.hg ruby
The first line is our Ruby program, which has the PID
17400. Next, we start up
lldb and attach to the program via
> lldb (lldb) attach 17400 Process 17400 stopped * thread #1: tid = 0x17d6329, 0x00007fff95dc9c8a libsystem_kernel.dylib`__psynch_cvwait + 10, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x00007fff95dc9c8a libsystem_kernel.dylib`__psynch_cvwait + 10 libsystem_kernel.dylib`__psynch_cvwait: -> 0x7fff95dc9c8a <+10>: jae 0x7fff95dc9c94 ; <+20> 0x7fff95dc9c8c <+12>: movq %rax, %rdi 0x7fff95dc9c8f <+15>: jmp 0x7fff95dc2d6f ; cerror_nocancel 0x7fff95dc9c94 <+20>: retq Executable module set to "/Users/kitto/.rbenv/versions/2.3.1/bin/ruby". Architecture set to: x86_64h-apple-macosx. (lldb)
Here is where the fun starts: You only see the machine code, because, well, that's what machines understand.
The nice thing about
lldb is that you can use Python to write helper scripts. I've assembled one with two helpful commands to ease debugging Ruby programs. We'll see in a minute what they will do for us:
# ~./lldb/ruby.py import lldb def __lldb_init_module(debugger, internal_dict): debugger.HandleCommand('command script add -f ruby.rb_backtrace rb_backtrace') debugger.HandleCommand('command script add -f ruby.rb_eval rb_eval') def rb_backtrace(debugger, command, result, internal_dict): debugger.HandleCommand('expr (void)rb_backtrace()') def rb_eval(debugger, command, result, internal_dict): debugger.HandleCommand('expr (void *)rb_p((void *)rb_eval_string_protect(%s, (int *) 0))' % command)
We can import that script into lldb via
command script import ~/.lldb/ruby.py, which will add the two commands
rb_eval to our session.
Next, let's execute
rb_backtrace, which will print a stacktrace of our script to its stdout:
[rspec related lines omitted] from /Users/kitto/Projects/olymp/spec/interactors/book_interactor_spec.rb:11:in `block (3 levels) in <top (required)>' from /Users/kitto/Projects/olymp/app/interactors/book_interactor.rb:18:in `perform' from /Users/kitto/Projects/olymp/app/interactors/book_interactor.rb:28:in `with_lock' from /Users/kitto/Projects/olymp/app/interactors/book_interactor.rb:28:in `map' from /Users/kitto/Projects/olymp/app/interactors/book_interactor.rb:28:in `block in with_lock' from /Users/kitto/Projects/olymp/app/interactors/book_interactor.rb:28:in `sleep'
That gives us a lot of information about the reason for that hanging test. In my case, it was just about an old configuration file still lying around and carrying a timeout value way too high for testing.
Let's quickly examine the second command we've installed,
rb_eval. As the name implies, this lets you execute arbitrary Ruby code from within
With that you can print or even change variables, call methods, manipulate objects,… Basically everything that you would do in a normal debugging session. Say we want to see a list of all running threads in our program, we would execute
rb_eval("Thread.list"), which would give us this list:
#<Thread:0x007fa63107f3d0> #<Honeybadger::Agent::Thread:0x007fa621552570> #<Honeybadger::Agent::Worker::Thread:0x007fa63c0a4e18> #<Honeybadger::Agent::Worker::Thread:0x007fa6237eeca0>
I hope you enjoyed this very quick introduction to lldb in conjunction with Ruby. If you know any tricks, let me know in the comments!