Simple implementation of exception stack trace in Linux system

  • 2020-05-15 03:28:03
  • OfStack

Doing C/C++ development in Linux often results in unexpected problems that cause the program to crash without leaving any trace of the code running after the crash, so the stack-trace technique is extremely important. This article will briefly introduce the C/C++ program runtime stack acquisition in Linux, starting with the backtrace series of functions -- the use range is suitable for cases where GDB is not installed or if you want to quickly clarify the sequence of function calls. The header file is execinfo.h

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

This function is used to get the call stack of the current thread, and the information obtained will be stored in buffer, which is an array of Pointers. The parameter size is used to specify how many void* elements can be saved in buffer. The return value of the function is the number of Pointers actually obtained, and the maximum size of the pointer in size is the return address actually obtained from the stack, and each stack frame has one return address. Note that some compiler optimizations interfere with getting the correct call stack, and inline functions do not have a stack frame. Removing the frame pointer also makes it impossible to properly parse the stack contents.

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

This function converts the information obtained from the backtrace function into an array of strings. The parameter buffer is a pointer to an array obtained from the backtrace function. size is the number of elements in the array (the return value of backtrace). The return value of the function is a pointer to an array of strings of the same size as buffer. Each string contains one printable message relative to the corresponding element in buffer. It includes the function name, the offset address of the function, and the actual return address. The strings generated by backtrace_symbols are all generated by malloc, but not the last one and one free, because backtrace_symbols will release malloc out of a block of memory once according to the callstack layer number given by backtrace, so only OK needs to return the pointer at the end of free.

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

This function has the same functionality as the backtrace_symbols function, except that instead of returning an array of strings to the caller, it writes the result to a file with the file descriptor fd, one line for each function. It does not need to call the malloc function, so it is suitable for situations where it is possible to call the function and fail.


In the C++ program, we also need to focus on 1 function:


/**
*  Is used to backtrace_symbols The string returned by the function is parsed into the corresponding function name for easy understanding 
*  The header file   cxxabi.h
*  Name space 	abi
* @param mangled_name A NUL-terminated character string containing the name to be demangled.
* @param output_buffer  A region of memory, allocated with malloc, of *length bytes, into which the demangled name is stored. If output_buffer is not long enough, it is expanded using realloc. 
*     output_buffer may instead be NULL; in that case, the demangled name is placed in a region of memory allocated with malloc. 
* @param length  If length is non-NULL, the length of the buffer containing the demangled name is placed in *length.
* @param status  *status is set to one of the following values:
*        0: The demangling operation succeeded.
*       -1: A memory allocation failiure occurred.
*       -2: Mangled_name is not a valid name under the C++ ABI mangling rules.
*       -3: One of the arguments is invalid.
*/
char *__cxa_demangle (const char *mangled_name, char *output_buffer, size_t *length, int *status);

Step by step, how to use these functions to get the stack of a program

1. The code of version 1 is as follows


#define MAX_FRAMES 100
void GetStackTrace (std::string* stack)
{
  void* addresses[MAX_FRAMES];
  int size = backtrace (addresses, MAX_FRAMES);
  std::unique_ptr<char*, void(*)(void*)> symbols {
    backtrace_symbols (addresses, size),
    std::free
  };
  for (int i = 0; i < size; ++i) {
    stack->append (symbols.get()[i]);
    stack->append ("\n");
  }
}

void TestFunc (std::string& stack, int value)
{
  while (--value);
  GetStackTrace (&stack);
}
int main(int argc, char* argv[])
{
  std::string stack;
  TestFunc (stack, 5);
  std::cout << stack << std::endl;
  return 0;
}

After compiling into an executable file StackTrace, the output is as follows:

./StackTrace(_Z13GetStackTracePSs+0x27) [0x4035d5]
./StackTrace(_Z8TestFuncRSsi+0x2a) [0x4036e6]
./StackTrace(main+0x2d) [0x403715]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f7302027de5]
./StackTrace() [0x403139]

You can tell from the output how the program was called, but it seems a little hard to understand. Let's change the GetStackTrace function a little bit.

2. Advanced version of the code, changing the code in point 1


