Vlad Lazarenko

... making all this up as I go along

C++ Exceptions, Stack Trace and GDB Automation

The tricky part about any exception is that the stack is already unwinded by the time exception is caught and there is no easy way to figure out from which part of the code it was thrown. Have you ever caught an exception that has no information that can be used to find where the problem is? The one that says «this should never happen» or even has no text at all. Hopefully, this doesn’t happen to you very often. But if you work with a large C++ codebase with tons of different components written by different developers then sooner or later it would definitely happen. It can be quite disappointing and take a lot of time going through the pile of somebody else’s code trying to figure out what went wrong. So you must be prepared. I might be a little bit unlucky in this regard – this happened to me a lot, so I decided to share a few ways for overcoming this type of situations.

Backtrace API

Quite a few C runtime libraries provide a mechanism to examine the current thread’s call stack. It consists of three functions declared in execinfo.h header:

1
2
3
4
5
int backtrace(void** array, int size);

char** backtrace_symbols(void* const* array, int size);

void backtrace_symbols_fd(void* const* array, int size, int fd);

Using those functions, one can access the stack trace at any given point of program execution. Here is a simple example that prints the stack to standard output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <execinfo.h>
#include <iostream>

int main()
{
    void *callstack[256];
    int frames = ::backtrace(
        callstack, sizeof(callstack) / sizeof(callstack[0]));
    char **symbols = ::backtrace_symbols(callstack, frames);
    std::cout << "Stack Trace:\n";
    for (int i = 0; i < frames; ++i) {
        std::cout << '\t' << symbols[i] << '\n';
    }
    std::free(symbols);
}

This method can be used to get the stack trace before the exception is thrown and before the stack is unwinded. In order to do that, one could define a custom exception class that grabs the stack trace in its constructor. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <execinfo.h>
#include <string>
#include <stdexcept>
#include <cstdlib>
#include <iostream>

class Exception : public std::exception {
  public:
    Exception() {
        grab_backtrace();
    }

    explicit Exception(const std::string &reason) : reason_(reason) {
        reason_.append(1, '\n');
        grab_backtrace();
    }

    virtual ~Exception() throw() { }

    virtual const char *what() const throw() {
        return reason_.c_str();
    }

  private:
    void grab_backtrace() {
        void *callstack[256];
        int frames = ::backtrace(
            callstack, sizeof(callstack) / sizeof(callstack[0]));
        char **symbols = backtrace_symbols(callstack, frames);
        try {
            reason_.append("Stack Trace:");
            for (int i = 0; i < frames; ++i) {
                reason_.append("\n\t").append(symbols[i]);
            }
        } catch(const std::exception &) {
            std::free(symbols);
            throw;
        }
        std::free(symbols);
    }

    std::string reason_;
};

static void do_something(int n, int i = 0)
{
    if (i < n) {
        do_something(n, ++i);
    } else {
        throw Exception("Recursion limit exceeded");
    }
}

int main(int argc, char *argv[])
{
    try {
        do_something(argc > 1 ? std::atoi(argv[1]) : 10, argc);
    } catch(const std::exception &e) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }
}

So far so good! Here is an example of running the above program:

1
2
3
4
5
6
7
8
9
10
$ clang++ -Wall -pedantic ./test.cpp && ./a.out 3
Recursion limit exceeded
Stack Trace:
  0   a.out                0x00000001034fa7af _ZN9Exception14grab_backtraceEv + 63
  1   a.out                0x00000001034fa6f4 _ZN9ExceptionC2ERKSs + 116
  2   a.out                0x00000001034fa56d _ZN9ExceptionC1ERKSs + 29
  3   a.out                0x00000001034fa33c _Z12do_somethingii + 140
  4   a.out                0x00000001034fa2df _Z12do_somethingii + 47
  5   a.out                0x00000001034fa42d main + 93
  6   libdyld.dylib        0x00007fff935e27e1 start + 0

Unfortunately, there are a few problems with this approach. Firstly, there no file names and no line numbers. Secondly, this approach requires to use the “Exception” class as a base class of all exceptions in the project (well, at least for those you want to get a stack trace for), which indeed can be very problematic if not impossible. But even if all of the above is fine with you, this approach may still not work out for you because frame pointers can be omitted in optimized builds. For example, omitting frame pointers is a default behavior of recent GCC compilers for x86_64 platforms (which can also be turned on/off using -fomit-frame-pointer option). Our call stack becomes useless if frame pointers are omitted:

