Debugging Native RubyGems - Part 1

As the first in a series on debugging native C RubyGems on Windows using the RubyInstaller's DevKit, this post focuses on a few configuration actions you must take in order to begin the actual debugging session using GDB from the DevKit.

First, if you haven't done so already, install Ruby on your Windows system using one of the RubyInstaller downloads.

Second, ensure you've installed the RubyInstaller's DevKit toolchain. If you're installing it for the first time, read these instructions. No really, read the instructions!

Third, install the rake-compiler gem by typing gem install rake-compiler.

In future posts we'll be building and debugging (on Windows) a native C RubyGem that causes the Ruby interpreter to segfault and crash. Don't worry, the crash is isolated to the Ruby interpreter rather than your Windows system. UPDATE: for those wanting to get a jump on Part 2, here's the oops-null source repo for the native RubyGem, and the source gem is available from rubygems.org. If you want to start debugging it, ensure the DevKit is installed and type gem install oops-null -- --enable-debug.

In general, building native RubyGems on Windows is rather straightforward these days once you've properly configured your RubyInstaller + DevKit environment to take advantage of the rake-compiler. The key remaining technical hurdle is to figure out how to ensure both Ruby and your native RubyGem includes the debugging symbols required for efficient debugging with GDB.

The first one's easy; all of our RubyInstaller downloads are built with debugging symbols included.

However, building your native RubyGem with debugging symbols is a bit more challenging because, by default, symbols are stripped from the native shared library by lines similar to the following found in the gem's mkmf generated Makefile:

# Ruby 1.9.2 build configuration

cflags   =  $(optflags) $(debugflags) $(warnflags)
optflags = -O3
debugflags = -g
...
CFLAGS   =  $(cflags)
...
# NOTE: MRI 1.8.7 has LDSHARED = gcc -shared -s
LDSHARED = $(CC) -shared $(if $(filter-out -g -g0,$(debugflags)),,-s)
...
$(DLLIB): $(DEFFILE) $(OBJS) Makefile
    @-$(RM) $(@)
    $(LDSHARED) -o $@ $(OBJS) $(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)

Is this a bug with MRI Ruby's build configuration? Not at all.

As debugging symbols add size to your shared library, for typical use you want the symbols removed. Also, while you normally want more performance rather than less, the -O3 performance optimization can make it harder to debug the root cause of many problems.

Since this is Just How MRI Ruby Works, you're stuck right? Not really since you're a persistent hacker who rarely takes 'No' for an answer. You puzzle over the above Makefile snippet for awhile, look at the mkmf.rb source code, then decide to include the following little hack (lines 3-8) in the gem's extconf.rb, the file responsible for creating the Makefile which builds and installs the native RubyGem.

require 'mkmf'

# override normal build configuration to build debug friendly library
# if installed via 'gem install oops-null -- --enable-debug'
if enable_config('debug')
  puts '[INFO] enabling debug library build configuration.'
  if RUBY_VERSION < '1.9'
    $CFLAGS = CONFIG['CFLAGS'].gsub(/\s\-O\d?\s/, ' -O0 ')
    $CFLAGS.gsub!(/\s?\-g\w*\s/, ' -ggdb3 ')
    CONFIG['LDSHARED'] = CONFIG['LDSHARED'].gsub(/\s\-s(\s|\z)/, ' ')
  else
    CONFIG['debugflags'] << ' -ggdb3 -O0'
  end
end

create_makefile('oops_null/oops_null')

UPDATE: refactored to work with MRI 1.8.7 and use proper mkmf customization.

NOTE: another option may be to put a similar hack into the gem's Rakefile and count on require's behavior of trying to prevent multiple loads of the same rbconfig file. This could cause other issues, but I've not looked into it.

While you can simply type gem install oops-null -- --enable-debug to build and install the debuggable oops-null native RubyGem, if you've cloned the oops-null source repository you can also build the debuggable gem locally and install it by typing something similar to:


C:\projects\oops-null-git>rake gem
(in C:/projects/oops-null-git)
Temporarily enhancing PATH to include DevKit...
mkdir -p pkg
...
ln Rakefile pkg/oops-null-0.2.0/Rakefile
WARNING:  no rubyforge_project specified
mv oops-null-0.2.0.gem pkg/oops-null-0.2.0.gem


C:\projects\oops-null-git>gem install pkg\oops-null-0.2.0.gem -- --enable-debug
Temporarily enhancing PATH to include DevKit...
Building native extensions.  This could take a while...
Successfully installed oops-null-0.2.0
1 gem installed

Conclusion

While the above hack isn't perfect by any means, adding it to a gem's extconf.rb currently ensures that the gem's native shared library will contain usable debug symbols needed by GDB. While this hack works for your gem's, it doesn't solve the problem when you're trying to debug someone else's native C RubyGem.

In the next post, I'll show you one way to begin debugging (on Windows with GDB) a native C RubyGem that segfault's the Ruby interpreter. Until then, if you've found another clever way to build native RubyGem's with included symbols, I'd like to hear from you. Drop me an email!

« Back