Click here to Skip to main content
15,867,141 members
Articles / Programming Languages / C
Article

Drivers, Exceptions and C++

Rate me:
Please Sign up or sign in to vote.
4.97/5 (16 votes)
14 Jan 2008CPOL11 min read 95.8K   962   80   21
Using C++ objects with destructors and exceptions in drivers

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:

  1. 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.
  2. Pass some parameters with the exception that may select the appropriate catch block and its behaviour.
  3. Cleanup code execution at every scope during the stack unwind.

The C++ language defines exception handling mechanism that supports all those features:

  1. throw statement.
  2. The thrown type defines the appropriate catch block, and the thrown value is also passed to it for inspection.
  3. 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):

  1. RaiseException() function.
  2. __except statement may decide whether or not to handle the exception, based on its code and parameters.
  3. __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:

  1. Raise an exception
  2. Exception catching
  3. Cleanup during unwind

Now let's see how those things are implemented by the compiler under various exception handling settings.

  1. 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:

    C++
    // The following line:
    throw obj;
    // is interpreted by the compiler in the following manner:
    _s__ThrowInfo exc_info; // this structure describes the type of the thrown object
    // compiler fills this structure with the thrown type info, so that the appropriate
    // catch block can recognize the thrown type.
    // ..
    _CxxThrowException(&obj, &exc_info);

    That's how _CxxThrowException function is implemented:

    C++
    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.

  2. 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:

    C++
    try {
        // ... do something
    } catch (...) {
        // catch all exceptions regardless to their type.
    }

    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.

  3. 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:

    C++
    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:

    C++
    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:

    C++
    void RaiseExc()
    {
        ((char*) NULL)[0]++; // invalid memory access
    }

    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

    C++
    void RaiseExc()
    {
        if (GetTickCount() == 50)
            throw 1;
        ((char*) NULL)[0]++; // invalid memory access
    }

    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:

  1. Examine the exception and decide whether this block catches it.
  2. 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:

C++
void Crash() { TRACE("Crashing!"); ((char*) NULL)[0]++; } // invalid memory access

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 ?

C++
try {
    StupObjB objB;
    StupObjA objA;
    Crash();
} catch (...) {
}

Both destructors are called, two exceptions are caught. Now let's rewrite it the following way:

C++
void TstFunc()
{
    StupObjB objB;
    StupObjA objA;
    Crash();
}

try {
    TstFunc();
} catch (...) {
}

objB's destructor is not called! Another redesign:

C++
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Israel Israel
My name is Vladislav Gelfer, I was born in Kiev (former Soviet Union), since 1993 I live in Israel.
In programming I'm interested mostly in low-level, OOP design, DSP and multimedia.
Besides of the programming I like physics, math, digital photography.

Comments and Discussions

 
Question__EH_epilog3? Pin
whitebrow23-Jul-12 22:12
whitebrow23-Jul-12 22:12 
GeneralMy vote of 5 Pin
mikimotoh28-Jun-12 23:06
mikimotoh28-Jun-12 23:06 
GeneralFollow-up problem: Linking error when throwing exceptions Pin
J. Ruffing11-Mar-11 1:27
J. Ruffing11-Mar-11 1:27 
GeneralRe: Pin
valdok14-Mar-11 12:29
valdok14-Mar-11 12:29 
GeneralDriver Loading error in Non Admin mode Pin
Waqar Abbas16-Jul-10 4:38
Waqar Abbas16-Jul-10 4:38 
GeneralRe: Pin
valdok16-Jul-10 10:07
valdok16-Jul-10 10:07 
GeneralRe: Pin
Waqar Abbas18-Jul-10 19:47
Waqar Abbas18-Jul-10 19:47 
GeneralRe: Pin
Waqar Abbas18-Jul-10 22:21
Waqar Abbas18-Jul-10 22:21 
GeneralRe: Pin
valdok19-Jul-10 0:28
valdok19-Jul-10 0:28 
GeneralGood article Pin
Anthony Akentiev5-May-09 8:56
Anthony Akentiev5-May-09 8:56 
Only one correction: "Well, it is argued that C++ objects should not raise exceptions in constructors and destructors, but what if they do ?". C++ should not rise an exception ONLY IN DESTRUCTORS, but not in constructors! I believe that one of the reasons "exceptions" as an entity were implemented in C++ - is missing return type for constructors. At least that is what Straustroup said.
GeneralRe: Pin
valdok13-Jul-09 0:24
valdok13-Jul-09 0:24 
GeneralRe: Pin
Member 459652610-Sep-09 4:41
Member 459652610-Sep-09 4:41 
GeneralRe: Pin
valdok13-Sep-09 2:30
valdok13-Sep-09 2:30 
Generalbetter CxxFrameHandler3 Pin
s.t.a.s.22-Feb-09 15:27
s.t.a.s.22-Feb-09 15:27 
GeneralEnjoyed your in-depth analysis Pin
wtwhite22-Jan-08 17:51
wtwhite22-Jan-08 17:51 
GeneralRe Pin
valdok23-Jan-08 2:02
valdok23-Jan-08 2:02 
GeneralRe: Re Pin
wtwhite23-Jan-08 13:58
wtwhite23-Jan-08 13:58 
GeneralConfusing final conclusion, please explain Pin
V.S.10-Jan-08 6:28
V.S.10-Jan-08 6:28 
GeneralSorry for the confuse Pin
valdok14-Jan-08 0:11
valdok14-Jan-08 0:11 
GeneralRe: Sorry for the confuse Pin
V.S.15-Jan-08 5:36
V.S.15-Jan-08 5:36 
GeneralThe C++ standard on exceptions in destructors Pin
Don Clugston10-Jan-08 1:55
Don Clugston10-Jan-08 1:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.