1
2
3
4
$ clang++ -Wall -fomit-frame-pointer ./test.cpp && ./a.out 3
Recursion limit exceeded
Stack Trace:
  0   a.out                0x0000000105eca82d _ZN9Exception14grab_backtraceEv + 61

Depending on the system, there could be other requirements in order to make this API work. For example, GNU runtime requires you to specify “-rdynamic” flag to instruct the linker to add all symbols, not only used ones, to the dynamic symbol table. So don’t forget to read a manual page for this API in your system before using this.

__FILE__ and __LINE__

C++ has many standard predefined macros. __FILE__ and __LINE__ macros are those two that come handy in order to identify a point in a program:

__FILE__ macros expands to the name of the current input file, in the form of a C string constant. This is the path by which the preprocessor opened the file, not the short name specified in ‘#include’ or as the input file name argument. For example, “/usr/local/include/myheader.h” is a possible expansion of this macros.

__LINE__ macros expands to the current input line number, in the form of a decimal integer constant. While we call it a predefined macro, it’s a pretty strange macro, since its “definition” changes with each new line of source code.|

Using the above macros, it is possible to include both file and line information along with exception’s text, or maybe as additional member fields of any given exception class. There are multiple choices. Here is an example of one of possible implementations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdexcept>
#include <cstdlib>
#include <iostream>

class Exception : public std::runtime_error {
  public:
    template <unsigned int location_len>
    inline Exception(const char (&location)[location_len],
                     const std::string &reason)
        : std::runtime_error(reason + location)
    { }

    virtual ~Exception() throw() { }
};

#define MY_THROW_STR_I(Arg) #Arg
#define MY_THROW_STR(Arg) MY_THROW_STR_I(Arg)
#define MY_THROW(Type, ...)                                             \
    do {                                                                \
        throw Type(" @ "; __FILE__ ";:" MY_THROW_STR(__LINE__), ##__VA_ARGS__); \
    } while (0)

static void do_something(int n, int i = 0)
{
    if (i < n) {
        do_something(n, ++i);
    } else {
        MY_THROW(Exception, "Recursion limit exceeded");
    }
}

int main(int argc, char *argv[])
{
    try {
        do_something(argc > 1 ? std::atoi(argv[1]) : 10, argc);
    } catch(const std::exception &e) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }
}

When the exception is caught and printed, the source file name and a line numbers are included, which makes it easy to trace the origins of such an exception:

1
2
$ ./a.out
Recursion limit exceeded @ ./test.cpp:26

Of course, this does not include a stack trace. But it works if frame pointers are omitted, tail recursion optimization is applied, or even if all symbol names are stripped out of the executable. There is one problem though — it may not be useful without a stack trace in certain cases. For example, one may declare a helper function used to throw an exception, like this:

1
2
3
__attribute__((noreturn)) void report_error(const std::string &reason) {
    MY_THROW(Exception, reason);
}

In that case, the file and line of the exception origins will always be the same even if it was called from different places in the program. So this approach is also not a cure for all diseases.

GDB Scripting: Automatic Backtrace on Exception Catchpoints

And the last but not least method is to use GDB debugger. If you are not familiar with this debugger, you definitely should spend some time learning it. It is one of the most powerful tools out there. And it comes extremely handy when dealing with exceptions.

GDB provides special catch points, including those to catch exceptions being thrown, caught or even unhandled. We are interested in exceptions that are being thrown and so must use catch throw command. Here is an example of manually using GDB in order to examine a stack trace before the exception is thrown:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ gdb -args ./a.out 5
Reading symbols from /tmp/a.out...done.
(gdb) catch throw
Catchpoint 1 (throw)
(gdb) run
Starting program: /tmp/a.out 5
Catchpoint 1 (exception thrown), __cxxabiv1::__cxa_throw (obj=0x6030d0, tinfo=0x401420 , dest=0x401198 )
    at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:70
