The whole process of linux C++ crash stack capture

1. Crash signal processing

When a program crashes, the operating system sends a signal to the program to notify it that an exception has occurred. In C++, you can register a signal handler through the signal function so that the program can execute custom code when it receives the signal.

#include <execinfo.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void signal_handler(int sig) {
    // print out all the info to a log file
    _exit(1); // exit the program
}

int main() {
    // register signal handler
    signal(SIGSEGV, signal_handler);

    // your code here

    return 0;
}

This callback function solves the problem of whether the crash can be observed.

Next is the way to obtain crash information.

2. Print crash information

The general way to view the runtime stack of a function is to use an external debugger such as GDB. However, in the actual operating environment, it is impossible for us to keep the program running in the debugging state. Therefore, it is very useful to print out the call stack of a function when an error occurs during normal operation. The backtrace function provides us with a good interface.

Three functions are declared in the header file “execinfo.h” to obtain the function call stack of the current thread.

int backtrace(void **buffer, int size)

This function is used to obtain the call stack of the current thread. The obtained information will be stored in the buffer, which is a list of pointers. The parameter size is used to specify how many void* elements can be saved in the buffer. The function return value is the actual number of pointers obtained, and the maximum does not exceed size. The pointers in the buffer are actually the return addresses obtained from the stack. Each stack frame has a return address. Please note that some compilers Optimization optionsinterfere with obtaining the correct call stack. In addition,inline functions do not have a stack frame; deleting the frame pointer will also prevent the stack content from being correctly parsed.

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

backtrace_symbols converts the information obtained from the backtrace function into a string array. The parameter buffer should be the array pointer obtained from the backtrace function, and size is the number of elements in the array (the return value of backtrace). The function return value is a pointer to a string array, its size is the same as the buffer. Each string contains a printable message relative to the corresponding element in the buffer. It includes the function name, the function’s offset address, and the actual return address.

The return value of this function is the space requested through the malloc function, so calling this must use the free function to release the pointer.
Note: The return value of the function will be NULL if it cannot obtain enough space for the string.

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

backtrace_symbols_fd has the same function as the backtrace_symbols function, except that it does not return a string array to the caller, but writes the results to a file with file descriptor fd, with one line corresponding to each function. It does not require calling the malloc function, so it is suitable for situations where calling this function may fail.

Here is an example of using backtrace to catch an exception and print the function call stack:

#include <execinfo.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void signal_handler(int sig) {
    void *array[10];
    size_t size;

    // get void*'s for all entries on the stack
    size = backtrace(array, 10);

    // print out all the frames to a log file
    FILE* fp = fopen("crash.log", "w");
    if (fp != NULL) {
        fprintf(fp, "Error: signal %d\\
", sig);
        backtrace_symbols_fd(array, size, fileno(fp));
        fclose(fp);
    }

    _exit(1); // exit the program
}

int main() {
    // register signal handler
    signal(SIGSEGV, signal_handler);

    // your code here

    return 0;
}

Crash stack:

  • Function call address: Indicates the location of the function in the program memory.
  • Function name and parameters: Indicates the name and parameter list of the function. If the function name cannot be obtained, it is displayed as “?”.
  • File name and line number: Indicates the location of the function in the source code. If the file name and line number cannot be obtained, it is displayed as “(unknown)”.
Error: signal 11
[0] ./myprogram( + 0x1139) [0x563a21a8f139]
[1] ./myprogram( + 0x1298) [0x563a21a8f298]
[2] /lib/x86_64-linux-gnu/libc.so.6(+0x3ef20) [0x7f9c1d069f20]
[3] ./myprogram( + 0xe75) [0x563a21a8ee75]
[4] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main + 0xf3) [0x7f9c1cf9e0b3]
[5] ./myprogram( + 0x992) [0x563a21a8e992]

The symbol table (Symbol Table) is a database that stores program function names, variable names and other symbolic information. It usually contains information such as function name, starting address of the function, function size, file name, line number and other information.

Under Linux, you can use the addr2line command provided by GNU Binutils to query the symbol table to convert function addresses and line numbers into specific locations in the source code.

The following is a sample code that uses the addr2line command to query the symbol table and output the corresponding code location:

#include <execinfo.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void signal_handler(int sig) {
    void *array[10];
    size_t size;

    // get void*'s for all entries on the stack
    size = backtrace(array, 10);

    // print out all the frames to a log file
    FILE* fp = fopen("crash.log", "w");
    if (fp != NULL) {
        fprintf(fp, "Error: signal %d\\
", sig);
        for (int i = 0; i < size; i + + ) {
            fprintf(fp, "[%d] ", i);
            if (array[i] != NULL) {
                char addr2line_cmd[256];
                sprintf(addr2line_cmd, "addr2line -f -e ./myprogram %p", array[i]);
                FILE* addr2line_fp = popen(addr2line_cmd, "r");
                if (addr2line_fp != NULL) {
                    char line[256];
                    while (fgets(line, sizeof(line), addr2line_fp) != NULL) {
                        fprintf(fp, "%s", line);
                    }
                    pclose(addr2line_fp);
                }
            } else {
                fprintf(fp, "(unknown)\\
");
            }
        }
        fclose(fp);
    }

    _exit(1); // exit the program
}

int main() {
    // register signal handler
    signal(SIGSEGV, signal_handler);

    // your code here

    return 0;
}

After conversion via addr2line:

Error: signal 11
[0] ./myprogram(foo + 0x1d) [0x563a21a8f139]
    at example.cpp:27
[1] ./myprogram(bar + 0x18) [0x563a21a8f298]
    at example.cpp:36
[2] /lib/x86_64-linux-gnu/libc.so.6(__GI___libc_read + 0x10) [0x7f9c1d069f20]
    at ../sysdeps/unix/syscall-template.S:185
[3] ./myprogram(main + 0x25) [0x563a21a8ee75]
    at example.cpp:45
[4] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main + 0xf3) [0x7f9c1cf9e0b3]
    at ../csu/libc-start.c:308
[5] ./myprogram(_start + 0x2a) [0x563a21a8e992]
    at ?:0

refer to:
C++ captures the entire process of exception crash_c++ captures all exceptions-CSDN Blog