EXCEPTION_EXECUTE_HANDLER

[Previous] [Next]

In Funcmeister2, the exception filter expression evaluates to EXCEPTION_EXECUTE_HANDLER. This value basically says to the system, "I recognize the exception. That is, I had a feeling that this exception might occur some time, and I've written some code to deal with it that I'd like to execute now." At this point, the system performs a global unwind (discussed later in this chapter) and then execution jumps to the code inside the except block (the exception handler code). After the code in the except block has executed, the system considers the exception to be handled and allows your application to continue executing. This mechanism allows Windows applications to trap errors, handle them, and continue running without the user ever knowing that the error happened.

But, once the except block has executed, where in the code should execution resume? With a little bit of thought, we can easily imagine several possibilities.

The first possibility would be for execution to resume after the CPU instruction that generates the exception. In Funcmeister2, execution would resume with the instruction that adds 10 to dwTemp. This might seem like a reasonable thing to do, but in reality, most programs are written so that they cannot continue executing successfully if one of the earlier instructions fails to execute.

In Funcmeister2, the code can continue to execute normally; however, Funcmeister2 is not the normal situation. Most likely, your code will be structured so that the CPU instructions following the instruction that generates the exception will expect a valid return value. For example, you might have a function that allocates memory, in which case a whole series of instructions will be executed to manipulate that memory. If the memory cannot be allocated, all the lines will fail, making the program generate exceptions repeatedly.

Here is another example of why execution cannot continue after the failed CPU instruction. Let's replace the C statement that generated the exception in Funcmeister2 with the following line:

 malloc(5 / dwTemp); 

For the line above, the compiler generates CPU instructions to perform the division, pushes the result on the stack, and calls the malloc function. If the division fails, the code can't continue executing properly. The system has to push something on the stack; if it doesn't, the stack gets corrupted.

Fortunately, Microsoft has not made it possible for us to have the system resume execution on the instruction following the instruction that generates the exception. This decision saves us from potential problems like these.

The second possibility would be for execution to resume with the instruction that generated the exception. This is an interesting possibility. What if inside the except block you had this statement:

 dwTemp = 2; 

With this assignment in the except block, you could resume execution with the instruction that generated the exception. This time, you would be dividing 5 by 2, and execution would continue just fine without raising another exception. You can alter something and have the system retry the instruction that generated the exception. However, you should be aware that this technique could result in some subtle behaviors. We'll discuss this technique in the "EXCEPTION_CONTINUE_EXECUTION" section.

The third and last possibility would be for execution to pick up with the first instruction following the except block. This is actually what happens when the exception filter expression evaluates to EXCEPTION_EXECUTE_HANDLER. After the code inside the except block finishes executing, control resumes at the first instruction after the except block.

Some Useful Examples

Let's say that you want to implement a totally robust application that needs to run 24 hours a day, 7 days a week. In today's world, with software so complex and so many variables and factors to affect an application's performance, I think that it's impossible to implement a totally robust application without the use of SEH. Let's look at a simple example: the C run-time function strcpy:

 char* strcpy( char* strDestination, const char* strSource); 

This is a pretty simple function, huh? How could little old strcpy ever cause a process to terminate? Well, if the caller ever passes NULL (or any bad address) for either of these parameters, strcpy raises an access violation and the whole process is terminated.

