Title:vc++ crash processing and printing call stacks
Tags: [VC + +, structured exception handling, crash logging]
Date:2018-08-28 20:59:54
Categories:windows Advanced Programming
keywords:vc++, structured exception handling SEH, crash logging
---
We are always faced with a crash after the release of the program, this time is generally difficult to reproduce or difficult to locate the location of the program crashes, before the method of the program crashes when the log dump file and then through the WinDbg to analyze. That approach has a high demand for developers, and it requires programmers to understand a series of concepts such as memory, registers, and so on, and manually load the corresponding symbol table. Java, Python, and so on when the language crashes will print an unusual stack of information and tell the user that the error, according to this information programmers can easily find the corresponding code location and processing, and C + + will pop up a box to tell the user program crashes, in contrast, C + + Seems to be too unfriendly to the user, and according to its frame is difficult to find the corresponding problem, then it is possible to make C + + like Java to print the exception stack it? This nature is possible, this article is to discuss how to implement similar functions on Windows
Exception handling
In general, when an exception occurs, the user code stops executing, and the control of the CPU is transferred to the operating system, the operating system receives control, the environment of the current thread is saved to the struct context, and then the handler function for this exception is found. The system uses the structure Exception_record to preserve the exception description information, which together with the context constitutes a structure exception_pointers, which is often used in exception handling.
The exception information Exception_record is defined as follows:
typedef struct _EXCEPTION_RECORD{ DWORD ExceptionCode; //异常码 DWORD ExceptionFlags; //标志异常是否继续,标志异常处理完成后是否接着之前有问题的代码 struct _EXCEPTION_RECORD* ExceptionRecord; //指向下一个异常节点的指针,这是一个链表结构 PVOID ExceptionAddress; //异常发生的地址 DWORD NumberParameters; //异常附加信息 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //异常的字符串} EXCEPTION_RECORD, *PEXCEPTION_RECORD;
The Windows platform provides this set of exception handling mechanisms that we call structured exception handling (SEH), which is typically handled as follows:
- If the program is debugged (for example, we debug the run program in the VS compiler), when the exception occurs, the system first gives the exception information to the debugger, and if the debugger processes the program to continue running, the system looks for possible processing code in the thread stack where the exception occurred. Handle the exception if found and continue running the program
- If the online stacks is not found, then notify the debugger again, if this time still can not handle the exception, then the operating system will be the exception process default processing, this time is usually directly pop up an error dialog box and then terminate the program.
The system maintains an SEH table in the stack environment of each thread, which is the type of exception that the user registers and its corresponding handler, and whenever the user registers a new exception handler in the function, the information is stored in the head of the list, that is, it is inserted into a new handler using the head plug capable. From this point of view, we can easily understand why in general high-level language is generally found with the try block the nearest catch block, and then find its upper catch, from inside to outside to find. The catch that is closest to the try block is last registered, because the head interpolation method is used, and it is naturally processed first.
In Windows, for exception handling, extended __try
and two operators, both of __except
which are very similar to try and catch in C + +, are basically similar, and its general syntax structure is as follows:
__try{ //do something}__except(filter){ //handle}
__try
when used and __except
it is divided into 3 parts, namely: Protection code body, filter expression, exception processing block
- The protection code body is generally the statement in try, it values the protected code, that is, we want to handle the exception generated by the code block
- The filter expression is the value in the except, which can only be one of the 3 values, and Exception_continue_search continues to look down for exception handling, which means that the exception handling block here does not handle the exception, Exception_continue_ Execution indicates that the exception has been handled, this time can continue to execute the line to produce the exception code, exception_execute_handler that the exception has been processed, this time jump directly into the except inside the code block, In this way, its execution process is similar to the normal process of exception handling.
- The exception handling block, which refers to the code block in the except below the extension number.
Note: We say that the filter expression can only be one of these three values, but it does not say that you have to fill in these three values, it also supports functions or other expression types, as long as the return value of the function or expression is one of these three values.
The above approach also has his limitations, that is, it can only protect the code we specify, if the __try
code outside the block crashes, may still cause the program to be killed, and each location need to write the code is too much trouble. In fact, there is a way to deal with exceptions, that is, SetUnhandledExceptionFilter
to register a global exception handler to handle all unhandled exceptions, in fact, its main work is to add a handler function to the header of the exception handling, the prototype of the function is as follows:
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(__in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter);
It needs to pass in a function so that when an exception occurs, the function is called, and the prototype of the callback function is as follows:
LONG WINAPI UnhandledExceptionFilter( __in struct _EXCEPTION_POINTERS* ExceptionInfo);
The callback function passes in a pointer to the struct that represents the current stack and exception information, and for the specifics of the structure, refer to MSDN, where the function returns a Long value that is one of the above 3 values, indicating how the user code continues to execute after the system calls the exception handler to handle the exception.
SetUnhandledExceptionFilter
The function returns a function pointer that points to the head of the list, and if the insert handler fails then it points to the original linked header, otherwise it points to the new linked header (that is, the address of the registered callback function)
This is the way to implement the ability to print exception information and call stacks.
Print function Call stack
About the contents of the print stack, there is no more to say, please refer to the blog I wrote before
Windows platform Call function stack tracking method
The main idea here is to use Stackwalker to get the corresponding function information based on the current stack environment, which needs to be generated from the symbol table, so we need to load the symbol table first and get the environment of the current thread. We can use GetThreadContext to get it as I wrote in my blog, but it's much simpler in the exception, remember the prototype of the exception handling function? The exception handler itself brings in a pointer to the EXCEPTION_POINTERS structure, which contains information about the exception stack.
There are some problems to be aware of, I put it to the realization of that piece, please look down carefully ^_^
Realize
Realization part of the source code I put on GitHub, address
This project is divided into two classes cbaseexception, mainly a simple encapsulation of exceptions, providing some of the features we need, such as getting information about the loaded module, getting the stack of calls, and parsing the information when an exception occurs. And these are all based on Cstackwalker.
In use, I have defined most of the functions in Cbaseexception as virtual allow for rewriting. Because I'm not sure what kind of extensions this piece of follow up will need. But the main function is the outputstring function, this function is used for information output, the default cbaseexception is to output information to the console, and subsequently can be overloaded with this function to output data to the log.
Cbaseexception class
Cbaseexception is mainly used to deal with exceptions, in the code I provide two ways to do exception handling, the first is SetUnhandledExceptionFilter
to register a global processing function, the function is a static function in the class UnhandledExceptionFilter, In this function I initialize a cbaseexception class based on the exception's stack environment, and then simply call the class's method to display the information about the exception and the stack. The second is _set_se_translator
to register a way to convert Seh to C + + exception, in the corresponding callback I simply throw a cbaseexception exception, in the specific code as long as the simple use of C + + exception handling to catch such an exception can be
The Cbaseexception class is mainly used to parse the information of the exception, which provides functions such as the main 3
- Showexceptionresoult: This function is mainly based on the exception code to obtain the specific string information of the exception, such as illegal memory access, except 0 exceptions and so on
- Getlogicaladdress: To obtain the corresponding module information according to the address of the code where the exception occurred, for example, it belongs to the section in the PE file, the address range of the section, etc., it first uses virtualquery to obtain the corresponding virtual memory information in the implementation. Mainly this module's first address information, and then parse the PE file to get the section table information, we follow the link table of each item, according to the address range in the section table to judge it belongs to the section, note here we calculate its offset in the PE file based on its offset in memory, Please refer to the relevant contents of PE file for the specific calculation method.
3.ShowRegistorInformation: Get the value of each register, this value is stored in the context structure, we just need to print it simply.
Cstackwalker class
This class mainly implements some basic functions, it mainly provides the initialization symbol table environment, obtains the corresponding call stack information, obtains the loaded module information
When initializing the symbol table, it is possible to iterate through the positions of several common symbol tables and load the symbol tables in these locations to get a better picture of the stack call. After getting to the corresponding symbol table location, there is this code
if (NULL != m_lpszSymbolPath){ m_bSymbolLoaded = SymInitialize(m_hProcess, T2A(m_lpszSymbolPath), TRUE); //这里设置为TRUE,让它在初始化符号表的同时加载符号表}DWORD symOptions = SymGetOptions();symOptions |= SYMOPT_LOAD_LINES;symOptions |= SYMOPT_FAIL_CRITICAL_ERRORS;symOptions |= SYMOPT_DEBUG;SymSetOptions(symOptions);return m_bSymbolLoaded;
Here the last function of the syminitialize is set to true, this parameter means whether to enumerate the loaded module and load the corresponding symbol table, directly at the beginning of the loading may be a waste of memory, this time we can use the dynamic loading mode, Fill in False when initializing, then enumerate all the modules yourself when needed, then manually load all the module's symbol table, manual loading needs to call Symloadmoduleex. Here we need to remind you that if this is false, then you must load the module's symbol table, otherwise you will get a bunch of 487 errors (i.e. invalid address) when you call SymGetSymFromAddr64 later.
I was the problem that bothered me for a long time.
There are two main ways to get information about a module, one is to use the CreateToolhelp32Snapshot function to get a snapshot of the module information in the process and then call Module32next and Module32first to enumerate the module information. Another way is to use EnumProcessModules to get the handle of all the modules, then get the information of the module according to the handle, and of course there are other ways to refer to my blog to enumerate the modules in the process.
The function is called for each module while enumerating the loaded modules, which GetModuleInformation
has two functions, gets the version number of the module file and gets the loaded symbol table information.
The next is the play--Get the call stack. Getting the call stack first gets the current environment, which is determined in the code, and if the current incoming context is NULL, the function gets the current stack information itself. When the stack information is obtained first determine whether the current thread, if not so for the result is accurate, you need to stop the target thread, and then get, or directly use the macro to get, the corresponding macro definition is as follows:
#define GET_CURRENT_THREAD_CONTEXT(c, contextFlags) do { memset(&c, 0, sizeof(CONTEXT)); c.ContextFlags = contextFlags; __asm call $+5 __asm pop eax __asm mov c.Eip, eax __asm mov c.Ebp, ebp __asm mov c.Esp, esp} while (0)
You only need to pay attention to the information of ESP EBP EIP when calling Stackwalker, so here we can simply get the environment of these registers, and the rest of them. The problem with this is that we are getting this threading environment in a function in the Cstackwalker class, and this environment will contain cstackwalker::stackwalker, The result is naturally different from what we want (what we want is to hide the information in this library and keep only the caller's relevant stack information). I don't have any good solutions to this problem.
After getting to the threading environment, it is simple to call stackwalker and the function of the heap sym to get all kinds of information, which is no longer explained in detail.
At this point the function has been realized almost. For the specific use of the library, please refer to main.cpp This document, I believe this blog post and source code you should be able to use it easily.
It is said that these functions are not multithreaded security, I am not in the multi-threaded environment to test, so specific it in the multi-threaded environment how the performance is still an unknown, if the follow-up I am interested in continuing to improve its words, may join multi-threaded support.
VC + + crash processing and print call stack