Stack Backtracing Inside Your Program
If you usually work with non-trivial C sources, you may have wondered which execution path (that is, which sequence of function calls) brought you to a certain point in your program. Also, it would be even more useful if you could have that piece of information whenever your beautiful, bug-free program suddenly crashes, and you have no debugger at hand. What is needed is a stack backtrace and, thanks to a little known feature of the GNU C library, obtaining it is a fairly easy task.
Before diving into the article, let's briefly go over how function calls and parameters pass work in C. In order to prepare for the function call, parameters are pushed on the stack in reverse order. Afterwards, the caller's return address also is pushed on the stack and the function is called. Finally, the called function's entry code creates some more space on the stack for storage of automatic variables. This layout commonly is called a stack frame for that particular instance of the function call. When more function calls are nested, the whole procedure is repeated, causing the stack to keep growing downwards and building a chain of stack frames (see Figure 1). Thus, at any given point in a program it theoretically is possible to backtrace the sequence of stack frames to the originating calling point, up to the main() function (to be exact, up to the libc function, which calls main() when the process starts up).
Getting the stack backtrace with GDB (or an equivalent graphical front end) for a program that crashed while running is straightforward: you simply issue the bt command, which returns the list of functions called up to the point of the crash. As this is a standard practice, we do not provide any more details here; have a look at the GDB info page if you need specifics (info gdb stack gets you there).
If for some reason you're not running inside a debugger, two options are available for tracing what the program is doing. The first method is to disseminate it with print and log messages in order to pinpoint the execution path. In a complex program, this option can become cumbersome and tedious even if, with the help of some GCC-specific macros, it can be simplified a bit. Consider, for example, a debug macro such as
#define TRACE_MSG fprintf(stderr, __FUNCTION__ \ "() [%s:%d] here I am\n", \ __FILE__, __LINE__)
You can propagate this macro quickly throughout your program by cutting and pasting it. When you do not need it anymore, switch it off simply by defining it to no-op.
A nicer way to get a stack backtrace, however, is to use some of the specific support functions provided by glibc. The key one is backtrace(), which navigates the stack frames from the calling point to the beginning of the program and provides an array of return addresses. You then can map each address to the body of a particular function in your code by having a look at the object file with the nm command. Or, you can do it a simpler way--use backtrace_symbols(). This function transforms a list of return addresses, as returned by backtrace(), into a list of strings, each containing the function name offset within the function and the return address. The list of strings is allocated from your heap space (as if you called malloc()), so you should free() it as soon as you are done with it.
If you prefer to avoid dynamic memory allocation during the backtrace--reasonable, as the backtrace is likely to happen under faulty conditions--you can resort to backtrace_symbols_fd(). This prints the strings directly to the given file descriptor and does not allocate new memory for strings storage. It is a safer choice in those cases where memory heap potentially is corrupted.
In order to convert an address to a function name, the last two functions rely on symbol information to be available inside the program itself. To enable this feature, compile your program with the -rdynamic option (see man dlopen for more details).
Listing 1 demonstrates how to use these functions. The test() function calls either func_low() or func_high(), both of which call show_stackframe() to print out the execution path. The program is compiled with
gcc -rdynamic listing1.c -o listing1
The output should look something like:
Execution path: ./listing1(show_stackframe+0x2e) [0x80486de] ./listing1(func_high+0x11) [0x8048799] ./listing1(test+0x43) [0x80487eb] ./listing1(main+0x13) [0x8048817] /lib/libc.so.6(__libc_start_main+0xbd) [0x4003e17d] ./listing1(backtrace_symbols+0x31) [0x80485f1] First call: 167 Execution path: ./listing1(show_stackframe+0x2e) [0x80486de] ./listing1(func_low+0x11) [0x8048779] ./listing1(test+0x21) [0x80487c9] ./listing1(main+0x33) [0x8048837] /lib/libc.so.6(__libc_start_main+0xbd) [0x4003e17d] ./listing1(backtrace_symbols+0x31) [0x80485f1] Second call: -3
By the way, function prototypes for the backtrace functions reside in the header file execinfo.h.
- William Rothwell and Nick Garner's Certified Ethical Hacker Complete Video Course (Pearson IT Certification)
- Preseeding Full Disk Encryption
- Three EU Industries That Need HPC Now
- FinTech and SAP HANA
- Chemistry on the Desktop
- Two Factors Are Better Than One
- Hodge Podge
- HOSTING Monitoring Insights
- GRUB Boot from ISO
- Minifree Ltd.'s GNU+Linux Computers