void DemangleSymbol (std::string* symbol)
{
  size_t size = 0;
  int status = -4;
  char temp[256] = {'\0'};
  //first, try to demangle a c++ name
  if (1 == sscanf (symbol->c_str (), "%*[^(]%*[^_]%[^)+]", temp)) {
    std::unique_ptr<char, void(*)(void*)> demangled {
      abi::__cxa_demangle (temp, NULL, &size, &status),
      std::free
    };
    if (demangled.get ()) {
      symbol->clear ();
      symbol->append (demangled.get ());
      return;
    }
  }
  //if that didn't work, try to get a regular c symbol
  if (1 == sscanf(symbol->c_str (), "%255s", temp)) {
    symbol->clear ();
    symbol->append (temp);
  }
}

void GetStackTrace (std::string* stack)
{
  void* addresses[MAX_FRAMES];
  int size = backtrace (addresses, MAX_FRAMES);
  std::unique_ptr<char*, void(*)(void*)> symbols {
    backtrace_symbols (addresses, size),
    std::free
  };
  for (int i = 0; i < size; ++i) {
    std::string demangled (symbols.get()[i]);
    DemangleSymbol (&demangled);
    stack->append (demangled);
    stack->append ("\n");
  }
}

This version USES s 86en_demangle to parse each string returned by backtrace_symbols into a comprehensible string. Since s 90en_demangle can only parse the string _Z13GetStackTracePSs, sscanf is used to simply intercept the data returned by the backtrace_symbols function. However, sscanf is no longer recommended. After compiling into an executable file StackTrace, the output is as follows:

GetStackTrace(std::string*)
TestFunc(std::string&, int)
./StackTrace(main+0x2d)
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5)
./StackTrace()

From the output, you can see how the program was called, but some other information is missing. Let's change the DemangleSymbol function

3. Import the code and change it based on the code in point 1 and point 2


// The prefix used for mangled symbols, per the Itanium C++ ABI:
// http://www.codesourcery.com/cxx-abi/abi.html#mangling
const char kMangledSymbolPrefix[] = "_Z";
// Characters that can be used for symbols, generated by Ruby:
// (('a'..'z').to_a+('A'..'Z').to_a+('0'..'9').to_a + ['_']).join
const char kSymbolCharacters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
// Demangles C++ symbols in the given text. Example:
// "out/Debug/base_unittests(_ZN10StackTraceC1Ev+0x20) [0x817778c]"
// =>
// "out/Debug/base_unittests(StackTrace::StackTrace()+0x20) [0x817778c]"
void DemangleSymbol (std::string* symbol)
{
  std::string::size_type search_from = 0;
  while (search_from < symbol->size ()) {
    // Look for the start of a mangled symbol from search_from
    std::string::size_type mangled_start = symbol->find (kMangledSymbolPrefix, search_from);
    if (mangled_start == std::string::npos) {
      break; // Mangled symbol not found
    }
    // Look for the end of the mangled symbol
    std::string::size_type mangled_end = symbol->find_first_not_of (kSymbolCharacters, mangled_start);
    if (mangled_end == std::string::npos) {
      mangled_end = symbol->size ();
    }
    std::string mangled_symbol = std::move (symbol->substr (mangled_start, mangled_end - mangled_start));
    // Try to demangle the mangled symbol candidate
    int status = -4; // some arbitrary value to eliminate the compiler warning
    std::unique_ptr<char, void(*)(void*)> demangled_symbol {
      abi::__cxa_demangle (mangled_symbol.c_str (), nullptr, 0, &status),
      std::free
    };
    // 0 Demangling is success
    if (0 == status) {
      // Remove the mangled symbol
      symbol->erase (mangled_start, mangled_end - mangled_start);
      // Insert the demangled symbol
      symbol->insert (mangled_start, demangled_symbol.get ());
      // Next time, we will start right after the demangled symbol
      search_from = mangled_start + strlen (demangled_symbol.get ());
    }
    else {
      // Failed to demangle. Retry after the "_Z" we just found
      search_from = mangled_start + 2;
    }
  }
}

The DemangleSymbol function of this version is slightly changed from the DemangleSymbol function of version 2. This version mainly finds a string like _Z13GetStackTracePSs to parse the function of _Z13GetStackTracePSs. Finally, the parsed content is replaced with the original content.

./StackTrace(GetStackTrace(std::string*)+0x27) [0x403720]
./StackTrace(TestFunc(std::string&, int)+0x2a) [0x4038c0]
./StackTrace(main+0x2d) [0x4038ef]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7fb9d560bde5]
./StackTrace() [0x403279]

The above output gives us a lot of information for debugging code, but it still leaves out some auxiliary information, such as the file name, the line of code in which the function is located, and the process or thread number (which is important in multiple lines). You can refer to the open source project libunwind or google-coredumper for more information.


Related articles: