Understanding Termination Handlers by Example

[Previous] [Next]

Because the compiler and the operating system are intimately involved with the execution of your code when you use SEH, I believe that the best way to demonstrate how SEH works is by examining source code samples and discussing the order in which the statements execute in each example.

Therefore, the next few sections show different source code fragments, and the text associated with each fragment explains how the compiler and operating system alter the execution order of your code.

Funcenstein1

To appreciate the ramifications of using termination handlers, let's examine a more concrete coding example.

 DWORD Funcenstein1() { DWORD dwTemp; // 1. Do any processing here.     _ _try { // 2. Request permission to access // protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; } _ _finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // 4. Continue processing. return(dwTemp); } 

The numbered comments above indicate the order in which your code will execute. In Funcenstein1, using the try-finally blocks isn't doing much for you. The code will wait for a semaphore, alter the contents of the protected data, save the new value in the local variable dwTemp, release the semaphore, and return the new value to the caller.

Funcenstein2

Now let's modify the function a little and see what happens:

 DWORD Funcenstein2() { DWORD dwTemp; // 1. Do any processing here.     _ _try { // 2. Request permission to access // protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; // Return the new value. return(dwTemp); } _ _finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // Continue processing--this code // will never execute in this version. dwTemp = 9; return(dwTemp); } 

In Funcenstein2, a return statement has been added to the end of the try block. This return statement tells the compiler that you want to exit the function and return the contents of the dwTemp variable, which now contains the value 5. However, if this return statement had been executed, the thread would not have released the semaphore—and no other thread would ever regain control of the semaphore. As you can imagine, this kind of sequence can become a really big problem because threads waiting for the semaphore might never resume execution.

However, by using the termination handler, you have avoided the premature execution of the return statement. When the return statement attempts to exit the try block, the compiler makes sure that the code in the finally block executes first. The code inside the finally block is guaranteed to execute before the return statement in the try block is allowed to exit. In Funcenstein2, putting the call to ReleaseSemaphore into a termination handler block ensures that the semaphore will always be released. There is no chance for a thread to accidentally retain ownership of the semaphore, which would mean that all other threads waiting for the semaphore would never be scheduled CPU time.

After the code in the finally block executes, the function does, in fact, return. Any code appearing below the finally block doesn't execute because the function returns in the try block. Therefore, this function returns the value 5, not the value 9.

You might be asking yourself how the compiler guarantees that the finally block executes before the try block can be exited. When the compiler examines your source code, it sees that you have coded a return statement inside a try block. Having seen this, the compiler generates code to save the return value (5 in our example) in a temporary variable created by the compiler. The compiler then generates code to execute the instructions contained inside the finally block; this is called a local unwind. More specifically, a local unwind occurs when the system executes the contents of a finally block because of the premature exit of code in a try block. After the instructions inside the finally block execute, the value in the compiler's temporary variable is retrieved and returned from the function.

As you can see, the compiler must generate additional code and the system must perform additional work to pull this whole thing off. On different CPUs, the steps needed for termination handling to work vary. The Alpha processor, for example, must execute several hundred or even several thousand CPU instructions to capture the try block's premature return and call the finally block. You should avoid writing code that causes premature exits from the try block of a termination handler because the performance of your application could be adversely impacted. Later in this chapter, I'll discuss the _ _leave keyword, which can help you avoid writing code that forces local unwinds.

Exception handling is designed to capture exceptions—the exceptions to the rule that you expect to happen infrequently (in our example, the premature return). If a situation is the norm, checking for the situation explicitly is much more efficient than relying on the SEH capabilities of the operating system and your compiler to trap common occurrences.

Note that when the flow of control naturally leaves the try block and enters the finally block (as shown in Funcenstein1), the overhead of entering the finally block is minimal. On the x86 CPUs using Microsoft's compiler, a single machine instruction is executed as execution leaves the try block to enter the finally block—I doubt that you will even notice this overhead in your application. When the compiler has to generate additional code and the system has to perform additional work, as in Funcenstein2, the overhead is much more noticeable.

Funcenstein3

Now let's modify the function again and take a look at what happens:

 DWORD Funcenstein3() { DWORD dwTemp; // 1. Do any processing here.     _ _try { // 2. Request permission to access // protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; // Try to jump over the finally block. goto ReturnValue; } _ _finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } dwTemp = 9; // 4. Continue processing. ReturnValue: return(dwTemp); } 

In Funcenstein3, when the compiler sees the goto statement in the try block, it generates a local unwind to execute the contents of the finally block first. However, this time, after the code in the finally block executes, the code after the ReturnValue label is executed because no return occurs in either the try or the finally block. This code causes the function to return a 5. Again, because you have interrupted the natural flow of control from the try block into the finally block, you could incur a high performance penalty depending on the CPU your application is running on.

Funcfurter1

Now let's look at another scenario in which termination handling really proves its value. Look at this function:

 DWORD Funcfurter1() { DWORD dwTemp; // 1. Do any processing here.     _ _try { // 2. Request permission to access // protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); dwTemp = Funcinator(g_dwProtectedData); } _ _finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // 4. Continue processing. return(dwTemp); } 

Now imagine that the Funcinator function called in the try block contains a bug that causes an invalid memory access. Without SEH, this situation would present the user with the ever-popular Application Error dialog box. When the user dismissed the error dialog box, the process would be terminated. When the process is terminated (because of an invalid memory access), the semaphore would still be owned and would never be released—any threads in other processes that were waiting for this semaphore would never be scheduled CPU time. But placing the call to ReleaseSemaphore in a finally block guarantees that the semaphore gets released even if some other function causes a memory access violation.

If termination handlers are powerful enough to capture a process while terminating because of an invalid memory access, we should have no trouble believing that they will also capture setjump and longjump combinations and, of course, simple statements such as break and continue.

Pop Quiz Time: FuncaDoodleDoo

Now for a test. Can you determine what the following function returns?

 DWORD FuncaDoodleDoo() { DWORD dwTemp = 0; while (dwTemp < 10) { _ _try { if (dwTemp == 2) continue; if (dwTemp == 3) break; } _ _finally { dwTemp++; } dwTemp++; } dwTemp += 10; return(dwTemp); } 

Let's analyze what the function does step by step. First dwTemp is set to 0. The code in the try block executes, but neither of the if statements evaluates to TRUE. Execution moves naturally to the code in the finally block, which increments dwTemp to 1. Then the instruction after the finally block increments dwTemp again, making it 2.

When the loop iterates, dwTemp is 2 and the continue statement in the try block will execute. Without a termination handler to force execution of the finally block before exit from the try block, execution would immediately jump back up to the while test, dwTemp would not be changed, and we would have started an infinite loop. With a termination handler, the system notes that the continue statement causes the flow of control to exit the try block prematurely and moves execution to the finally block. In the finally block, dwTemp is incremented to 3. However, the code after the finally block doesn't execute because the flow of control moves back to continue and thus to the top of the loop.

Now we are processing the loop's third iteration. This time, the first if statement evaluates to FALSE, but the second if statement evaluates to TRUE. The system again catches our attempt to break out of the try block and executes the code in the finally block first. Now dwTemp is incremented to 4. Because a break statement was executed, control resumes after the loop. Thus, the code after the finally block and still inside the loop doesn't execute. The code below the loop adds 10 to dwTemp for a grand total of 14—the result of calling this function. It should go without saying that you should never actually write code like FuncaDoodleDoo. I placed the continue and break statements in the middle of the code only to demonstrate the operation of the termination handler.

Although a termination handler will catch most situations in which the try block would otherwise be exited prematurely, it can't cause the code in a finally block to be executed if the thread or process is terminated. A call to ExitThread or ExitProcess will immediately terminate the thread or process without executing any of the code in a finally block. Also, if your thread or process should die because some application called TerminateThread or TerminateProcess, the code in a finally block again won't execute. Some C run-time functions (such as abort) that in turn call ExitProcess again preclude the execution of finally blocks. You can't do anything to prevent another application from terminating one of your threads or processes, but you can prevent your own premature calls to ExitThread and ExitProcess.

Funcenstein4

Let's take a look at one more termination handling scenario.

 DWORD Funcenstein4() { DWORD dwTemp; // 1. Do any processing here.     _ _try { // 2. Request permission to access // protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; // Return the new value. return(dwTemp); } _ _finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); return(103); } // Continue processing--this code will never execute. dwTemp = 9; return(dwTemp); } 

In Funcenstein4, the try block will execute and try to return the value of dwTemp (5) back to Funcenstein4's caller. As noted in the discussion of Funcenstein2, trying to return prematurely from a try block causes the generation of code that puts the return value into a temporary variable created by the compiler. Then the code inside the finally block is executed. Notice that in this variation on Funcenstein2 I have added a return statement to the finally block. Will Funcenstein4 return 5 or 103 to the caller? The answer is 103 because the return statement in the finally block causes the value 103 to be stored in the same temporary variable in which the value 5 has been stored, overwriting the 5. When the finally block completes execution, the value now in the temporary variable (103) is returned from Funcenstein4 to its caller.

We've seen termination handlers do an effective job of rescuing execution from a premature exit of the try block, and we've also seen termination handlers produce an unwanted result because they prevented a premature exit of the try block. A good rule of thumb is to avoid any statements that would cause a premature exit of the try block part of a termination handler. In fact, it is always best to remove all returns, continues, breaks, gotos, and so on from inside both the try and the finally blocks of a termination handler and to put these statements outside the handler. Such a practice will cause the compiler to generate both a smaller amount of code—because it won't have to catch premature exits from the try block—and faster code, because it will have fewer instructions to execute in order to perform the local unwind. In addition, your code will be much easier to read and maintain.

Funcarama1

We've pretty much covered the basic syntax and semantics of termination handlers. Now let's look at how a termination handler could be used to simplify a more complicated programming problem. Let's look at a function that doesn't take advantage of termination handlers at all:

 BOOL Funcarama1() { HANDLE hFile = INVALID_HANDLE_VALUE; PVOID pvBuf = NULL; DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { return(FALSE); } pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (pvBuf == NULL) { CloseHandle(hFile); return(FALSE); } fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL); if (!fOk || (dwNumBytesRead == 0)) { VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle(hFile); return(FALSE); } // Do some calculation on the data.     // Clean up all the resources. VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle(hFile); return(TRUE); } 

All the error checking in Funcarama1 makes the function difficult to read, which also makes the function difficult to understand, maintain, and modify.

Funcarama2

Of course, it's possible to rewrite Funcarama1 so that it is a little cleaner and easier to understand:

 BOOL Funcarama2() { HANDLE hFile = INVALID_HANDLE_VALUE; PVOID pvBuf = NULL; DWORD dwNumBytesRead; BOOL fOk, fSuccess = FALSE; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile != INVALID_HANDLE_VALUE) { pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (pvBuf != NULL) { fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL); if (fOk && (dwNumBytesRead != 0)) { // Do some calculation on the data.              fSuccess = TRUE; } } VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); } CloseHandle(hFile); return(fSuccess); } 

Although easier to understand than Funcarama1, Funcarama2 is still difficult to modify and maintain. Also, the indentation level gets to be pretty extreme as more conditional statements are added; with such a rewrite, you soon end up writing code on the far right of your screen and wrapping statements after every five characters!

Funcarama3

Let's rewrite the first version, Funcarama1, to take advantage of an SEH termination handler:

 DWORD Funcarama3() { // IMPORTANT: Initialize all variables to assume failure. HANDLE hFile = INVALID_HANDLE_VALUE; PVOID pvBuf = NULL; _ _try { DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { return(FALSE); } pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (pvBuf == NULL) { return(FALSE); } fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL); if (!fOk || (dwNumBytesRead != 1024)) { return(FALSE); } // Do some calculation on the data.        } _ _finally { // Clean up all the resources. if (pvBuf != NULL) VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile); } // Continue processing. return(TRUE); } 

The real virtue of the Funcarama3 version is that all of the function's cleanup code is localized in one place and one place only: the finally block. If we ever need to add some additional code to this function, we can simply add a single cleanup line in the finally block—we won't have to go back to every possible location of failure and add our cleanup line to each failure location.

Funcarama4: The Final Frontier

The real problem with the Funcarama3 version is the overhead. As I mentioned after the discussion of Funcenstein4, you really should avoid putting return statements into a try block as much as possible.

To help make such avoidance easier, Microsoft added another keyword, _ _leave, to its C/C++ compiler. Here is the Funcarama4 version, which takes advantage of the _ _leave keyword:

 DWORD Funcarama4() { // IMPORTANT: Initialize all variables to assume failure. HANDLE hFile = INVALID_HANDLE_VALUE; PVOID pvBuf = NULL; // Assume that the function will not execute successfully. BOOL fFunctionOk = FALSE; _ _try { DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { _ _leave; } pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (pvBuf == NULL) { _ _leave; } fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL); if (!fOk || (dwNumBytesRead == 0)) { _ _leave; } // Do some calculation on the data.        // Indicate that the entire function executed successfully. fFunctionOk = TRUE; } _ _finally { // Clean up all the resources. if (pvBuf != NULL) VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile); } // Continue processing. return(fFunctionOk); } 

The use of the _ _leave keyword in the try block causes a jump to the end of the try block. You can think of it as jumping to the try block's closing brace. Because the flow of control will exit naturally from the try block and enter the finally block, no overhead is incurred. However, it was necessary to introduce a new Boolean variable, fFunctionOk, to indicate the success or failure of the function. That's a relatively small price to pay.

When designing your functions to take advantage of termination handlers in this way, remember to initialize all of your resource handles to invalid values before entering your try block. Then, in the finally block, you can check to see which resources have been allocated successfully so that you'll know which ones to free. Another popular method for tracking which resources will need to be freed is to set a flag when a resource allocation is successful. Then the code in the finally block can examine the state of the flag to determine whether the resource needs freeing.

Notes About the finally Block

So far we have explicitly identified two scenarios that force the finally block to be executed:

  • Normal flow of control from the try block into the finally block
  • Local unwind: premature exit from the try block (goto, longjump, continue, break, return, and so on) forcing control to the finally block

A third scenario—a global unwind—occurred without explicit identification as such in the Funcfurter1 function we saw earlier in the chapter. Inside the try block of this function was a call to the Funcinator function. If the Funcinator function caused a memory access violation, a global unwind caused Funcfurter1's finally block to execute. We'll look at global unwinding in greater detail in the next chapter.

Code in a finally block always starts executing as a result of one of these three situations. To determine which of the three possibilities caused the finally block to execute, you can call the intrinsic function 1 AbnormalTermination:

 BOOL AbnormalTermination(); 

This intrinsic function can be called only from inside a finally block and returns a Boolean value indicating whether the try block associated with the finally block was exited prematurely. In other words, if the flow of control leaves the try block and naturally enters the finally block, AbnormalTermination will return FALSE. If the flow of control exits the try block abnormally—usually because a local unwind has been caused by a goto, return, break, or continue statement or because a global unwind has been caused by a memory access violation or another exception—a call to AbnormalTermination will return TRUE. It is impossible to determine whether a finally block is executing because of a global or a local unwind. This is usually not a problem because you have, of course, avoided writing code that performs local unwinds.

Funcfurter2

Here is Funcfurter2, which demonstrates use of the AbnormalTermination intrinsic function:

 DWORD Funcfurter2() { DWORD dwTemp; // 1. Do any processing here.     _ _try { // 2. Request permission to access // protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); dwTemp = Funcinator(g_dwProtectedData); } _ _finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); if (!AbnormalTermination()) { // No errors occurred in the try block, and // control flowed naturally from try into finally.           } else { // Something caused an exception, and // because there is no code in the try block // that would cause a premature exit, we must // be executing in the finally block // because of a global unwind. // If there were a goto in the try block, // we wouldn't know how we got here.           } } // 4. Continue processing. return(dwTemp); } 

Now that you know how to write termination handlers, you'll see that they can be even more useful and important when we look at exception filters and exception handlers in the next chapter. Before we move on, let's review the reasons for using termination handlers:

  • They simplify error processing because all cleanup is in one location and is guaranteed to execute.
  • They improve program readability.
  • They make code easier to maintain.
  • They have minimal speed and size overhead if used correctly.

The SEH Termination Sample Application

The SEHTerm application,"23 SEHTerm.exe" (listed in Figure 23-1), demonstrates how termination handlers work. The source code and resource files for the application are in the 23-SEHTerm directory on the companion CD-ROM.

When you run the application, the primary thread enters a try block. Inside this try block, the following message box is displayed.

This message box asks whether you want the program to access an invalid byte in memory. (Most applications aren't as considerate as this; they usually just access invalid memory without asking.) Let's examine what happens if you click on the Yes button. In this case, the thread attempts to write a 5 to memory address NULL. Writing to address NULL always causes an access violation exception. When the thread raises an access violation, the system displays the message box shown below on Windows 98.

On Windows 2000, the message box shown here is displayed.

click to view at full size.

If you now click on the Close button (in Windows 98) or the OK button (in Windows 2000), the process will be terminated. However, there is a finally block in the source code, so the finally block executes before the process terminates. The finally block displays this message box.

The finally block is executing because its associated try block exited abnormally. When this message box is dismissed, the process does, in fact, terminate.

OK, now let's run the application again. This time, however, let's click on the No button so that we do not attempt to access invalid memory. When you click No, the thread naturally flows out of the try block and enters the finally block. Again, the finally block displays a message box.

Notice, however, that this time the message box indicates that the try block exited normally. When we dismiss this message box, the thread leaves the finally block and displays one last message box.

When this message box is dismissed, the process terminates naturally, because WinMain returns. Notice that you do not see this last message box when the process is terminated because of an access violation.

Figure 23-1. The SEHTerm sample application

SEHTerm.cpp

 /****************************************************************************** Module: SEHTerm.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\CmnHdr.h" /* See Appendix A. */ #include <tchar.h> /////////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) { _ _try { int n = MessageBox(NULL, TEXT("Perform invalid memory access?"), TEXT("SEHTerm: In try block"), MB_YESNO); if (n == IDYES) { * (PBYTE) NULL = 5; // This causes an access violation. } } _ _finally { PCTSTR psz = AbnormalTermination() ? TEXT("Abnormal termination") : TEXT("Normal termination"); MessageBox(NULL, psz, TEXT("SEHTerm: In finally block"), MB_OK); } MessageBox(NULL, TEXT("Normal process termination"), TEXT("SEHTerm: After finally block"), MB_OK); return(0); } //////////////////////////////// End of File ///////////////////////////////// 



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