Printing function call stack information using backtrace under Linux

Posted by debigmac on Sat, 04 Dec 2021 20:25:00 +0100

Printing function call stack information using backtrace under Linux

Java, Python and other languages have relatively simple methods to print function call stacks. Is there any way to use C language under Linux?
It is said that there are many ways. This article introduces the most basic method, that is, using glibc's backtrace() and backtrace_symbols() and other API
Under Linux, run the man command to view the help document.

man 3 backtrace

The document is not long. The main parts of the following translation are as follows:

First, you need to include the header file and look at the declarations of the three related API s:

#include <exeinfo.h>

int backtrace(void **buffer, int size);

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

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

  1. backtrace
    backtrace has two parameters. The first parameter is actually an array of void * type. In the future, all frame information will be stored in this array; The second parameter size indicates the size of the array. For example, if the array size is 5 and the actual frames have 10 layers, only the latest 5 frames are stored;
    The returned value of backtrace means how many layers of frames are returned. For example, if the size is specified as 10 and the frames have only 5 layers, 5 is returned; If the size is specified as 5 and the frames has 10 layers, 5.0 is returned

  2. backtrace_symbols
    This function is used to resolve the function name represented by the function address in each frame. It can only be used after running the backtrace function above. Why? Because its first parameter is the void * array filled by the backtrace function above. Its second parameter refers to several elements in the buffer array to be parsed.
    There are three points to note when using this function:
    1, It returns a char * array representing the array of function names, but the returned char * * must be free by the caller, and each char * in the char * array cannot be free;
    2, NULL in case of error
    3, In general, the function cannot resolve the function name; Only when the "- rddynamic" option is added during compilation can the function name be resolved at run time in the future.

  3. backtrace_symbols_fd
    Modification function and backtrace_ Similar to symbols, it resolves the function address into the function name. The difference is that it does not return anything, but writes the parsed function name into the third parameter, a file descriptor.

  4. other
    1> Among the three functions, only backtrace() is thread safe
    2> Compiler optimization, such as "- O3" and "- O2", may lead to the loss of call hierarchy (note to the author: an example will be given later)
    3> The inline function does not have a stack frame, so it will not be printed
    4> Tail call optimization will also result in loss of function name printing

An example program is given below:

#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_FRAMES 10

int myfunc1(int);
int myfunc2(int);
int myfunc3(int);

void printCallers()
{
    int layers = 0, i = 0;
    char ** symbols = NULL;
    
    void * frames[MAX_FRAMES];
    memset(frames, 0, sizeof(frames));
    layers = backtrace(frames, MAX_FRAMES);
    for (i=0; i<layers; i++) {
        printf("Layer %d: %p\n", i, frames[i]);
    }
    printf("------------------\n");
    
    symbols = backtrace_symbols(frames, layers);
    if (symbols) {
        for (i=0; i<layers; i++) {
            printf("SYMBOL layer %d: %s\n", i, symbols[i]);
        }
         free(symbols);
    }
    else {
        printf("Failed to parse function names\n");
    }
}


int myfunc1(int a)
{
    int b = a + 5;
    int result = myfunc2(b);
    return result;
}

int myfunc2(int b)
{
    int c = b * 2;
    int result = c + myfunc3(c);
    return result;
}

int myfunc3(int c)
{
    int d = c << 2;
    printCallers();
    d = d/0;
    return d;
}

int main()
{
    int result = 0;
    result = myfunc1(1);
    printf("result = %d\n", result);
    
    return 0;
}

The call stack of this program is:_ start -> _ start_ main -> main -> myfunc1 -> myfunc2 -> myfunc3 -> printCallers
So there are six floors altogether
In addition, after myfunc3 calls printCallers, a sentence divided by 0 is performed, which will cause coredump

First, compile without any optimization:

gcc -rdynamic test.c

The operation results are as follows:

./a.out
Layer 0: 0x556a4408ab5f
Layer 1: 0x556a4408ac79
Layer 2: 0x556a4408ac4c
Layer 3: 0x556a4408ac27
Layer 4: 0x556a4408aca5
Layer 5: 0x7f4ecbabf34a
Layer 6: 0x556a4408aa3a
------------------
SYMBOL layer 0: ./a.out(printCallers+0x45) [0x556a4408ab5f]
SYMBOL layer 1: ./a.out(myfunc3+0x1e) [0x556a4408ac79]
SYMBOL layer 2: ./a.out(myfunc2+0x1d) [0x556a4408ac4c]
SYMBOL layer 3: ./a.out(myfunc1+0x1e) [0x556a4408ac27]
SYMBOL layer 4: ./a.out(main+0x19) [0x556a4408aca5]
SYMBOL layer 5: /lib64/libc.so.6(__libc_start_main+0xea) [0x7f4ecbabf34a]
SYMBOL layer 6: ./a.out(_start+0x2a) [0x556a4408aa3a]
Floating point exception (core dumped)

It can be seen from the above that the function name and the function name of each layer can be printed.
Run gdb, print all BTS, and then compare with the function addresses in the above running results. You will find that basically, the function addresses printed above are in bt.
However, the address of printCallers does not exist. This is because printCallers are running when the above information is printed, and printCallers have finished executing when the divide by 0 statement is executed. Therefore, there will be no stack frame of this function in the core dump file.

Finally, try a little Optimization:

gcc -O3 -rdynamic test.c

The operation results are as follows:

./a.out
Layer 0: 0x5593d28eab18
Layer 1: 0x5593d28ea9cb
Layer 2: 0x7f6e0a67034a
Layer 3: 0x5593d28ea9fa
------------------
SYMBOL layer 0: ./a.out(printCallers+0x38) [0x5593d28eab18]
SYMBOL layer 1: ./a.out(main+0xb) [0x5593d28ea9cb]
SYMBOL layer 2: /lib64/libc.so.6(__libc_start_main+0xea) [0x7f6e0a67034a]
SYMBOL layer 3: ./a.out(_start+0x2a) [0x5593d28ea9fa]
Illegal instruction (core dumped)

It can be seen from the above that after the compiler is optimized, the function call stack in the actual operation may be a few layers less than originally thought, which is more simple.

Finally, if you want to print the call stack information in C language under Linux, in addition to the methods described in this article, you can also use libunwind. It is not within the scope of this article.
In addition, if it is not C but C + + program, it needs demangling, which will be a little troublesome.

(end)

Topics: C Linux