70      header->exc.unexpectedHandler = __unexpected_handler;
(gdb) backtrace
#0  __cxxabiv1::__cxa_throw (obj=0x6030d0, tinfo=0x401420 , dest=0x401198 )
    at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:70
#1  0x0000000000400fee in report_error (reason="Recursion limit exceeded") at ./test.cpp:25
#2  0x000000000040105d in do_something (n=5, i=5) at ./test.cpp:33
#3  0x000000000040102e in do_something (n=5, i=5) at ./test.cpp:31
#4  0x000000000040102e in do_something (n=5, i=4) at ./test.cpp:31
#5  0x000000000040102e in do_something (n=5, i=3) at ./test.cpp:31
#6  0x00000000004010cb in main (argc=2, argv=0x7fffffffe1f8) at ./test.cpp:40
(gdb) continue
Continuing.
Recursion limit exceeded @ ./test.cpp:25
[Inferior 1 (process 3865) exited with code 01]
(gdb) quit

At first, this might sound silly because in the real world a program may encounter a lot of exceptions and manually typing “backtrace” and “continue” every type that happens is nothing but a waste of time. But there is one trick — GDB can be automated. There are two ways of doing this — use a batch mode or a more sophisticated Python scripting. For our purpose, a batch script will do just fine. Here is a simple script that turns off verbose output and paging, sets up a throw catch point and executes backtrace + continue commands every time the event is caught:

1
2
3
4
5
6
7
8
9
set verbose off
set pagination off
catch throw
commands
backtrace
continue
end
run
quit

Save the above script into a file, and then run GDB in batch mode telling it to use that file, which in my case is called “gdb_bt_script”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ gdb -n -batch -x ./gdb_bt_script --args ./a.out 5
Catchpoint 1 (throw)
Catchpoint 1 (exception thrown), __cxxabiv1::__cxa_throw (obj=0x6030d0, tinfo=0x401420 , dest=0x401198 ) at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:70
70      header->exc.unexpectedHandler = __unexpected_handler;
#0  __cxxabiv1::__cxa_throw (obj=0x6030d0, tinfo=0x401420 , dest=0x401198 ) at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:70
#1  0x0000000000400fee in report_error (reason="Recursion limit exceeded") at ./test.cpp:25
#2  0x000000000040105d in do_something (n=5, i=5) at ./test.cpp:33
#3  0x000000000040102e in do_something (n=5, i=5) at ./test.cpp:31
#4  0x000000000040102e in do_something (n=5, i=4) at ./test.cpp:31
#5  0x000000000040102e in do_something (n=5, i=3) at ./test.cpp:31
#6  0x00000000004010cb in main (argc=2, argv=0x7fffffffe1f8) at ./test.cpp:40
Recursion limit exceeded @ ./test.cpp:25
[Inferior 1 (process 5849) exited with code 01]
$

Whoala! Now we get a full stack trace including function parameters, their values, and file and line numbers. If the program is optimized and has no debug symbols, however, we get a little bit less:

1
2
3
4
5
6
7
8
9
10
$ gdb -n -batch -x ./gdb_bt_script --args ./a.out 5
Catchpoint 1 (throw)
Catchpoint 1 (exception thrown), __cxxabiv1::__cxa_throw (obj=0x6030d0, tinfo=0x401470 , dest=0x401270 ) at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:70
70      header->exc.unexpectedHandler = __unexpected_handler;
#0  __cxxabiv1::__cxa_throw (obj=0x6030d0, tinfo=0x401470 , dest=0x401270 ) at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:70
#1  0x00000000004010a8 in report_error(std::string const&) ()
#2  0x000000000040121e in do_something(int, int) ()
#3  0x0000000000400ef6 in main ()
Recursion limit exceeded @ ./test.cpp:25
[Inferior 1 (process 1877) exited with code 01]

Yet still a lot more than we get using backtrace API. But the most beautiful part is that this method does not require any code changes and works for pretty much any binary.

The End

Those were three fundamental methods that can help to identify a place where exception is thrown from. They are not mutually exclusive. Each has its cons and pros. I personally prefer to use GDB because it doesn’t require a code change, but use other two methods as well, depending on a situation. It is up to you to decide which one to use.

Hope it helps and Good Luck!