Gdb support for exceptions (Re: using backtrace() in exception throwing?)

Tom Tromey tromey at redhat.com
Fri Feb 22 10:14:19 PST 2013


>>>>> "Michael" == Michael Meeks <michael.meeks at suse.com> writes:

Michael> The basic debugging experience in these "an exception broke
Michael> something" flows is that we get an exception thrown that
Michael> ultimately ends up in a pathalogical situation - an abort, or
Michael> some similar horrible badness. At that point the most
Michael> interesting thing is not the catcher - which usually ends up
Michael> being utterly random - but the last guy that threw the
Michael> exception. So then as Lubos says comes the knotty job of trying
Michael> to put a breakpoint on the -one- exception that ends up being
Michael> caught where we are now [ and that of course requires
Michael> re-running, and inevitably we throw dozens of exceptions in the
Michael> normal case ].

Thanks.  This kind of discussion is very helpful to me.

This problem is a bit tricky.

The various low-level exception-related functions, like __cxa_throw,
treat the exception object as a "void *".  However, the value of this
seems to change depending on the "throw" point.  It's clear that this
can't always be the argument to throw, due to scalar and object throws.
So I wonder what exactly it refers to.  I'll have to dig a bit deeper to
see how all this code really works.

Anyway, this makes tracking backward from std::terminate to the original
throw point more difficult.


It helps a bit to install the libstdc++ debuginfo.  Then at least you
can dig into some of the details.  However, due to optimization, even
with the improvements in newer version of gcc, this turns out to be less
than perfect.


I tried this out to see what it was like.  It is kind of awful!  At
least, I had to dig around through several frames of libstdc++ to find
the object that lead to std::terminate being called:

terminate called after throwing an instance of 'char const*'

Program received signal SIGABRT, Aborted.
0x0000003be3036285 in __GI_raise (sig=6)
    at ../nptl/sysdeps/unix/sysv/linux/raise.c:64
64	  return INLINE_SYSCALL (tgkill, 3, pid, selftid, sig);
(gdb) up
#1  0x0000003be3037b9b in __GI_abort () at abort.c:91
91	      raise (SIGABRT);
(gdb) up
#2  0x0000003be80bbc5d in __gnu_cxx::__verbose_terminate_handler ()
    at ../../../../libstdc++-v3/libsupc++/vterminate.cc:95
95	    abort();
(gdb) up
#3  0x0000003be80b9e16 in __cxxabiv1::__terminate (handler=<optimized out>)
    at ../../../../libstdc++-v3/libsupc++/eh_terminate.cc:40
40	      handler ();
(gdb) up
#4  0x0000003be80b9e43 in std::terminate ()
    at ../../../../libstdc++-v3/libsupc++/eh_terminate.cc:50
50	  __terminate (__terminate_handler);
(gdb) up
#5  0x0000003be80b9f3e in __cxxabiv1::__cxa_throw (obj=0x601090, 
    tinfo=<optimized out>, dest=<optimized out>)
    at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:83
83	  std::terminate ();


At this point I can do:

(gdb) catch throw
Catchpoint 1 (throw)
(gdb) cond 1 obj == 0x601090
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
[Inferior 25299 exited]
Starting program: /tmp/r 
warning: failed to reevaluate condition for breakpoint 1: No symbol "obj" in current context.
warning: failed to reevaluate condition for breakpoint 1: No symbol "obj" in current context.
warning: failed to reevaluate condition for breakpoint 1: No symbol "obj" in current context.
Catchpoint 1 (exception thrown), __cxxabiv1::__cxa_throw (obj=0x601090, tinfo=
    0x600a60, dest=0) at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:70
70	  header->exc.unexpectedHandler = __unexpected_handler;


Ignore the warnings; I'm not sure what they are about, but I will file a
bug.

