牵着老婆满街逛

严以律己,宽以待人. 三思而后行.
GMail/GTalk: yanglinbo#google.com;
MSN/Email: tx7do#yahoo.com.cn;
QQ: 3 0 3 3 9 6 9 2 0 .

Microsoft Visual C++ and Win32 structured exception handling

转载自:http://www.howzatt.demon.co.uk/articles/oct04.html

Introduction

In an earlier article [1] I described some performance measurements when using exceptions in various languages on Windows. 
A couple of people since then have asked me questions about how the windows exception model actually works and how it is used to implement try ... catch constructs in MSVC. That's quite a big question to answer, and this article is a start. There is quite a lot written about how to use these language features safely; but much less written about how they are implemented. There are several good reasons for this:
  • lessons learned about the language features themselves apply to all versions of standard C++, whatever the platform, whereas details of the implementation are specific to both the vendor and the platform.
  • knowing how it works is not necessary to using it
  • the details are very sketchily documented and not guaranteed by Microsoft to remain unchanged
On the other hand I for one like to know what is going on "under the covers" so that:
  • I can satisfy my 'how do they do that?' curiousity
  • I can understand the flow of control when trying to debug application problems
  • I can perhaps provide some platform specific value-added services.
In order to give some motivation to the investigation here's my task: I want to develop an 'exception helper' so I can print out a simple call stack for a caught C++ exception. Java and C# both let you print the stack trace for the exception, but standard C++ does not provide a way to do this. Although I understand why this feature is not part of the language I do miss it in C++ and would like to do what I can towards providing it for a specific platform, in this case MSVC. 
There are some, rather intrusive, source level solutions involving adding code to the constructors of all exception types used in your application or adding code to each use of 'throw'. This sort of solution means you must change the way exceptions are used throughout the code base which can be a large task even if you have access to all the source code, and impossible if not. 
I'll describe writing a class for the MSVC compiler so that this code:
	void testStackTrace()
	{
	    ExceptionStackTrace helper;

	    try
	    {
	        doSomethingWhichMightThrow();
	    }
	    catch ( std::exception & ex )
	    {
	        std::cerr << "Exception occurred: " << ex.what() << std::endl;
	        helper.printStackTrace( std::cerr );
	    }
	}
prints out a stack trace for the exception:
Exception occurred: A sample error...
  Frame       Code address
  0x0012FE70  0x7C57E592 RaiseException+0x55
  0x0012FEB0  0x7C359AED CxxThrowException+0x34
  0x0012FF10  0x004013CA throwIt+0x4a at teststacktrace.cpp(32)
  0x0012FF18  0x004013E8 doSomethingWhichMightThrow+0x8 at teststacktrace.cpp(37)
  0x0012FF58  0x0040142A testStackTrace+0x3a at teststacktrace.cpp(45)
  0x0012FF60  0x004014A8 main+0x8 at teststacktrace.cpp(57)
  0x0012FFC0  0x00404E87 mainCRTStartup+0x143 at crtexe.c(398)
  0x0012FFF0  0x7C581AF6 OpenEventA+0x63d

Processing the Stack Trace

Microsoft provide a debugging library, DbgHelp.dll, which provides among other things functions to walk up the stack and print out the return addresses. A full description of DbgHelp.dll is outside the scope of this article - I refer you to Matt Peitrek's MSJ article [5] or John Robbins' book [6] if you want more details. 
The StackWalk() function provided by the DbgHelp DLL takes nine parameters, but the key ones are a StackFrame and a ContextRecord. The StackFrame is an in/out parameter used to contain data for successive stack frames and the ContextRecord contains the thread state in a platform dependent manner. (Different definitions of the structure are given in WinNt.h and the correct one is picked by the C++ preprocessor). The ContextRecord is technically optional, but contains enough information to initialise the StackFrame and also improve the reliability of the StackWalk function so it is preferable to require one.

So here is a function prototype for a simple stack trace routine implemented using the functionality of DbgHelp.dll:

	void SimpleSymbolEngine::StackTrace( CONTEXT *pContextRecord, std::ostream & os );
