This
article will explain a bit of how the the low-level handling of exceptions
works in the Win32/C++ environment.
I will not
be very technical here so that reading this will not be too difficult,
while still useful for the average programmer. You can find more detailed
information on the Internet, and I will also give you some links at the
bottom of the page.
Disclaimer: Don't take the things written here as 100% exact. The principles are correct but I'm still not an exception guru and I don't know some details. It's possible that I will update this page in the future when I discover something more or find mistakes.
Interrupts,
exceptions and throw statements:
Exceptions
can be generated by some software fault or with a throw statement.
Some
software faults are e.g. DivideByZero error and GeneralProtectionFault
(read/write where you couldn't). All software faults trigger an interrupt
on the CPU. [Actually, they are called "exceptions"
even at CPU level, but you can think to them like if they where
interrupts]
The
Operating System has installed its own exception handler in the interrupt
descritptor table (IDT) of the CPU, so that one gets called when your
software errs. Your exception handler will be called by that one, as
described below.
Your
exception handler WILL NEVER go into the IDT of the CPU because
that one is unique for all the applicatons in the system, so forget it.
User-mode code cannot even access IDT.
If you dig into the assembler you will find that a throw statement is finally mapped by the compiler into an INT 3 assembler instruction, which calls the exception handler exactly as if it was a software failure. So software-failure exceptions and throw statements are finally the same thing.
The Exception
Handlers linked list:
As you
know, you can put a try{} (and corresponding catch) statement in your
code, and call a function from inside it. That's in facts the normal way
to operate. The called function can set an exception handler itself with
try{} and catch() {} statements, and do things inside the try block
itself.
Every
exception handler can decide to handle some types of exception and to
leave the other exceptions to the more external handlers. If no handlers
accept that exception, the process gets terminated.
So you can begin to think that there is a linked list of exception handlers somewhere, which the OS interrupt exception handler will browse asking "Do you want to handle this kind of exception?" "Do you want...?" "Do you...?" until one of them accepts.
The linked
list works as a stack: every time the compilers finds a try/catch
statement it will add one exception handler at the top, and as soon as the
try block ends it will remove it.
Actually the
linked list is made in the usual way: every block contains the address of
the exception handler and then a pointer to the next block. So how does
the Operating System know where is the first block?
The 386
segment register called FS points to a segment which contains the
information associated to the current thread. The first DWORD points to
the first exception block of the list (the top of the "Exception
Handlers Stack").
The gate in the IDT for exception handling by the OS is CallGate-like and not TaskGate-like, so the CPU registers are mantained when the exception is raised, and the OS exception handler is executed in the address space of the process which has raised the exception. So the OS procedure for handling exception can easily read FS:[0] and find the list of custom exception handlers.
When the compiler wants to add an exception handler it creates a new block, sets the second field pointing to the previous block (= copies the address from FS:[0]), sets the first field so that it points to the new exception handling procedure, and sets FS:[0] to the address of the new block. The compiler will remove the exception handler when the execution goes out from the try block. The technique for removing the last added exception handler is exactly inverse.
Where
should we create this new exception handler block? The simplest place is
pushing it on the stack (the normal stack, I mean SS:ESP). And this is
what is done in facts.
You can find the pointer to the first block in
FS:[0]: the pointed DWORD is the address of the previous exception handler
block, and the one above (pushed first, but the stack grows down) is the
address of the exception handler function. In C++ notation we would write:
struct ExceptionHandlerBlock
{
ExceptionHandlerBlock *pPrevBlock;
EXCEPTIONHANDLERFUNCTION pfnExceptionHandler;
};
Restoring the
stack pointer:
So, when a nested function raises an exception the OS begins to call all the handlers from the innermost (most recently installed) to the outermost (older) until one of them accepts to handle that type of exception.
The
exception handler which accepts to handle the raised exception will resume
the execution at its corresponding catch block. Now you can be wondering
how could the stack pointer be OK now, since the program was executing a
nested function.
The answer
is that the compiler helps you by writing the "REAL" exception
handler: a function which sets the stack back and then jumps to your catch
block.
And how can the compiler-made exception handler (which is just a procedure, it's written statically) know what is the correct value of ESP to execute the catch block? Not every time the function is entered the value of ESP will be the same, because it depends on the exact sequence of calls which can vary according to external events.
[I'm only 80% sure here] I think the OS helps the handler by telling where it found the handler address. Yep! If the handler gets informed on where its exception-handler-block is located on the stack it can calculate the value of ESP at the moment the function was entered and so it can calculate the correct value of ESP required to execute the catch block.
This is the way VisualC++6 operates. Another compiler might choose to store larger exception blocks where there could be a third field with custom data (e.g. value of ESP and other stuff). This always requires that the OS informs the handler about where it found its address (= position of the block).
Objects and stack
unwinding:
There is one more thing to investigate: The C++ specifications tell that the classes must be exception safe. What does it mean? It means that if a class was created on the stack (= stack space allocated and constructor called) and then an exception is raised, if the handler is out of scope (e.g. in the caller function) the destructor must get called.
Since exceptions can be raised by most ASM instruction, the compiler can never be sure that an exception handler is not needed if a class is instantiated. And the situation is even more dangerous when a function is called.
So every time you use a class (with a nonempty destructor) as a local variable the compiler generates a half exception handler (my word) which is not able to handle any sort of exception but is able to do the stack unwinding for that function and will call the destructor for every class instance which was in-scope at the moment of the exception.
So the real sequence of things the OS does is this: it scans the exception handlers a first time by asking "Can you handle this kind of exception?". Then when one has been found the handlers get scanned a second time, up to that one, and they are told "Please now do your stack unwinding".
So now there is another problem: how can the half-exception-handler know for which classes the constructor was called and the destructor wasn't, and which classes are on the contrary ok? (Ok = both constructor and destructor called, or none of them). Remember that there can be many levels of { } in the code, so some objects can enter in scope (->be constructed) after others are already destroyed.
Well, VisualC++6 allocates a local DWORD variable (the first one in the local stack area: SS:[EBP-4] ) which uses as a flag to trace the execution. Every time a constructor or destructor returns, the value is changed (usually incremented). So by looking that local variable the half-handler is able to understand for which classes the destructor is to be called.
VisualC++6
decides not to use the half-exception handler if no functions are called,
even if a class is instantiated. It means it trusts the code you write "directly
there" while the object is in-scope. If you generate e.g. a GPF fault
while an object is in scope you will see that the destructor doesn't get
called. This is against the C++ specifications but I suppose it is done
for optimization purposes (a function which doesn't call another function
it's often time-critical) but if you want to change this behaviour you can
easily set a try block around your dangerous code and just set the catch
statement like this
catch (...)
{throw;}
rethrowing
the exception. This will force the compiler to write an exception handler,
and now the stack unwinding will be handled correctly.
Catching a
structure:
We are
nearly done.
One last
thing to investigate: thrown exception can throw a structure or class, and
that is catched by the catch statement.
[the class
or structure is allocated on the heap, and its address is stored in a
register before calling int3, there is no magic in this]
So you could wonder: how do the throw and catch statement agree on the type of custom structs/classes ?
Maybe when I compile the throw I have a definition of what is structure A
struct A {//Substitute
class for struct as you like
int i;
};
and when I
compile the catch I have another definition for the structure
called "A"
struct A {
float
i;
};
maybe
because the function with the throw and the function with the catch
are in
different files .cpp with different #includes.
so when
the function with the throw does
......
A a;
throw a;
......
};
and the function with the catch does:
......
try{
......
};
catch (A &a)
{
......
};
they mean
two different things.
Well, I
have tried this example and it seems this time the compiler makes the
mistake. The catch CATCHES the structure A even if it shouldn't.
Since the
compiler always creates a code which is name-wise probably the linker is
involved in all this. My supposition is that the linker generates
exception identifying numbers based on names.
There are
some fixed numbers which belong to the software faults and they are
defined by Intel I think (you can see them in win32 manuals), but as far
as throwing is concerned I think custom numbers to identify datatypes are
defined at the linking step.
Ok, We end
here :) I hope it was useful.
If you are
interested you can see more technical Win32-related exception handling
information
here.
Regards,
Gabriele Trombetti