|
As you may recall, the try..endtry statement surrounds a block of statements in order to capture any exceptions that occur during the execution of those statements. The system raises exceptions in one of three ways: through a hardware fault (such as a divide-by-zero error), through an operating system generated exception, or through the execution of the HLA raise statement. You can write an exception handler to intercept specific exceptions using the exception clause. The program in Listing 1-8 provides a typical example of the use of this statement.
Listing 1-8: TRY..ENDTRY Example.
program testBadInput; #include( "stdlib.hhf" ) static u: int32; begin testBadInput; try stdout.put( "Enter a signed integer:" ); stdin.get( u ); stdout.put( "You entered: ", u, nl ); exception( ex.ConversionError ) stdout.put( "Your input contained illegal characters" nl ); exception( ex.ValueOutOfRange ) stdout.put( "The value was too large" nl ); endtry; end testBadInput;
HLA refers to the statements between the try clause and the first exception clause as the protected statements. If an exception occurs within the protected statements, then the program will scan through each of the exceptions and compare the value of the current exception against the value in the parentheses after each of the exception clauses.[19] This exception value is simply a 32-bit value. The value in the parentheses after each exception clause, therefore, must be a 32-bit value. The HLA excepts.hhf header file predefines several exception constants. Other than it would be an incredibly bad style violation, you could substitute the numeric values for the two exception clauses above.
If the program scans through all the exception clauses in a try..endtry statement and does match the current exception value, then the program searches through the exception clauses of a dynamically nested try..endtry block in an attempt to find an appropriate exception handler. For example, consider the code in Listing 1-9.
Listing 1-9: Nested TRY..ENDTRY Statements.
program testBadInput2; #include( "stdlib.hhf" ) static u: int32; begin testBadInput2; try try stdout.put( "Enter a signed integer:" ); stdin.get( u ); stdout.put( "You entered: ", u, nl ); exception( ex.ConversionError ) stdout.put( "Your input contained illegal characters" nl ); endtry; stdout.put( "Input did not fail due to a value out of range" nl ); exception( ex.ValueOutOfRange ) stdout.put( "The value was too large" nl ); endtry; end testBadInput2;
In Listing 1-9 one try statement is nested inside another. During the execution of the stdin.get statement, if the user enters a value greater than four billion and some change, then stdin.get will raise the ex.ValueOutOfRange exception. When the HLA run-time system receives this exception, it first searches through all the exception clauses in the try..endtry statement immediately surrounding the statement that raised the exception (this would be the nested try..endtry in the example above). If the HLA run-time system fails to locate an exception handler for ex.ValueOutOfRange then it checks to see if the current try..endtry is nested inside another try..endtry (as is the case in Listing 1-9). If so, the HLA run-time system searches for the appropriate exception clause in the outer try..endtry statement. Within the try..endtry block appearing in Listing 1-9 the program finds an appropriate exception handler, so control transfers to the statements after the "exception( ex.ValueOutOfRange )" clause.
After leaving a try..endtry block, the HLA run-time system no longer considers that block active and will not search through its list of exceptions when the program raises an exception.[20] This allows you to handle the same exception differently in other parts of the program.
If two try..endtry statements handle the same exception, and one of the try..endtry blocks is nested inside the protected section of the other try..endtry statement, and the program raises an exception while executing in the innermost try..endtry sequence, then HLA transfers control directly to the exception handler provided by the innermost try..endtry block. HLA does not automatically transfer control to the exception handler provided by the outer try..endtry sequence.
In the previous example (Listing 1-9) the second try..endtry statement was statically nested inside the enclosing try..endtry statement.[21] As mentioned without comment earlier, if the most recently activated try..endtry statement does not handle a specific exception, the program will search through the exception clauses of any dynamically nesting try..endtry blocks. Dynamic nesting does not require the nested try..endtry block to physically appear within the enclosing try..endtry statement. Instead, control could transfer from inside the enclosing try..endtry protected block to some other point in the program. Execution of a try..endtry statement at that other point dynamically nests the two try statements. Although there are many ways to dynamically nest code, there is one method you are probably familiar with from your high level language experience: the procedure call. Later in this text, when you learn how to write procedures (functions) in assembly language, you should keep in mind that any call to a procedure within the protected section of a try..endtry block can create a dynamically nested try..endtry if the program executes a try..endtry within that procedure.
Whenever a program executes the try clause, it preserves the current exception environment and sets up the system to transfer control to the exception clauses within that try..endtry statement should an exception occur. If the program successfully completes the execution of a try..endtry protected block, the program restores the original exception environment and control transfers to the first statement beyond the endtry clause. This last step, restoring the execution environment, is very important. If the program skips this step, any future exceptions will transfer control to this try..endtry statement even though the program has already left the try..endtry block. Listing 1-10 demonstrates this problem.
Listing 1-10: Improperly Exiting a TRY..ENDTRY Statement.
program testBadInput4; #include( "stdlib.hhf" ) static input: int32; begin testBadInput4; // This forever loop repeats until the user enters // a good integer and the BREAK statement below // exits the loop. forever try stdout.put( "Enter an integer value: "); stdin.get( input ); stdout.put( "The first input value was: ", input, nl ); break; exception( ex.ValueOutOfRange ) stdout.put( "The value was too large, reenter." nl ); exception( ex.ConversionError ) stdout.put( "The input contained illegal characters, reenter." nl ); endtry; endfor; // Note that the following code is outside the loop and there // is no TRY..ENDTRY statement protecting this code. stdout.put( "Enter another number: " ); stdin.get( input ); stdout.put( "The new number is: ", input, nl ); end testBadInput4;
This example attempts to create a robust input system by putting a loop around the try..endtry statement and forcing the user to reenter the data if the stdin.get routine raises an exception (because of bad input data). While this is a good idea, there is a big problem with this implementation: The break statement immediately exits the forever..endfor loop without first restoring the exception environment. Therefore, when the program executes the second stdin.get statement, at the bottom of the program, the HLA exception handling code still thinks that it's inside the try..endtry block. If an exception occurs, HLA transfers control back into the try..endtry statement looking for an appropriate exception handler. Assuming the exception was ex.ValueOutOfRange or ex.ConversionError, the program in Listing 1-10 will print an appropriate error message and then force the user to reenter the first value. This isn't desirable.
Transferring control to the wrong try..endtry exception handlers is only part of the problem. Another big problem with the code in Listing 1-10 has to do with the way HLA preserves and restores the exception environment: specifically, HLA saves the old execution environment information in a special region of memory known as the stack. If you exit a try..endtry without restoring the exception environment, this leaves the old execution environment information on the stack, and this extra data could cause your program to malfunction.
Although this discussion makes it quite clear that a program should not exit from a try..endtry statement in the manner that Listing 1-10 uses, it would be nice if you could use a loop around a try..endtry block to force the reentry of bad data as this program attempts to do. To allow for this, HLA's try..endtry statement provides an unprotected section. Consider the code in Listing 1-11.
Listing 1-11: The TRY..ENDTRY UNPROTECTED Section.
program testBadInput5; #include( "stdlib.hhf" ) static input: int32; begin testBadInput5; // This forever loop repeats until the user enters // a good integer and the BREAK statement below // exits the loop. Note that the BREAK statement // appears in an UNPROTECTED section of the TRY..ENDTRY // statement. forever try stdout.put( "Enter an integer value: " ); stdin.get( input ); stdout.put( "The first input value was: ", input, nl ); unprotected break; exception( ex.ValueOutOfRange ) stdout.put( "The value was too large, reenter." nl ); exception( ex.ConversionError ) stdout.put( "The input contained illegal characters, reenter." nl ); endtry; endfor; // Note that the following code is outside the loop and there // is no TRY..ENDTRY statement protecting this code. stdout.put( "Enter another number: " ); stdin.get( input ); stdout.put( "The new number is: ", input, nl ); end testBadInput5;
Whenever the try..endtry statement hits the unprotected clause, it immediately restores the exception environment. As the phrase suggests, the execution of statements in the unprotected section is no longer protected by that try..endtry block (note, however, that any dynamically nesting try..endtry statements will still be active; unprotected only turns off the exception handling of the try..endtry statement containing the unprotected clause). Because the break statement in Listing 1-11 appears inside the unprotected section, it can safely transfer control out of the try..endtry block without "executing" the endtry because the program has already restored the former exception environment.
Note that the unprotected keyword must appear in the try..endtry statement immediately after the protected block. That is, it must precede all exception keywords.
If an exception occurs during the execution of a try..endtry sequence, HLA automatically restores the execution environment. Therefore, you may execute a break statement (or any other instruction that transfers control out of the try..endtry block) within an exception clause.
Because the program restores the exception environment upon encountering an unprotected block or an exception block, an exception that occurs within one of these areas immediately transfers control to the previous (dynamically nesting) active try..endtry sequence. If there is no nesting try..endtry sequence, the program aborts with an appropriate error message.
In a typical situation, you will use a try..endtry statement with a set of exception clauses that will handle all possible exceptions that can occur in the protected section of the try..endtry sequence. Often, it is important to ensure that a try..endtry statement handles all possible exceptions to prevent the program from prematurely aborting due to an unhandled exception. If you have written all the code in the protected section, you will know the exceptions it can raise so you can handle all possible exceptions. However, if you are calling a library routine (especially a third-party library routine), making a OS API call, or otherwise executing code that you have no control over, it may not be possible for you to anticipate all possible exceptions this code could raise (especially when considering past, present, and future versions of the code). If that code raises an exception for which you do not have an exception clause, this could cause your program to fail. Fortunately, HLA's try..endtry statement provides the anyexception clause that will automatically trap any exception the existing exception clauses do not handle.
The anyexception clause is similar to the exception clause except it does not require an exception number parameter (because it handles any exception). If the anyexception clause appears in a try..endtry statement with other exception sections, the anyexception section must be the last exception handler in the try..endtry statement. An anyexception section may be the only exception handler in a try..endtry statement.
If an otherwise unhandled exception transfers control to an anyexception section, the EAX register will contain the exception number. Your code in the anyexception block can test this value to determine the cause of the exception.
The try..endtry statement preserves about 16 bytes of data whenever you enter a try..endtry statement. Upon leaving the try..endtry block (or hitting the unprotected clause), the program restores the exception environment. As long as no exception occurs, the try..endtry statement does not affect the values of any registers upon entry to or upon exit from the try..endtry statement. However, this claim is not true if an exception occurs during the execution of the protected statements.
Upon entry into an exception clause the EAX register contains the exception number, but values of all other general purpose registers are undefined. Because the operating system may have raised the exception in response to a hardware error (and, therefore, has played around with the registers), you can't even assume that the general purpose registers contain whatever values they happened to contain at the point of the exception. The underlying code that HLA generates for exceptions is subject to change in different versions of the compiler, and certainly it changes across operating systems, so it is never a good idea to experimentally determine what values registers contain in an exception handler and depend upon those values in your code.
Because entry into an exception handler can scramble the register values, you must ensure that you reload important registers if the code following your endtry clause assumes that the registers contain certain values (i.e., values set in the protected section or values set prior to executing the try..endtry statement). Failure to do so will introduce some nasty defects into your program (and these defects may be very intermittent and difficult to detect because exceptions rarely occur and may not always destroy the value in a particular register). The following code fragment provides a typical example of this problem and its solution:
static sum: int32; . . . mov( 0, sum ); for( mov( 0, ebx ); ebx < 8; inc( ebx )) do push( ebx ); // Must preserve EBX in case there is an exception. forever try stdin.get.geti32(); unprotected break; exception( ex.ConversionError ) stdout.put( "Illegal input, please reenter value: " ); endtry; endfor; pop( ebx ); // Restore EBX's value. add( ebx, eax ); add( eax, sum ); endfor;
Because the HLA exception handling mechanism messes with the registers, and because exception handling is a relatively inefficient process, you should never use the try..endtry statement as a generic control structure (e.g., using it to simulate a switch/case statement by raising an integer exception value and using the exception clauses as the cases to process). Doing so will have a very negative impact on the performance of your program and may introduce subtle defects because exceptions scramble the registers.
For proper operation, the try..endtry statement assumes that you only use the EBP register to point at activation records (the chapter on procedures later in this book discusses activation records). By default, HLA programs automatically use EBP for this purpose; as long as you do not modify the value in EBP, your programs will automatically use EBP to maintain a pointer to the current activation record. If you attempt to use the EBP register as a general purpose register to hold values and compute arithmetic results, HLA's exception handling capabilities will no longer function properly (along with other possible problems). Therefore, you should never use the EBP register as a general purpose register. Of course, this same discussion applies to the ESP register.
[19]Note that HLA loads this value into the EAX register. So upon entry into an exception clause, EAX contains the exception number.
[20]Unless, of course, the program reenters the TRY..ENDTRY block via a loop or other control structure.
[21]"Statically nested" means that one statement is physically nested within another in the source code. When we say one statement is nested within another, this typically means that the statement is statically nested within the other statement.
|