The implementation of this function sets up AddrPC, AddrFrame and AddrStack in a StackFrame record from the Eip, Ebp and Esp registers in the context record and then calls StackWalk repeatedly until the stack walk is completed. Each frame address is printed, together with the return address. Two functions in the DbgHelp library (SymGetSymFromAddr and SymGetLineFromAddr) are called to get any symbolic information available for the return address. Note that even if you don't have debug symbols for your program (and DLLs) DbgHelp will try to provide information based on any exported names from in DLLs.

A context record can be obtained in a variety of ways. As it is simply a snapshot of the thread state it could be built up manually using inline assembler to populate the various fields from CPU registers - or more easily by using the GetThreadContext call. The operating system also uses them in various places when managing thread state and finally they also crop up in exception handling.

The main reason to write the 'ExceptionHelper' class is to obtain the context record of the thread state when the exception occurred. We can then use this key piece of data to extract the stack trace. Let's look at Microsoft's implementation of try, throw and catch in Win32 C++ to see how it lets us build something to extract this information.

Structured exception handling

Microsoft integrated standard C++ exception handling with window's own exception handling model: so-called "structured exception handling" or "SEH" and this section tries to give an overview of what is happening inside SEH from an application's viewpoint - however you don't need to completely understand the principles to follow the ExceptionHelper code.

The definitive article about Win32 structured exception handling is by Matt Peitrek [2] and I refer interested readers there. However, his article focuses on the Microsoft extensions to support SEH: _try, _except and _finally and less on the language native concepts embodied in try, throw, etc. Other articles, such as [3], focus on what is happening at the assembler level which is great for the minority of programmers who understand assembler but not for the rest.

There is a relatively natural fit between the SEH exception model and the 'try ... catch' exception model in programming languages such as C++ so it is not too surprising that Microsoft decided to use this operating system level structured exception handling to provide the basis for their C++ exception handling code. However other implementors of C++ on the Win32 platform have not necessarily followed the same pattern.

Windows provides a portable exception architecture which recognises two main type of exceptions: 'system' exceptions such as an access violation or an integer divide by zero, which are also known as 'asynchronous' exceptions, and 'user' exceptions generated by a call to RaiseException(), which are also known as 'synchronous' exceptions. Each thread contains a linked list of exception handlers and when an exception occurs information about the exception and a context record for the thread are passed to each exception handler in the chain in turn for possible processing. There are several things each handler can do; the commonest cases are:

  • return 'keep looking' (and the next exception handler will be called)
  • unwind the thread context back to a known state and execute a failure path (in C++, a 'catch' block).
If the context is to be unwound then each exception handler which is unwound off the stack is called so it can perform any required tidy-up. If all of the exception handlers return 'keep looking' the operating system has a final, process wide, exception handler which by default produces one of the 'Application Error' popups. (Note that this is a slight simplification of the full picture)

Each handler has a signature like this:

DWORD exceptionHandler( EXCEPTION_RECORD *pException, EXCEPTION_REGISTRATION_RECORD *pRegistrationRecord, CONTEXT *pContext );
Where:
  • pException contains information about the exception being processed, such as the exception code and the fault address.
  • pRegistrationRecord points to the current node in the list of exception handlers
  • pContext contains the processor-specific thread state when the exception occurred
Our task is to retain the thread context from the last parameter so we can use it later in a call to the StackTrace function.

What makes this exception style 'structured' is that the chain of exception handlers exists in the thread's own stack. In a typically block-structured programming language each call to a function, method or procedure pushes a new activation frame onto the stack; this frame contains the current arguments, any local variables and the exception handler for this function (if any). Additionally, the algorithm which passes the exception along the chain of handlers naturally moves from the most recently called function up the stack to the top most function.

In the Win32 world the 'ESP' register contains the current stack pointer, by convention the current frame pointer is usually held in the 'EBP' register and the 'FS' selector register holds the base of the thread information block (TIB) which holds, among other things, the head of the exception chain.

To try and make this clearer here is a schematic representation of the bottom of the stack when function 'A' has called function 'B' which in turn has called function 'C'. Functions A and C have an SEH handler, but function B doesn't.

