Unconstant Conjunction A personal blog

Setting up GDB for Debugging Python on OS X

Note: this post can be read as a series of terminal commands if you’re not interested in the intervening explanations.

Recently, I started writing a Python extension that wraps the procedural generation library libnoise using SIP-generated code, and encountered the inevitable segfaults. It didn’t take long to establish that the error was somewhere in my C++ code, but I couldn’t figure out where exactly it was. A couple of searches online revealed that the way to go about finding these things was to use a proper debugger, and because the GNU Debugger – ``gdb` – supports Python, that seemed to be the way to go.

Getting it working was not, however, particularly straightforward. gdb supports debugging Python code as of version 7. Unfortunately, Apple’s version of gdb is currently sitting at 6.3. So all I had to do was get a newer version, right? Wrong.

Assuming you have Homebrew – if you don’t, go read about it – you can get a newer version of gdb from there. Since gdb is sort of a compiler, it’s not available in the main Homebrew repository (there’s a policy you can read on the wiki), so you’ll have to:

$ brew tap homebrew/dupes

first, and then run:

$ brew install gdb

You’ll notice immediately that if you run gdb --version in a terminal you’ll see:

GNU gdb 6.3.50-20050815 (Apple version gdb-1822) (Sun Aug  5 03:00:42 UTC 2012)
Copyright 2004 Free Software Foundation, Inc.
...
This GDB was configured as "x86_64-apple-darwin".

(That’s right, the Apple version is circa 2004…) Since you now have two versions of gdb installed, the shell will by default start the one in /usr/bin, while your Homebrew’d version is located in /usr/local/bin. There are a few ways to fix this. One is to edit the order of the paths in /etc/paths; you could, if you wanted, copy over the system version of gdb, too. However, both of these solutions may run you into other problems, particularly if you use XCode, which depends on Apple’s version of gdb for debugging. I haven’t actually tried deleting the system gdb, but I suspect you could screw some related things up this way. Instead, I prefer the simple renaming method::

$ mv /usr/local/bin/gdb /usr/local/bin/gdb7

Which allows you to run gdb7 --version and get the expected::

GNU gdb (GDB) 7.6.1
Copyright (C) 2013 Free Software Foundation, Inc.
...
This GDB was configured as "x86_64-apple-darwin12.5.0".

So far, so good. Unfortunately, debuggers need to inspect the internals of other processes, which programs aren’t normally allowed to do. So in order to really use gdb you need to give it permission to do this. Follow the instructions on the gdb website here to create a certificate, and then run::

$ codesign -s gdb-cert gdb7

Now it’s time to test debugging Python code, which should be as easy as gdb7 python. However, this gives:

"/usr/bin/python": not in executable format: File format not recognized

on OS X Mountain Lion. The reason? The system python executable is a ‘fat’ binary, meaning it can run on multiple system archtectures, i.e.:

$ file /usr/bin/python
/usr/bin/python: Mach-O universal binary with 2 architectures
/usr/bin/python (for architecture i386):	Mach-O executable i386
/usr/bin/python (for architecture x86_64):	Mach-O 64-bit executable x86_64

Unfortunately, gdb does not support debugging fat binaries (at least according to their mailing list). So you’ll have to extract the right system architecture using the lipo (hah!) program. If you have an Intel Mac running a 64-bit operating system (the mostly likely), that means the “x86_64” one. As a side note, the python program will actually call on the specific version of Python when you launch it, meaning that the executable you’re really interested in is /usr/bin/python2.7:

$ file /usr/bin/python2.7
/usr/bin/python2.7: Mach-O universal binary with 2 architectures
/usr/bin/python2.7 (for architecture i386):	Mach-O executable i386
/usr/bin/python2.7 (for architecture x86_64):	Mach-O 64-bit executable x86_64

From which we can extract a “thin”, architecture-specific binary (I’ve called python64) as follows::

$ lipo /usr/bin/python2.7 -thin x86_64 -output /usr/bin/python64

You can now run:

$ gdb7 python64

And see that:

Reading symbols from /usr/bin/python64...(no debugging symbols found)...done.

As expected. I’m not quite sure what gdb means when it says that python does not have debugging symbols – backtraces seem to work fine anyway. To verify that it works, try something like:

$ gdb7 python64
...
(gdb) run test.py
Starting program: /usr/bin/python64 test.py

Program received signal SIGTRAP, Trace/breakpoint trap.
0x00007fff5fc01028 in ?? ()
(gdb) continue
Continuing.
Hello, world!

Note that for some reason, Python always sends a SIGTRAP before it begins executing the script. Don’t worry, just continue. If you want to find out why your program segfaults, you can run bt:

Program received signal SIGSEGV, Segmentation fault.
0x000000010003441a in PyErr_Occurred () from /System/Library/Frameworks/Python.framework/Versions/2.7/Python
(gdb) bt
#0  0x000000010003441a in PyErr_Occurred () from /System/Library/Frameworks/Python.framework/Versions/2.7/Python
#1  0x000000010003a81b in _PyArg_NoKeywords () from /System/Library/Frameworks/Python.framework/Versions/2.7/Python
#2  0x0000000100039c91 in _PyArg_NoKeywords () from /System/Library/Frameworks/Python.framework/Versions/2.7/Python
#3  0x0000000100038c43 in PyArg_Parse () from /System/Library/Frameworks/Python.framework/Versions/2.7/Python
#4  0x0000000100038f0d in PyArg_ParseTuple () from /System/Library/Frameworks/Python.framework/Versions/2.7/Python
#5  0x0000000100493335 in meth_Clamp_SetBounds () from /Users/Aaron/Code/Python/simplex-test/noisypy/noisypy.so
#6  0x00000001000136c6 in PyObject_Call () from /System/Library/Frameworks/Python.framework/Versions/2.7/Python
...

Which located the error pretty quickly my wrapper of Clamp.SetBounds().

Happy debugging!

comments powered by Disqus