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!