Inside the each stack frame the function arguments are above the frame register (and appear in assembler as [EBP + offset]) and local variables are below the frame register (and appear in assembler as [EBP - offset]). In practice things are more complicated than this - particularly when the optimiser gets involved - and the frame register EBP can get used for other purposes. To reduce the complexity of this article I'm not going to worry about optimised code.

We need insert our own exception helper object into the chain of exception registration records so we can extract the context record for the thrown exception.

MSVC exception handling

Before we can write our own exception handler we need to know a bit about how MSVC makes use of SEH handling to implement C++ exceptions. I've annotated the following code fragment with some of the key places that SEH handling is involved.
	void thrower()
        {
		SomeClass anObject; // ** 5 **
		throw std::runtime_error( "An error" ); // ** 2 **
	}

	void catcher()
	{	// ** 1 **
		std::string functionName( "catcher" );
		try
		{
			thrower();
		}
		catch ( std::exception & ex )	// ** 3 **
		{
			std::cerr << functionName << ": " << ex.what() << std::endl;	
		}
	}	// ** 4 **

1) When you write a function containing 'try' ... 'catch' the Microsoft compiler adds code to the function prolog to register a structured exception handler for this function at the head of the thread's exception handler chain. The actual structure created for the exception registration extends the basic EXCEPTION_REGISTRATION_RECORD; the additional fields are used for managing the exception handling state and recovering the stack pointer.

2) 'Throw' is implemented by calling RaiseException with a special exception code 0xe06d7363 (the low bits spell 'msc' in ascii). Other fields in the exception record are set up to hold the address and run-time type of the thrown object - in this case a std::runtime_error object.

3) The catch code is actually implemented by the exception handler. If the exception being handled has the special exception code value then the run-time type information is extracted and compared to the type of the argument in the catch statement. If a match is found (or a conversion is possible) then the exception chain is unwound and the body of the catch is entered, after which the execution will continue directly after the try ... catch block. If a match is not found the exception handler returns the 'keep looking' value and the new handler in the chain will be tried.

4) On function exit the exception handler for catcher is removed from the chain.

5) There's another place that SEH code is needed of course - the destructor for 'anObject' must be called during the unwind back to the 'catch' statement. So there is actually yet another exception handler registered for 'thrower' too, to deal with ensuring that anObject gets deleted. This one never tries to handle the exception but simply ensures local variables are destructed during the unwind.

One key thing about the way MSVC exception handling works is that it involves making extra calls down the stack. At point (2) the C++ runtime calls RaiseException, which snapshots the exception and thread state and then it in turn calls the code to work along the exception chain calling exception handlers. At point (3) when the exception handler for 'catcher' gets control it is a long way down the stack. The exception chain is unwound by yet another call, this time to RtlUnwind. This function throws another exception along the exception chain with a special flag value 'EXCEPTION_UNWINDING' set, giving each exception handler in turn a chance to do tidying up before it is removed from the exception chain. After returning from RtlUnwind the body of the catch statement is then called. When the catch body completes control returns back to the C++ runtime which completes tidying up the stack pointer, deletes the exception object and then resumes execution at the next instruction after the catch block.

So how does the catch block make use of the local variable 'functionName' if it is so far down the stack when it gets control?

What the C++ runtime does is to use the extended exception registration record (passed to the handler as the second argument) to recover the value of the frame pointer EBP. Having reset the frame pointer the code in the catch body can make use of local variables and function arguments without difficulty. It is does not affect the function that the stack pointer is not simply a few bytes below the frame pointer but several hundred bytes below it.

The upshot is that, when the catch body is executed, the complete stack down to the location of the 'throw' is still available in memory. The raw stack pointer will only be reset when the body of the stack completes, and before this point the call stack will not be touched. So if we can obtain the address of the context record that was passed into each exception handler as the third argument, the pointer will still be valid inside the body of the catch.

Looking back to the way the chain of exception handlers is processed we can see that if we can hook our code into the exception chain just before the compiler written exception handler we can extract information from the context record and then use that information inside the catch handler to allow us print a stack trace. Let's look at how we can do this.

Adding to the exception chain

