Introduction
C++ is not officially supported for driver development. All the tutorials and examples are written in plain C. But C++ actually can be used too, with some reasonable limitations. Not a big surprise, since C++ is based on C, and can be seen as "advanced C". As long as you don't do C++ - specific things, there's no problem.
There're many articles that review the use of C++ in drivers. One of the most "problematic" features of C++ is exception handling. Recently I was trying to use it in one of my drivers, and encountered some problems, not too surprising. The real surprise came to me when I started reading articles about this. There're plenty of them, but I could not find any regarding the problem that I had.
At the end I've found the solution, which is based on this article. It is an excellent reference about the exception handling, and it contains a custom implementation of exception handling. However I feel there's a need for my article too, because of the following:
- The exception handling implementation in the referenced article is pretty heavy, uses a lot of STL, and can't be used as-is in drivers.
- There's too much ambiguity about the exceptions, and I'd like to classify everything, in order to identify the problem and the solution.
- I believe many others suffer from this problem too, they just don't realize it.
In order to make things clear, let's agree on what is exception handling after all.
Exceptions and Exception Handling
There're different exception handling models and mechanisms. All of them usually give you the following abilities:
- Raise an exception. The normal program flow is interrupted, and there's a so-called stack unwind procedure, which is in fact rapid return from all the scopes up until some matching position, which we'll call the
catch
block. - Pass some parameters with the exception that may select the appropriate
catch
block and its behaviour. - Cleanup code execution at every scope during the stack unwind.
The C++ language defines exception handling mechanism that supports all those features:
throw
statement. - The
thrown
type defines the appropriate catch
block, and the thrown value is also passed to it for inspection. - The cleanup is supported via destructors of automatic (on-stack) objects.
The Windows OS also offers its own exception handling mechanism, the SEH (structured exception handling):
RaiseException
() function. __except
statement may decide whether or not to handle the exception, based on its code and parameters. __finally
block is always executed.
Both models theoretically offer what we agreed to call exception handling. SEH however is more powerful in the following ways:
- The
__except
statement may cancel the exception (by returning EXCEPTION_CONTINUE_EXECUTION
). - Implicitly raised exceptions by the hardware and the OS are handled via SEH too, so there's a uniform error handling.
- Exception can be analyzed within the
__except
statement before the unwind took place, hence the whole call stack (including the error condition) is valid. This is very valuable, especially for debugging.
How SEH Works
I'll not get into details, there're plenty of articles describing it. Just in general, there's a chain of special data structures (EXCEPTION_RECORD
). Whenever you use __try
/__except
or __try
/__finally
blocks - the compiler automatically generates such a structure and adds it to the chain, then when the program leaves that scope this exception record is removed from the chain. When the exception is raised (either by explicit call to RaiseException
or implicitly by the hardware) the control is passed to the OS. It then examines the exception records chain and calls the appropriate callback functions. Those decide if they handle the exception (__except
statement), and implement the cleanup during the stack unwind (__finally
block).
C++ Exceptions vs SEH
First of all, there's no versus here. Microsoft C++ compiler implements exceptions via SEH.
For every function that has either automatic object (with destructor) or the try
/catch
block, the compiler automatically generates an appropriate exception record which'll be activated if the exception is raised.
But if you check the Microsoft compiler options, you'll find the following exception handling options:
- No, /EH-
- Yes, /EHs (a.k.a. synchronous)
- Yes with SEH, /EHa (a.k.a. asynchronous)
The "Yes" option is also called "synchronous exception handling model", whereas "Yes with SEH" is called "asynchronous exception handling model".
This may seem confusing after what we've just discussed, especially "Yes with SEH" since we know there's no exception handling without SEH.
Moreover, you can select the "No" option, and in fact use the try
/catch
, __try
/__except
and __try
/__finally
blocks, use throw
statement and call RaiseException
function. And all that will work! The compiler may however generate the following warning:
warning C4530: C++ exception handler used, but unwind semantics are not enabled.
Specify /EHsc
But everything will work! Almost everything...
So, what do those options really mean ? In order to answer this question let's recall what the compiler is obliged to support:
- Raise an exception
- Exception catching
- Cleanup during unwind
Now let's see how those things are implemented by the compiler under various exception handling settings.
- Let's start with the exception raising. It always works the same way regardless of the exception handling option. If you use
throw
statement - the compiler translates it into the following:
throw obj;
_s__ThrowInfo exc_info; _CxxThrowException(&obj, &exc_info);
That's how _CxxThrowException
function is implemented:
void _CxxThrowException(void* pObj, _s__ThrowInfo* type_info)
{
ULONG args[3];
args[0] = 0x19930520;
args[1] = (ULONG) pObj;
args[2] = (ULONG) type_info;
RaiseException(0xe06d7363, EXCEPTION_NONCONTINUABLE, 3, args);
}
So that C++ exception is in fact a regular SEH exception with C++ - specific code and arguments.
- Exception catching: This is implemented by the compiler via SEH exception records. As in the previous case, this is implemented always regardless of the exception handling settings, however there's a little difference between different options in the following scenario:
If you use catch all
block:
try {
} catch (...) {
}
If you choose to work with "Yes with SEH" then such a catch
block catches all the exceptions.
Otherwise, if you choose either "No" or "Yes" option - it will not catch non-C++ exceptions. By saying non-C++ I mean SEH exceptions with the exception code other than 0xe06d7363.
- Cleanup during unwind: Here, there is a big difference between the exception handling options. The "No" option means that the compiler should not generate exception records for automatic objects. You can use exception handling at will, but be aware that your destructors are obsolete then. That is:
struct SomeClass {
SomeClass() { TRACE(_T("SomeClass - c'tor\n")); }
~SomeClass() { TRACE(_T("SomeClass - d'tor\n")); }
};
void RaiseExc()
{
throw 1;
}
void SomeFunc()
{
try {
SomeClass obj;
RaiseExc();
} catch (...) {
}
}
Try running this example with "No" option, and you'll see that the d'tor (destructor) of obj
is not called. The same effect would be if you replace the try
/catch
block with the __try
/__except
:
void SomeFunc()
{
__try {
SomeClass obj;
RaiseExc();
} __except(EXCEPTION_EXECUTE_HANDLER) {
}
}
If you compile however with "Yes" or "Yes with SEH" option, the destructor is called.
Now, what's the difference between "Yes" and "Yes with SEH" options? One difference that we already know is that catch
(...) will ignore non-C++ exceptions, but there's another difference as well.
Set the exception handling to "Yes" and change the RaiseExc
function:
void RaiseExc()
{
((char*) NULL)[0]++; }
Run the example. If you used catch
(...) - the exception won't be handled, as expected, and the application will crash. Try now replace the try
/catch
by __try
/__except
. The exception is caught now, but... the destructor is NOT called !
Now change the RaiseExc
again
void RaiseExc()
{
if (GetTickCount() == 50)
throw 1;
((char*) NULL)[0]++; }
And now the destructor is called ! Although exactly the same exception was raised (assume GetTickCount
() wasn't equal to 50), the behaviour is now different.
Under "Yes with SEH" the destructor is called in both cases.
And this is the main difference between those two options.
With both options, the compiler should be exception-aware and ensure that destructors are called during the stack unwind.
The difference is:
- Under synchronous exception handling model ("Yes" option) the compiler is allowed to examine the program flow and if it doesn't see a possibility for exception to occur - it will OMIT the exception record block.
- Asynchronous exception handling ("Yes with SEH" option) means that the compiler is NOT ALLOWED to assume anything about where exceptions can occur, and it MUST put the exception record block for every object with destructor.
This explains why after we changed RaiseExc
under "Yes" option the compiler generated different code. Although no throw
took place actually, the compiler assumed that it might occur and generated the exception record for our obj
. And once it is generated - the destructor will be called, not only for C++ exceptions.
The Problem That I Had
I had to write a driver and I wanted to use exception handling in it. Standard SEH, not C++ exceptions. As I said, pure SEH is more powerful, especially for debugging. Theoretically there was not to be any problem, since SEH is supported for drivers.
But at some point I noticed that destructors are not actually called upon exceptions. If you've read the previous section you'll realize that the problem is that the compiler didn't generate exception records for destructors, and this is probably because the compiler works in either "No" or "Yes" exception model. I've put the "Yes with SEH" option, but then I've got the following linker error:
error LNK2019: unresolved external symbol ___CxxFrameHandler3
referenced in function __ehhandler$...
I've tried to link with different libraries, but this didn't help.
Next, I've found a library, made by some unknown hero, called libcpp
. But unfortunately it couldn't help me. It has some C++ - specific things implemented, such as new
/delete
operators, call of global object's constructors and destructors, and "C++ exception support". This support actually just implements RaiseException
API function via RtlRaiseException
kernel function.
I've tried several other libraries, but none of them addressed my problem. They had support for RTTI and other useless crap, but that was of no interest to me.
The main reason why I needed C++ is destructors. IMHO - this is the most valuable C++ advantage over plain C. I'm too lazy to write __try
/__finally
block for every resource I allocate, I prefer to use RAII. I don't need C++ exceptions, catch
blocks, and all other C++ - related stuff, just let my destructors be called upon exception.
My guess is that many people that use C++ and exceptions in drivers (and not only there) have this problem too, they just don't realize it. OH yes, all the C++ features work, no compilation/link problems. But in fact you have nothing unless you set exception handling to "Yes with SEH". And this option is not the default one. And if you implement C++ exceptions via SEH (like the libcpp
does) without setting the correct exception handling model, then you simply shoot yourself in the foot.
Solution
After reading the reference article (which I mentioned at the beginning) I've realized what the mysterious __CxxFrameHandler3
is for. When the compiler generates exception records for objects destructors it places there a reference to this function, which gets as parameters the frame info (including the list of destructors to be called) and is responsible for the following things:
- Examine the exception and decide whether this block catches it.
- Call the destructors during the unwind.
I took the implementation of this function from the reference article, but it was very complicated, especially because of (1) - it had to identify all the catch
blocks in the frame, analyze the C++ type info, assign this exception parameter (the thrown value) to the catch
block (either by value or reference), and so on. Because I didn't need all that I just took the part that handles the stack unwind and calls the destructors, which is pretty small.
Plus I've made one more modification. Imagine the following scenario: There's an exception raised at some point. Then we have the stack unwind procedure during which destructors are (hopefully) called. But what if one of the destructors also raises an exception?
Well, it is argued that C++ objects should not raise exceptions in constructors and destructors, but what if they do ? I mean, what should happen then ? If you talk about plain SEH - there's no problem. If one of the __finally
blocks raises an exception - it is handled in the same way: The OS searches for the catching block again, then consequent __finally
blocks are called anyway.
But what happens (more correctly - should happen) in C++ ? I doubt if there's a brief rule in C++ about this. Nevertheless I've made a couple of experiments, and discovered weird issues. Consider the following example:
void Crash() { TRACE("Crashing!"); ((char*) NULL)[0]++; }
struct StupObjA {
StupObjA() { TRACE("c'tor A"); }
~StupObjA() { TRACE("d'tor A"); Crash(); }
};
struct StupObjB {
StupObjB() { TRACE("c'tor B"); }
~StupObjB() { TRACE("d'tor B"); }
};
Note that StupObjA
's destructor raises an exception. Now what happens there ?
try {
StupObjB objB;
StupObjA objA;
Crash();
} catch (...) {
}
Both destructors are called, two exceptions are caught. Now let's rewrite it the following way:
void TstFunc()
{
StupObjB objB;
StupObjA objA;
Crash();
}
try {
TstFunc();
} catch (...) {
}
objB
's destructor is not called! Another redesign:
void TstFuncInner()
{
StupObjA objA;
Crash();
}
void TstFunc()
{
StupObjB objB;
TstFuncInner();
}
try {
TstFunc();
} catch (...) {
}
objB
's destructor is alive again !
I'd call this behaviour inconsistent. As I've already said I don't know if according to C++ rules objB
's destructor has to be called, or if there's such a rule at all.
Nevertheless, I've modified the __CxxFrameHandler3
function in such a way that all the destructors are always called, just like in standard SEH all __finally
sections are always executed.
Conclusion
I hope this article is useful, for drivers development and as a theoretical background about exceptions. There's a lot of ambiguity around exceptions.
If you want to use the exception handling and destructors, and you want to do it properly, that's what you MUST do:
- Set the correct exception handling model, otherwise your destructor is worth nothing. The asynchronous model gives you the guarantee that destructors are called always, at the expense of some extra code and probably some optimizations disabled. You can also work with synchronous model, however you MUST make sure the compiler is aware where exceptions can occur. For example, calling
RaiseException
or accessing potentially inaccessible memory MUST be marked by stuff like throw
(...). - Add the implementation of the
__CxxFrameHandler3
function to your project if you get the appropriate linker error. - Use
try
/catch
blocks carefully. You can't use them at all if you use my modified __CxxFrameHandler3
function - it's simply ignores them. Otherwise you can use them, however take into account that catch
(...) catches only C++ exceptions in the synchronous model.
Comments are appreciated, as usual. Both positive and negative.