... but all this still fails if you insert a "manual re-throw" like
"throw x;" into the call chain.  At that point it gets really messy :(


Michael> Really nice ! though of course - having a full stack trace
Michael> would make that very substantially more useful.

This is reasonably easy to implement.  It may be expensive.  I've
appended a version that does this... well, it lists file name and line
number for all the frames.  If you want to get a really full stack
trace, capturing the arguments and locals, then you would have to do
more work.

Michael> Even better than this would (perhaps) be a "break inside thrower that
Michael> is caught here" type breakpoint - that we could invoke to land us in
Michael> whatever code is going to throw as it does that [ and before it started
Michael> all the magic cleanup / unwinding work ]. That is - assuming that it's
Michael> possible for the code to know (at that point) where it will ultimately
Michael> end up (? ;-)

I think it isn't possible in general.  When an exception is thrown, I
think all that can really be determined is the next catch point.

What this means is that if you have a series of throws and re-throws,
winding up at some "catch", then the best you could do is stop at the
re-throw that leads to that catch.

Does that make sense?

Like:

    void doit()
    {
      throw "hi";
    }

    void dd2()
    {
      try {
        doit();
      } catch (const char *x) {
        throw x;
      }
    }

    int main()
    {
      try {
        dd2();
      } catch (const char *x) {
        // The problem spot.
      }
    }

Here, suppose the comment marks the catch you are concerned with.
You want to find the throw that leads to this point.

The original throw in "doit" can only see as far as the catchpoint in
"dd2".  That is because arbitrary code can be run there -- for example
it may swallow the exception and no more throwing is even done.

So this hypothetical breakpoint would only trigger at the re-throw in
dd2.

But, that isn't what you want.  And from there it is even hard to track
backwards, you have to "catch catch" and filter for the particular one
you want.



I'm curious what types of exceptions are actually thrown in LibreOffice.
Does it throw -- scalars?  Objects?  Just pointers ("Java style")?


A few improvements come to mind.  I'd like to hear your take on these,
or any other ideas you've got.


It seems like it would be nice if gdb exposed some kind of convenience
variable so that "catch catch" and "catch throw" could be conditional on
the thrown object without needing the libstdc++ debuginfo.

This may require some libstdc++ change, perhaps a probe point.


It would be nice to solve the problem above, of following an exception
back to its ultimate origination point.  It seems like this would be
useful even if it were not 100% reliable, in the sense that it is ok to
have a few extra breakpoints -- filtering 90% of uninteresting
exceptions is better than filtering 0% of them.

If we had the convenience variable mentioned above, and if LibreOffice
has a relatively simple "exception identity" measure (e.g., if you only
throw pointers, you can just compare them with ==), then it could
perhaps be done by: break at the losing catch, make a conditional "catch
throw", then re-run.

Tom

import gdb

last_sals = None

throw_bp = None

class ThrowTracker(gdb.Breakpoint):
    def __init__(self):
        gdb.Breakpoint.__init__(self, '__cxa_throw')

    def stop(self):
        global last_sals
        frame = gdb.newest_frame().older()
        last_sals = []
        while frame is not None:
            last_sals.append(frame.find_sal())
            frame = frame.older()
        return False

class TrackThrows(gdb.Command):
    def __init__(self):
        gdb.Command.__init__(self, 'track-throws', gdb.COMMAND_BREAKPOINTS)

    def invoke(self, arg, from_tty):
        global throw_bp
        if throw_bp is None or not throw_bp.is_valid():
            # Still no good way to create a pending breakpoint from
            # Python.
            save = gdb.parameter('breakpoint pending')
            gdb.execute('set breakpoint pending on', to_string = True)
            throw_bp = ThrowTracker()
            if save is None:
                arg = 'auto'
            elif save:
                arg = 'on'
            else:
                arg = 'off'
            gdb.execute('set breakpoint pending %s' % arg, to_string = True)

class InfoThrow(gdb.Command):
    def __init__(self):
        gdb.Command.__init__(self, 'info last-throw', gdb.COMMAND_BREAKPOINTS)

    def invoke(self, arg, from_tty):
        global last_sals
        if last_sals is not None:
            first = True
            for sal in last_sals:
                filename = sal.symtab.filename
                line = sal.line
                if first:
                    print "Last exception thrown at file %s, line %d" % (filename, line)
                    first = False
                else:
                    print "From %s, %d" % (filename, line)
        else:
            print "No previous exception seen"

TrackThrows()
InfoThrow()


More information about the LibreOffice mailing list