The exception chain in Win32 consists of a singly linked list of EXCEPTION_REGISTRATION_RECORDs on the stack. Unfortunately Microsoft do not provide a C++ definition for this structure (possibly because it is different on each hardware platform running Windows) but they do provide one in an assembler include file EXSUP.INC which can be translated into C++ like this:
	/** Typedef for the exception handler function prototype */
	typedef DWORD (fnExceptionHandler)( EXCEPTION_RECORD *pException, struct _EXCEPTION_REGISTRATION_RECORD *pRegistrationRecord, CONTEXT *pContext );

	/** Definition of 'raw' WinNt exception registration record - this ought to be in WinNt.h */
	struct _EXCEPTION_REGISTRATION_RECORD
	{
	    struct _EXCEPTION_REGISTRATION_RECORD *PrevExceptionRegistrationRecord; // Chain to previous record
	    fnExceptionHandler *ExceptionHandler; // Handler function being registered
	};
So all we need to do, it seems, is to create an _EXCEPTION_REGISTRATION_RECORD, point ExceptionHandler to our exception handling function and insert the record at the top of the exception chain. Almost. There are a some complexities with registering your own exception handlers.

Firstly, the code which walks the exception chain requires (on some but not all versions of Windows) that the nodes in the chain are registered in strict address order. Fortunately the compiler always puts local variables below the exception registration record so by using a local variable for our exception helper we should always be able to insert it into the chain before the compiler generated exception registration record.

Secondly, I want to register the exception handler in a constructor. This function too has a compiler-generated exception handler. I must ensure that the handler is registered in the chain above the record for the constructor or my exception handler will be unregistered when the constructor completes! Additionally I want the code to work properly should there be two or more exception helper objects in a single function, and it is not in general possible to fix the offsets in the stack frame assigned by the compiler for local variables.

Lastly, as part of the security improvements included with Visual Studio .NET 2003 and Windows Server 2003/Windows XP service pack 2, the exception handling function to be called must be marked with a special attribute ('SAFESEH') at link time so it will appear in the "Safe Exception Handler Table" in the load configuration record. Failure to do this results in a security exception occurring at runtime which usually terminates the process. This check has been added to Windows to prevent security exploits that use buffer overrun in order to replace the exception handler address on the stack with a pointer to injected code. The SAFESEH attribute can only be granted by assembler code so it is therefore necessary, when using Visual Studio .NET 2003, to make use of a very simple piece of assembler code to add this attribute to the exception handling function.

Note: the assembler 'ml.exe' provided with the first Beta edition of Visual Studio 2005 access violates when using /safeseh [4] and that from 2003 must be used.

One mechanism I've found that provides good ease of use under the above constraints is to create a common base class for my own exception handlers. This class contains a static exception handling function which can be marked, once and for all, with the SAFESEH attribute. This common handler then makes a virtual call into the derived class for the specific action required for exception handling.

	class ExceptionHelperBase : public _EXCEPTION_REGISTRATION_RECORD
	{
	public:
	    /** Construct helper object */
	    ExceptionHelperBase();

	    /** Make safe to extend */
	    virtual ~ExceptionHelperBase() {}

	    /** Allow subclass to hook exception */
	    virtual void onException( EXCEPTION_RECORD *pException, CONTEXT *pContext ) = 0;

	private:
	    // Disable copy and assign
	    ExceptionHelperBase( ExceptionHelperBase const & );
	    ExceptionHelperBase& operator=( ExceptionHelperBase const & );

            // The one and only exception handler function
	    static fnExceptionHandler exceptionHandler;
	};
The exception handling function simply casts the exception registration record back to an ExceptionHelperBase and invokes 'onException':
	DWORD ExceptionHelperBase::exceptionHandler( EXCEPTION_RECORD *pException, struct _EXCEPTION_REGISTRATION_RECORD *pRegistrationRecord, CONTEXT *pContext )
	{
	    ExceptionHelperBase &self = static_cast( *pRegistrationRecord );
	    self.onException( pException, pContext );
            return ExceptionContinueSearch;
	}
