DISCLAMER
The article is comic, but with some truth (programming, the same). This article also contains a code that can be fatal to your vision. Read at your risk.
Introduction
Hello. I think many are faced with the lack of information of the majority of critical errors flying in the program. Let's list what situations can lead to the crash of the program:
An exception
Exceptions are a very powerful system for handling exceptional situations that arise in a program. But if the exception was not processed, then it drops the program through std :: terminate. Therefore, in well-written programs, the exception that was not handled often means a bug in the program that needs to be fixed.
This type of error is the most informative, since the exception method what () is output to stderr automatically when the program crashes.
')
Assert
Disabled method for checking the correct use of functions. Disabling is provided as a tool to increase performance in functions in which every nanosecond is counted. If the program has fallen on the assert, then the programmer has nakosyachil somewhere when using the interface of a module. But it is quite possible that he simply did not foresee any critical values and for this reason he left the conditions of the assert.
This type of error is not the most informative, but when it falls, it displays a condition that was violated.
SIGSEGV
You, as a professional in your own business, deposed a null pointer and happily wrote some value into it. The program is not particularly resisting fell.
Such a fall is not accompanied by any messages and is probably the most non-informative and presented, but it is and cannot be ruled out in any way.
All kinds of errors, regardless of their informativeness, do not really help determine for what reason it appeared. In the framework of this article, I will try to show that I got in a rush to get at least some kind of stack trace during error trapping.
We look around
First you need to understand how to track function calls in general. Google gave extremely disappointing results. Obviously, there is no cross-platform solution. Under Linux and Mac OS there is a header file execinfo.h with which you can get a linked list of the call stack. Under Windows, there is a WinAPI CaptureStackBackTrace function that allows you to walk along the stack and receive calls from frames. But we will go through C ++. We will not use platform-specific functions.
The data will be stored in a normal stack. For pushing and pushing functions, we will use the object that will be created during the function call. The advantages of this approach are that even if an exception is thrown, this object will be deleted.
And what exactly do we need data? Well for beauty, of course, it would not be bad to have a file, a string, and a function name. It would also be a good idea to have the arguments of this function, so that you can concretize the called function during an overload.
But which interface to use? How to write more or less beautiful code and at the same time get the required functionality.
The only solution that I could find was macros (maybe it’s also possible to somehow implement it through templates, but I know the surface very superficially and therefore I do as I can).
Implementation
To begin with, we implement a singleton that will be used to work with the stack. As the user interface, we implement only the method for getting the string representation of the trace trace.
class StackTracer { friend class CallHolder; public: static StackTracer& i() { static StackTracer s; return s; } std::string getStackTrace() const { std::stringstream ss; for (auto iterator = m_data.begin(), end = m_data.end(); iterator != end; ++iterator) ss << iterator->file << ':' << iterator->line << " -> " << iterator->name << std::endl; return ss.str(); } private: void push(const std::string &name, const char *file, int line) { m_data.push_front({name, file, line}); } void pop() { m_data.pop_front(); } struct CallData { std::string name; const char *file; int line; }; StackTracer() : m_data() {} std::list<CallData> m_data; };
There is no way to use std :: stack, because in order to get all the elements for output, you would have to copy the entire container.
Of the problems of this class is complete streaming insecurity. But we will deal with this later, and now PoC.
Now we will implement a class that will register and delete a function call.
class CallHolder { public: CallHolder(const std::string &name, const char *file, int line) { StackTracer::i().push(name, file, line); } ~CallHolder() { StackTracer::i().pop(); } };
Pretty nontrivial code right? Again, this “registrar” does not take into account multithreading.
Now we will try to throw a small example in order to check the performance of such a Frankenstein.
void func1(); void func2() { CallHolder __f("func2()", __FILE__, __LINE__); func1(); } void func1() { CallHolder __f("func1()", __FILE__, __LINE__); static int i = 1; if (i-- == 1) func2(); else std::cout << StackTracer::i().getStackTrace() << std::endl; } int main() { func1(); return 0; }
Result:
Figure 3.1 - “It is alive !!”Fine! But it is necessary to somehow pack a call to CallHolder, otherwise it’s not beautiful to somehow get the pens to call and prescribe the name of the method twice.
For the implementations of functions and methods, this is the macro:
#define MEM_IMPL(func_name, args)\ func_name args\ {\ CallHolder __f("" #func_name #args "", __FILE__, __LINE__);
Now our Frankenstein can be modified and get something like this. Already more like the "normal" code:
void func1(); void MEM_IMPL(func2, ()) func1(); } void MEM_IMPL(func1, ()) static int i = 1; if (i-- == 1) func2(); else std::cout << StackTracer::i().getStackTrace() << std::endl; } int main() { func1(); return 0; }
The result of the execution is exactly the same as before. But there is a clear problem with this approach. The opening brace that the macro hides disappears. This makes it difficult to read the code. Although people who adhere to the ideology with the opening brace in the string with the title will not consider it a strong minus. A stronger disadvantage is that the development environment, which I use, does not know how to work with such quirky cases and considers only braces outside of macros.
But we digress from our orgy. What to do if we have a class? Well, if the implementation is outside the class - then nothing. Example:
void func1(); void MEM_IMPL(func2, ()) func1(); } void MEM_IMPL(func1, ()) static int i = 1; if (i-- == 1) func2(); else std::cout << StackTracer::i().getStackTrace() << std::endl; } class EpicClass { public: void someFunc(); }; void MEM_IMPL(EpicClass::someFunc, ()) func1(); } int main() { EpicClass a; a.someFunc(); return 0; }
Result:
Figure 3.2 - Output from the classAnd what if you write the implementation directly in the class declaration? Then another macro is required:
#define CLASS_IMPL(class_name, func_name, args)\ func_name args\ {\ CallHolder __f("" #class_name "::" #func_name "", __FILE__, __LINE__);
But this approach has a problem. It is necessary to separately indicate the name of the class, which is not very good. This can be skipped if we use C ++ 11. I use the solution found on stack overflow. This is type_name <decltype (i)> (). Where type_name is
#include <type_traits> #include <typeinfo> #ifndef _MSC_VER # include <cxxabi.h> #endif #include <memory> #include <string> #include <cstdlib> template <class T> std::string type_name() { typedef typename std::remove_reference<T>::type TR; std::unique_ptr<char, void(*)(void*)> own ( #ifndef _MSC_VER abi::__cxa_demangle(typeid(TR).name(), nullptr, nullptr, nullptr), #else nullptr, #endif std::free ); std::string r = own != nullptr ? own.get() : typeid(TR).name(); // if (std::is_const<TR>::value) // r += " const"; // if (std::is_volatile<TR>::value) // r += " volatile"; // if (std::is_lvalue_reference<T>::value) // r += "&"; // else if (std::is_rvalue_reference<T>::value) // r += "&&"; return r; }
The part with modifiers is commented out for the reason that the result of processing (* this) will then at the end have a reference sign — an ampersand (&).
A smart macro looks like this:
#define CLASS_IMPL(func_name, args)\ func_name args\ {\ CallHolder __f(type_name<decltype(*this)>() + "::" + #func_name + #args, __FILE__, __LINE__);
Let's edit our franc and look at the result:
void func1(); void MEM_IMPL(func2, ()) func1(); } void MEM_IMPL(func1, ()) static int i = 1; if (i-- == 1) func2(); else std::cout << StackTracer::i().getStackTrace() << std::endl; } class EpicClass { public: void someFunc(); void CLASS_IMPL(insideFunc, ()) func1(); } }; void MEM_IMPL(EpicClass::someFunc, ()) func1(); } int main() { EpicClass a;
Result:
Figure 3.3 - The class method declared internallyWell, but what about the informative? How can you get at least some useful information in the fall. After all, now with the occurrence of the same Seg Fault, everything will just fall. Well, first of all, we implement our int main, which will catch errors. In the title we announce:
int safe_main(int argc, char *argv[]);
In cpp, we implement our “safe” main, which will already call safe_main.
void signal_handler(int signum) { std::cerr << "Death signal has been taken. Stack trace:" << std::endl << StackTracer::i().getStackTrace() << std::endl; signal(signum, SIG_DFL); exit(3); } int MEM_IMPL(main, (int argc, char * argv[])) signal(SIGSEGV, signal_handler); signal(SIGTERM, signal_handler); signal(SIGABRT, signal_handler); return safe_main(argc, argv); }
I think it is worth explaining. With the signal function, we set a handler that will be called when SIGSEGV, SIGTERM and SIGABRT signals appear. In which will already be displayed in stderr stack trace. (The latter is required for assert).
Let's try to break the program SIGSEGV. Again we change our "test stand":
void func1(); void MEM_IMPL(func2, ()) func1(); } void MEM_IMPL(func1, ()) static int i = 1; if (i-- == 1) func2(); else { int *i = nullptr; (*i) = 12; } } class EpicClass { public: void someFunc(); void CLASS_IMPL(insideFunc, ()) func1(); } }; void MEM_IMPL(EpicClass::someFunc, ()) func1(); } int MEM_IMPL(safe_main, (int argc, char *argv[])) EpicClass a;
Result:
Figure 3.4 - Work safe mainBut what about the exceptions? After all, if you throw an exception, it will simply destroy all the existing CallHolder and in the stack trace we will not get anything thorough. To do this, we create our own THROW macro, which would receive the stack trace when an exception was thrown:
#define THROW(exception, explanation)\ throw exception(explanation + std::string("\n\rStack trace:\n\r") + StackTracer::i().getStackTrace());
We also modify our test bench a bit:
void func1(); void MEM_IMPL(func2, ()) func1(); } void MEM_IMPL(func1, ()) static int i = 1; if (i-- == 1) func2(); else {
And we get the result:
Figure 3.5 - THROW does not forgiveGood. We have achieved full basic functionality, but what about multithreading? Will we do something with her?
Well, at least try!
To begin, edit StackTracer so that it starts working with different streams:
class StackTracer { friend class CallHolder; public: static StackTracer& i() { static StackTracer s; return s; } std::string getStackTrace() const { std::stringstream ss; std::lock_guard<std::mutex> guard(m_readMutex); for (auto mapIterator = m_data.begin(), mapEnd = m_data.end(); mapIterator != mapEnd; ++mapIterator) { ss << "Thread: 0x" << std::hex << mapIterator->first << std::dec << std::endl; for (auto listIterator = mapIterator->second.begin(), listEnd = mapIterator->second.end(); listIterator != listEnd; ++listIterator) ss << listIterator->file << ':' << listIterator->line << " -> " << listIterator->name << std::endl; ss << std::endl; } return ss.str(); } private: void push(const std::string &name, const char *file, int line, std::thread::id thread_id) { m_data[thread_id].push_front({name, file, line}); } void pop(std::thread::id thread_id) { m_data[thread_id].pop_front(); } struct CallData { std::string name; const char *file; int line; }; StackTracer() : m_data() {} mutable std::mutex m_readMutex; std::map<std::thread::id, std::list<CallData> > m_data; };
Similarly, we change the CallHolder so that the thread_id is passed to it:
class CallHolder { public: CallHolder(const std::string &name, const char *file, int line, std::thread::id thread_id) { StackTracer::i().push(name, file, line, thread_id); m_id = thread_id; } ~CallHolder() { StackTracer::i().pop(m_id); } private: std::thread::id m_id; };
Well, we modify some macros:
#define CLASS_IMPL(func_name, args)\ func_name args\ {\ CallHolder __f(type_name<decltype(*this)>() + "::" + #func_name + #args, __FILE__, __LINE__, std::this_thread::get_id()); #define MEM_IMPL(func_name, args)\ func_name args\ {\ CallHolder __f("" #func_name #args "", __FILE__, __LINE__, std::this_thread::get_id());
We are testing. Let's prepare such a “stand”:
void MEM_IMPL(sleepy, ()) std::this_thread::sleep_for(std::chrono::seconds(3)); THROW(std::runtime_error, "Thread exception"); } void MEM_IMPL(thread_func, ()) sleepy(); } int MEM_IMPL(safe_main, (int argc, char *argv[])) std::thread th(&thread_func); th.detach(); std::this_thread::sleep_for(std::chrono::seconds(20)); return 0; }
And try to run:
Figure 3.6 - Death occurred at 1:10 Moscow timeSo we got a multithreaded stack trace. The experiment is over, the test subject is dead. From the obvious problems of this implementation
- We cannot receive calls from libraries not written by us;
- Additional overhead for each function call.
Conclusion
Unfortunately, without a serious compiler support, it is extremely difficult to implement the debug stack trace and you have to use crutches. But in any case, thanks for reading this article.