Using SEH, it's possible to create a totally robust strcpy function:

 char* RobustStrCpy(char* strDestination, const char* strSource) { _ _try { strcpy(strDestination, strSource); } _ _except (EXCEPTION_EXECUTE_HANDLER) { // Nothing to do here } return(strDestination); } 

All this function does is place the call to strcpy inside a structured exception-handling frame. If strcpy executes successfully, the function just returns. If strcpy raises an access violation, the exception filter returns EXCEPTION_EXECUTE_HANDLER, causing the thread to execute the handler code. In this function, the handler code does nothing and so again, RobustStrCpy just returns to its caller. RobustStrCpy will never cause the process to terminate!

Let's look at another example. Here's a function that returns the number of space-delimited tokens in a string:

 int RobustHowManyToken(const char* str) { int nHowManyTokens = -1; // -1 indicates failure char* strTemp = NULL; // Assume failure _ _try { // Allocate a temporary buffer strTemp = (char*) malloc(strlen(str) + 1); // Copy the original string to the temporary buffer strcpy(strTemp, str); // Get the first token char* pszToken = strtok(strTemp, " "); // Iterate through all the tokens for (; pszToken != NULL; pszToken = strtok(NULL, " ")) nHowManyTokens++; nHowManyTokens++; // Add 1 since we started at -1 } _ _except (EXCEPTION_EXECUTE_HANDLER) { // Nothing to do here } // Free the temporary buffer (guaranteed) free(strTemp); return(nHowManyTokens); } 

This function allocates a temporary buffer and copies a string into it. Then the function uses the C run-time function strtok to obtain the tokens within the string. The temporary buffer is necessary because strtok modifies the string it's tokenizing.

Thanks to SEH, this deceptively simple function handles all kinds of possibilities. Let's see how this function performs under a few different circumstances.

First, if the caller passes NULL (or any bad memory address) to the function, nHowManyTokens is initialized to -1. The call to strlen, inside the try block, raises an access violation. The exception filter gets control and passes it to the except block, which does nothing. After the except block, free is called to release the temporary block of memory. However, this memory was never allocated, so we end up calling free, passing it NULL. ANSI C explicitly states that it is legal to call free, passing it NULL, in which case free does nothing—so this is not an error. Finally, the function returns -1, indicating failure. Note that the process is not terminated.

Second, the caller might pass a good address to the function but the call to malloc (inside the try block) can fail and return NULL. This will cause the call to strcpy to raise an access violation. Again, the exception filter is called, the except block executes (which does nothing), free is called passing it NULL (which does nothing), and -1 is returned, indicating to the caller that the function failed. Note that the process is not terminated.

Finally, let's assume that the caller passes a good address to the function and the call to malloc also succeeds. In this case, the remaining code will also succeed in calculating the number of tokens in the nHowManyTokens variable. At the end of the try block, the exception filter will not be evaluated, the code in the except block will not be executed, the temporary memory buffer will be freed, and nHowManyTokens will be returned to the caller.

Using SEH is pretty cool. The RobustHowManyToken function demonstrates how to have guaranteed cleanup of a resource without using try-finally. Any code that comes after an exception handler is also guaranteed to be executed (assuming that the function does not return from within a try block—a practice that should be avoided).

Let's look at one last and particularly useful example of SEH. Here's a function that duplicates a block of memory:

 PBYTE RobustMemDup(PBYTE pbSrc, size_t cb) { PBYTE pbDup = NULL; // Assume failure _ _try { // Allocate a buffer for the duplicate memory block pbDup = (PBYTE) malloc(cb); memcpy(pbDup, pbSrc, cb); } _ _except (EXCEPTION_EXECUTE_HANDLER) { free(pbDup); pbDup = NULL; } return(pbDup); } 

This function allocates a memory buffer and copies the bytes from the source block into the destination block. Then the function returns the address of the duplicate memory buffer to the caller (or NULL if the function fails). The caller is expected to free the buffer when it no longer needs it. This is the first example in which we actually have some code inside the except block. Let's see how this function performs under different circumstances.

  • If the caller passes a bad address in the pbSrc parameter or if the call to malloc fails (returning NULL), memcpy will raise an access violation. The access violation executes the filter, which passes control to the except block. Inside the except block, the memory buffer is freed and pbDup is set to NULL so that the caller will know that the function failed. Again, note that ANSI C allows free to be passed NULL.
  • If the caller passes a good address to the function and if the call to malloc is successful, the address of the newly allocated memory block is returned to the caller.

Global Unwinds

When an exception filter evaluates to EXCEPTION_EXECUTE_HANDLER, the system must perform a global unwind. The global unwind causes all of the outstanding try-finally blocks that started executing below the try-except block that handles the exception to resume execution. Figure 24-2 shows a flowchart that describes how the system performs a global unwind. Please refer to this figure while I explain the following example.

click to view at full size.

Figure 24-2. How the system performs a global unwind

 void FuncOStimpy1() { // 1. Do any processing here.     _ _try { // 2. Call another function. FuncORen1(); // Code here never executes. } _ _except ( /* 6. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER) { // 8. After the unwind, the exception handler executes. MessageBox(…); } // 9. Exception handled--continue execution.     } void FuncORen1() { DWORD dwTemp = 0; // 3. Do any processing here.     _ _try { // 4. Request permission to access protected data. WaitForSingleObject(g_hSem, INFINITE); // 5. Modify the data. // An exception is generated here. g_dwProtectedData = 5 / dwTemp; } _ _finally { // 7. Global unwind occurs because filter evaluated // to EXCEPTION_EXECUTE_HANDLER. // Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // Continue processing--never executes.     } 

Together, FuncOStimpy1 and FuncORen1 illustrate the most confusing aspects of SEH. The numbers that begin the comments show the order of execution, but let's hold hands and walk through it together anyway.

FuncOStimpy1 begins execution by entering its try block and calling FuncORen1. FuncORen1 starts by entering its own try block and waiting to obtain a semaphore. Once it has the semaphore, FuncORen1 tries to alter the global data variable g_dwProtectedData. However, the division by 0 causes an exception to be generated. The system grabs control now and searches for a try block matched with an except block. Since the try block in FuncORen1 is matched by a finally block, the system searches upward for another try block. This time, it finds the try block in FuncOStimpy1, and it sees that FuncOStimpy1's try block is matched by an except block.

The system now evaluates the exception filter associated with FuncOStimpy1's except block and waits for the return value. When the system sees that the return value is EXCEPTION_EXECUTE_HANDLER, the system begins a global unwind in FuncORen1's finally block. Note that the unwind takes place before the system begins execution of the code in FuncOStimpy1's except block. For a global unwind, the system starts back at the bottom of all outstanding try blocks and searches this time for try blocks associated with finally blocks. The finally block that the system finds here is the one contained inside FuncORen1.

When the system executes the code in FuncORen1's finally block, you can clearly see the power of SEH. Because FuncORen1's finally block releases the semaphore, another thread is allowed to resume execution. If the call to ReleaseSemaphore were not contained inside the finally block, the semaphore would never be released.

After the code contained in the finally block has executed, the system continues walking upward, looking for outstanding finally blocks that need to be executed. This example has none. The system stops walking upward when it reaches the try-except block that decided to handle the exception. At this point, the global unwind is complete, and the system can execute the code contained inside the except block.

That's how structured exception handling works. SEH can be difficult to understand because the system gets quite involved with the execution of your code. No longer does the code flow from top to bottom—the system makes sections of code execute according to its notions of order. This order of execution is complex but predictable, and by following the flowcharts in Figure 24-1 and Figure 24-2, you should be able to use SEH with confidence.

To better understand the order of execution, let's look at what happened from a slightly different perspective. When a filter returns EXCEPTION_ EXECUTE_HANDLER, the filter is telling the operating system that the thread's instruction pointer should be set to the code inside the except block. However, the instruction pointer was inside FuncORen1's try block. From Chapter 23, you'll recall that whenever a thread leaves the try portion of a try-finally block, the code in the finally block is guaranteed to execute. The global unwind is the mechanism that ensures this rule when exceptions are raised.

Halting Global Unwinds

It's possible to stop the system from completing a global unwind by putting a return statement inside a finally block. Let's look at the code here:

 void FuncMonkey() { _ _try { FuncFish(); } _ _except (EXCEPTION_EXECUTE_HANDLER) { MessageBeep(0); } MessageBox(…); } void FuncFish() { FuncPheasant(); MessageBox(…); } void FuncPheasant() { _ _try { strcpy(NULL, NULL); } _ _finally { return; } } 

When the strcpy function is called in FuncPheasant's try block, a memory access violation exception is raised. When this happens, the system starts scanning to see whether any exception filters exist that can handle the exception. The system will find that the exception filter in FuncMonkey wants to handle the exception, and the system initiates a global unwind.

The global unwind starts by executing the code inside FuncPheasant's finally block. However, this block of code contains a return statement. The return statement causes the system to stop unwinding, and FuncPheasant will actually end up returning to FuncFish. FuncFish will continue executing and will display a message box on the screen. FuncFish will then return to FuncMonkey. The code in FuncMonkey continues executing by calling MessageBox.

Notice that the code inside FuncMonkey's exception block never executes the call to MessageBeep. The return statement in FuncPheasant's finally block causes the system to stop unwinding altogether, and execution continues as though nothing ever happened.

Microsoft deliberately designed SEH to work this way. You might occasionally want to stop unwinding and allow execution to continue. This method allows you to do so, although it usually isn't the sort of thing you want to do. As a rule, be careful to avoid putting return statements inside finally blocks.



Programming Applications for Microsoft Windows
Programming Applications for Microsoft Windows (Microsoft Programming Series)
ISBN: 1572319968
EAN: 2147483647
Year: 1999
Pages: 193

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net