I would like my exception class to register itself in the constructor and deregister itself in the destructor. Unfortunately I can't simply do this in the ExceptionHelperBase constructor and destructor without risking problems if I get an exception during the constructor/destructor code itself. However, a use of the 'curiously recurring template pattern' fixes this problem and ensures registration happens last in the constructor and first in the destructor:
	    template 
	    class AutoRegister : public RegistrationRecord
	    {
	    public:
	        /** Auto-register an exception record for 'RegistrationRecord' */
	        AutoRegister()
	        : RegistrationRecord()
	        {
	            registerHandler( this );
	        }

	        /** Unregister and destroy an exception record */
	        ~AutoRegister()
	        {
	            unregisterHandler( this );
	        }
	    };
Where 'registerHandler' will install the handler in the exception chain and 'unregisterHandler' will remove it from the chain by using standard logic for singly-linked lists. The list head is held in the NT_TIB structure pointed to by the FS register and the list tail is the value "-1".

Processing the exception

The first derived class simply prints out the exception information to demonstrate that things are working properly:
	class ExceptionHelperImpl1 : public ExceptionHelperBase
	{
	    /** Print the address of the exception records */
	    virtual void onException( EXCEPTION_RECORD *pException, CONTEXT *pContext )
	    {
	       printf( "pException: %p (code: %p, flags: %x), pContext: %p\n", pException, pException->ExceptionCode, pException->ExceptionFlags, pContext );
	    }
	};

	typedef AutoRegister< ExceptionHelperImpl1 > ExceptionHelper1;
Since this code is executing while an exception is actually being processed I used printf() rather than std::cout to avoid any potentially harmful interactions with the standard library.

Sample code:

	int main()
	{
	    ExceptionHelper1 helper;

	    try
	    {
	        printf( "About to throw\n" );
	        throw std::runtime_error( "basic exception" );
	    }
	    catch ( std::exception & /*ex*/ )
	    {
	        printf( "In catch handler\n" );
	    }
            return 0;
	}
when executed this program generates output for two exceptions:
	About to throw
	pException: 0012FBA0 (code: E06D7363, flags: 1), pContext: 0012FBC0
	pException: 0012FBA0 (code: E06D7363, flags: 3), pContext: 0012F670
	In catch handler
The second call is generated by the Microsoft supplied exception handler unwinding the exception chain. This is easily identified as the EXCEPTION_UNWINDING flag (value 2) is set in pException->ExceptionFlags for the second exception. For our purposes we want to extract context data from the first call since this context describes the thread state when 'throw' was executed. (Note that our exception handler is removed from the chain during the exception unwind so would need to be re-inserted to catch subsequent exceptions in the same scope)

We now have everything we need for the basic version of the code to print a stack trace:

	class ExceptionStackTraceImpl : public ExceptionHelperBase
	{
	public:
	    ExceptionStackTraceImpl() : pSavedContext(0) {}

	    /** Use the saved pointer to print the stack trace */
	    void printStackTrace( std::ostream & os ) const
	    {
	        if ( pSavedContext != 0 )
	            SimpleSymbolEngine::instance().StackTrace( pSavedContext, os );
	    }

	private:
	    /** Capture the thread context when the initial exception occurred */
	    virtual void onException( EXCEPTION_RECORD *pException, CONTEXT *pContext )
	    {
	       if ( ( pException->ExceptionFlags & EXCEPTION_UNWINDING ) == 0 )
	       {
	           pSavedContext = pContext;
	       }
	    }

	    PCONTEXT pSavedContext; // context record from the last exception
	};

	typedef AutoRegister< ExceptionStackTraceImpl > ExceptionStackTrace;
We have now achieved the original aim of be able to print a stack trace in the catch block.

Interaction with normal SEH

This method of 'hooking' in to the MSVC handling of C++ exceptions means that the exception handler is also called for every other SEH exception, such as access violation. In released versions of MSVC the implementation of catch (...) also processed these types of exceptions. Although this seems at first sight to be a good thing it actually tends to cause more problems than it solves. One particular issue is that genuine problems such as corrupt memory, I/O errors on the paging file or load time problems with DLLs get handled in the same way as a C++ exception of unknown type, by code not written to deal with these error conditions. For current versions of MSVC it is usually best to avoid use of catch( ... ) unless either the code re-throws the exception or terminates the process.

Visual Studio 2005 Beta 1 handles non-C++ SEH exceptions in a catch( ... ) only when the compiler flag /EHa is set, which is a great improvement and gives maximum flexibility.

Whether or not /EHa is specified we can use ExceptionHelper to extract information about the OS exception. Microsoft provide some other ways to achieve a similar end, __try/__except and _set_se_translator, but they are not total solutions. Also not all compler vendors provide such extensions and the ExceptionHelper code could still be used to extract information about the exception.

For example I modified ExceptionHelper1 class for gcc on win32 (a couple of minor changes were required for a clean compile). Since gcc does not seem to use SEH for C++ exception handling ExceptionHelper1 did not capture information for such exceptions but it did do so for Win32 exceptions, such as access violations. For a "proof of concept" I changed the exception handler to throw a std::runtime exception rather than printing the exception information and was successful in mapping an SEH exception into a C++ exception, thus allowing additional tidyup to be performed before the program exited.

Conclusion

I have given a brief overview of how the Microsoft compilers on Win32 implement C++ exception handling. Using this information we've seen a simple class which enables additional information to be obtained about the exception during program execution.

What is the main strength and weakness of this approach?

On the positive side it enables better diagnostic information to be produced at runtime for MSVC on win32. This can significantly reduce the cost of finding bugs, since enough information might be gathered in the field to identify the root cause. Without this extra information it might be necessary to try and reproduce the problem under a debugger, with potential difficultly of getting the right execution environment to enure the problem does in fact appear.

The main weakness is that the code is platform specific and relies on undocumented behaviour of the compiler. Other compilers under Win32 do not use the SEH mechanism to handle C++ exceptions so this code is useless should your code need to be portable to them. The implementation of the exception mechanism even under 64bit windows is not the same as for 32 bit windows, so the technique described here will not work unchanged (if at all) in that environment even for Microsoft compilers.

The decision depends on the relative balance between these two items. However, having isolated the logic into a single class 'ExceptionStackTrace' it can be conditionally compiled to a do-nothing implementation on other platforms, or if may even be possible to re-implement the logic for another platform.

Updates

A couple of readers have pointed out that with VS 2005/2008 the assembler file needs SYSCALL added to the PROTO directive. VS 2003 works with or without this.

I have made this change to the code in the zip file.

Klaus Triendl also pointed out that the exception handler should have a 4th parameter `void* DispatcherContext'. This is correct, but the final parameter can be safely ignored if it is not used, as the function uses the C calling convention and so the caller is responsible for clearing up the stack.

Note on multi-threaded programs.

The simple stack walking code above may fail in unpredictable ways if an exception occurs in more than one thread at once. The Microsoft documentation for the functions in the DbgHelp library states: 
"All DbgHelp functions, such as this one, are single threaded. Therefore, calls from more than one thread to this function will likely result in unexpected behavior or memory corruption. To avoid this, you must synchronize all concurrent calls from more than one thread to this function."

Hence if you wish to debug multi-threaded programs you will need to add some explicit synchronisation to the access to the DbgHelp library. My apologies to Jonathan Lepolt, who was affected by this problem, for not making this clear in the original article.

References

[1] 'Efficient Exceptions?', Overload 61, June 2004
[2] 'A Crash Course on the Depths of Win32� Structured Exception Handling', Matt Pietrek, http://www.microsoft.com/msj/0197/Exception/Exception.aspx
[3] 'Win32 Exception handling for assembler programmers', Jeremy Gordon, http://www.jorgon.freeserve.co.uk/Except/Except.htm
[4] 'MSDN Product Feedback Center', http://lab.msdn.microsoft.com/ProductFeedback, search on keyword "FDBK12741"
[5] 'Improved Error Reporting with DBGHELP 5.1 APIs', Matt Pietrek, http://msdn.microsoft.com/msdnmag/issues/02/03/hood/default.aspx
[6] 'Debugging Applications for Microsoft .NET and Microsoft Windows', John Robbins, Microsoft Press
Source code for the article
Copyright (c) Roger Orr - rogero@howzatt.demon.co.uk 
Published in Overload issue 63, October 2004.
$Revision: 1.4 $ $Date: 2008/09/24 22:33:18 $

posted on 2012-02-06 00:23 杨粼波 阅读(1601) 评论(0)  编辑 收藏 引用 所属分类: C++


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理