include excepts.a or include ucrlib.aThe excepts.a include file exports several symbols. The UCR Standard Library prefaces all "private" names with a dollar sign ("$"). You should not call any routine in this package that begins with this symbol unless otherwise advised. To avoid name conflicts, you should not define any symbols in your programs that begin with a dollar sign ("$"). Note that future versions of the stdlib (that remain compatible with this release) may change "private" names. To remain compatible with future releases, you must not refer to these "private" names within your programs.
Source code appearing in this chapter is current as of Version Two, Release 40. There may be minor changes between this source code and the current release.
The Standard Library supports many different exceptions. Because this list is changing rapidly, you should consult the "excepts.a" file. This file enumerates all the possible exceptions. Examples of exceptions the stdlib package does support include:
The standard supported list of hardware exceptions include the INT 00 (divide errors), INT 04 (overflow), INT 05 (bounds violation), and INT 06 (invalid opcode). Many of the standard library routines have an optional version that raise exceptions on various errors. Finally, you can define your own exceptions and raise them if an error occurs.
To use the exception handling package you must call the InitExcept routine to initialize the package. This patches the system's interrupt vectors for hardware trap support and sets up other internal variables the exception handling package uses. After calling InitExcept you can use TRY..ENDTRY blocks to capture exceptions in your program. After you are through processing exceptions, and certainly before your program terminates, you must call CleanUpEx to restore the system's interrupt vector table (failure to do so may crash the system after your program terminates.
To protect a sequence of statements you use the TRY..EXCEPTION..ENDTRY statements. A typical try..endtry block takes the following form:
InitExcept . . . try . . . <Code that may generate an exception> . . . Exception $Break ;Handle Control-C exception print "You Pressed control-C",nl Exception $InsuffMem print "MALLOC error occured- insufficient memory",nl . . . endtry . . . <Code that is not protected by the above try..endtry block> . . . CleanUpExThis code begins by calling the stdlib InitExcept routine. You must always call InitExcept before attempting to use try..endtry blocks in your program. If you do not do this, the exceptions package will only be able to handle user software generated exceptions; it will not be able to process exceptions normally supported by the stdlib package. After calling InitExcept the stdlib package will properly process exceptions within a try..endtry block.
The InitExcept routine installs a default exception handler. Therefore, if an exception occurs outside a try..endtry block, the default exception handler will execute. This default exception handler print "Unclaimed Exception" along with the exception number and then aborts the program. You should handle all exceptions within your program. As a general rule, if the default handler executes then you are not using the exceptions package properly.
Immediately following the try block are the statements you want to protect if an exception occurs. After executing the try statement, your program executes the block of statements immediately following try up to the first "exception" or "endtry" statement. If an exception does not occur while executing this "try block," then program control transfers to the first statement following the endtry statement. Note that your program will skip all the exception blocks if an exception does not occur.
If an exception occurs during the execution of the try block, then the program loads the AX register with an exception number and jumps to the first exception statement in the try..endtry block. As a general rule the exception statements will have a single parameter specifying the exception number for that particular handler. The exception statement will compare the value in AX against the exception statement's parameter. If they match, then the program will execute that particular exception handler. If they do not match, the try..endtry block will seek the next exception statement (in that try..endtry block) and compare AX against it's exception number.
The try..endtry block repeats this operation until it matches an exception statement, it encounters an exception statement without an exception constant, or it encounters the endtry statement. An exception statement without a corresponding exception constant, if present, must be the last exception handler in a try..endtry block. That is, there must not be any additional exception statements before the corresponding endtry. This is the default exception handler. If none of the previous exception handlers took care of the exception, the try..endtry block executes the default handler. The default handler can determine the source of the exception via the exception number in the AX register.
If, while seeking an appropriate exception handler, the try..endtry block hits the endtry statement, the stdlib exception handling package transfers control to an enclosing try..endtry block and searches through its exception list. This continues until the system finds an appropriate handler or it encounters the system default exception handler.
Consider the previous example. In the code protected by the try..endtry block there are two exception handlers: one that handles the $Break (control-C) event and one that handles an out of memory event ($InsuffMem). Suppose a overflow exception ($Overflow) occurs. Since this try..endtry block does not provide a default exception handler, it would pass control to an enclosing try..endtry block. There is always at least one enclosing try..endtry block- the default exception handler that stdlib provides.
Consider the following example. This code asks the user to input an integer value. It calls gets to read a line from the user and then stoi (string to integer) to convert this text to a numeric value. Stoi raises a conversion exception if the string is not a proper integer value. The try..endtry block catches this exception, tells the user to reenter the value, and sets up the loop control variable to force a repeat of the input operation.
This code also adds 1000 to the value the user inputs and then checks for signed integer overflow via the INTO instruction. Note, however, that the try..endtry block enclosing the INTO instruction does not handle the $Overflow exception. Therefore, should overflow occur the innermost try..endtry block transfers control to the enclosing try..endtry block to handle the overflow exception. Should any other exception occur, the system default exception handler would handle the exception.
InitExcept . . . try NumInLoop: print "Enter an integer value:" mov GoodInput, 1 try lesi InputLine gets stoi add ax, 1000 into Exception $Conversion print nl,"Illegal value, please reenter",nl mov GoodInput,0 endtry cmp GoodInput, 1 jne NumInLoop Exception $Overflow print "The number was too large, using default value",nl mov ax, 1000 endtry . . . <Code that is not protected by the above try..endtry block> . . . CleanUpExNote that try..endtry nesting is dynamic, not lexical. That is, the try..endtry blocks do not need to be physically nested in your source code. Instead, execution of a second try statement before executing the endtry associated with the first try causes the nesting to occur. The following code sequence is semantically identical to the above:
Try2 proc try lesi InputLine gets stoi add ax, 1000 into Exception $Conversion print nl,"Illegal value, please reenter",nl mov GoodInput,0 endtry ret Try2 endp . . . InitExcept . . . try NumInLoop: print "Enter an integer value:" mov GoodInput, 1 call Try2 cmp GoodInput, 1 jne NumInLoop Exception $Overflow print "The number was too large, using default value",nl mov ax, 1000 endtry . . . CleanUpExAlthough you can next a try..endtry block within the try section, you cannot next a try..endtry block within an exception handler. The TRY and ENDTRY statements will emit an error if you lexically nest one try..endtry block within an exception handler. You can, however, dynamically nest a try..endtry block within an exception handler. That is, if you call a procedure from an exception handler and that procedure has a try..endtry block in it, things should work okay (this has not had exhaustive testing, though).
The try..endtry blocks do not allow resumption of the failed code. Whenever an exception occurs the system peels the stack back to the point it was at when it executed the corresponding try statement. This means that it is impossible to return from any subroutines called inside the try..endtry block or recover any data pushed onto the stack inside the try..endtry block. Keep this in mind when using try..endtry blocks.
Warning: since TRY pushes data onto the stack that ENDTRY pops, should should not, under any circumstances, jump into the middle of a try..endtry block from outside that block or jump out of a try..endtry block. Doing so will corrupt the exception handling system and the stack.
The standard library reserves exception codes 0..63 for hardware traps, interrupts, and exceptions; it reserves exception codes 64..127 for system (stdlib) exceptions. Exception codes 128 and up are available for user-defined exceptions (see the Raise statement later).
The $CritErr exception is a special case. DOS provides a workable, if not suitable, default exception handler for critical errors (this handler prints the "Abort, Retry, Ignore, or Fail" message. By default, the InitExcept statement does not replace the DOS default handler for critical errors. If you really want to take control of this handler, see the sections on InitExcept and InitEx24.
The constants listed above contain a "$" prefix to avoid name space pollution (that is, to avoid conflicts with names in your programs). This names should remain fixed despite the earlier warning about using names that begin with a "$" symbol.
You must not disturb this exception frame. It is the link between the current try..endtry exception handler and any enclosing try..endtry block (possibly the system default handler).
Executing the EndTry statement restores SS:SP so that it points at this exception frame and removes this exception frame from the stack. Note that any data pushed on the stack within the try..endtry block is lost at this point.
InitExcept . . . try . . . <Code that may produce an exception> . . . <presumably, some exception handlers go here> . . . endtry . . . CleanUpEx
Int 24h contains a reasonable default handler. Patching into the Int 24h interrupt vector would disable this default handler and force the user to supply their own. Since the user may not want to supply their own Int 24h handler, requiring a separate call to patch into Int 24h saves them the hassle. The other hardware traps provide default handlers that are no better than the one provided by the exceptions package. This is why they do not require any special handling. As a general rule, you should never call InitEx24 without first calling the InitExcepts routine.
The CleanUpEx statement restores the 80x86's interrupt vectors to their state prior to the InitExcept and InitEx24 calls. If you call InitExcept (and possibly InitEx24) you must call CleanUpEx before your program terminates, but after existing the last try..endtry block in your program. Failure to terminate your program without calling CleanUpEx may leave the system in an unstable state that will require a reboot (since you will not have restored the Int 0, Int 4, Int 5, and Int 6 vectors). Note that DOS automatically restores the Int 23h and Int 24h vectors when the program quits, although CleanUpEx does this as well.
InitExcept InitEx24 . . . try . . . <Code that may produce an exception> . . . <presumably, some exception handlers go here> . . . endtry . . . CleanUpEx
InitExcept $CritErr . . . try . . . <Code that may produce an exception> . . . <presumably, some exception handlers go here> . . . endtry . . . CleanUpEx
EnableExcept enables certain exceptions by storing a one into the internal $XEnabled variable. This is the default setting (i.e., InitExcept initializes this variable to one).
DisableExcept disables certain exceptions by writing a zero to the internal $XEnabled variable. Calling CleanUpEx also sets this variable to zero.
The GetXEnabled statement returns the current value of the $XEnabled variable in the AX register. Your own programs can make this call to fetch $XEnabled to determine if it is okay to raise an exception. By convention, your program should not raise any exceptions if this variable contains zero.
Note that the status of the $XEnabled variable does not affect the hardware or DOS traps since they are unaware of this variable. Therefore, if one of these traps occur, you must be prepared to handle it even if the $XEnabled variable contains zero. All standard library routines that raise exceptions respect this flag. As a general rule, if you write code that raises exceptions, you should test the value of this flag before actually raising the exception as well. Procedures that do not raise exceptions because $XEnabled is zero should attempt to return an error status to the caller using some other mechanism.
InitExcept InitEx24 . . . try . . . <Code that may produce an exception> . . . DisableExcept . . . <Code that only generates hardware exceptions> . . . EnableExcept . . . <Back to handling all exceptions> . . . Get$XEnabled cmp ax, 1 jne SkipRaise mov ax, 1234 ;User defined exception #. Raise SkipRaise: . . . <presumably, some exception handlers go here> . . . endtry . . . CleanUpEx
As a general rule, you should test the state of the $XEnabled flag before raising an exception. (see the GetXEnabled, EnableExcept, and DisableExcept routines above). Many programs may termporarily disable exceptions by setting this flag to zero. You should not raise any exceptions while this flag is zero.
Raise is not a subroutine call. Once control transfers to an exception handler, it never returns to the statement after the Raise (indeed, Raise is implemented as a jmp, not a call). Remember, there is no implicit way to resume code that has raised an exception.
InitExcept InitEx24 . . . try . . . <Code that may produce an exception> . . . DisableExcept . . . <Code that only generates hardware exceptions> . . . EnableExcept . . . <Back to handling all exceptions> . . . Get$XEnabled cmp ax, 1 jne SkipRaise mov ax, 1234 ;User defined exception #. Raise SkipRaise: . . . <presumably, some exception handlers go here> . . . endtry . . . CleanUpEx
raise $Conversion . . . raise MyOwnException
This is especially important if you've trapped a specific exception and you want to call the handler for that same exception in the enclosing block. If you were to simply reraise the exception with the Raise statement, you'd call the same handler and wind up generating an infinite loop.
The solution is to use the PassExcept call. The semantics of PassException are identical to Raise except that PassExcept skips the exception handlers in the current try..endtry block and transfers control to the enclosing block (if no explicit try..endtry block exists in your code, PassExcept transfers control to the system default exception handler).
InitExcept InitEx24 . . . try ;Enclosing try..endtry block. try ;Current try..endtry block. . . . <Code that may produce an exception> . . . exception $Break <Code that handles this exception> mov ax, 1234 ;Pass control to an enclosing PassExcept ; try..except block. <presumably, some other exception handlers go here> . . . endtry ;End of nested try..endtry block. . . . exception 1234 ;The user defined exception we . ; pass from the try..endtry block <Appropriate 1234 handler> ; above. . . . endtry ;End of outside try..endtry block. . . . CleanUpEx
PassExcept $Conversion . . . PassExcept MyOwnException
exception $Break exception MyException exception 256 exceptionIf an exception occurs in the body of a try..endtry block, the system transfers control to the first exception statement in the try..endtry block. The exception statement compares the value in AX (the exception number) against the parameter of the exception statement. If the value in AX is equal to the exception statement's parameter, program execution continues with the statement immediately following the exception statement. If the value in AX is not equal to the exception statement's parameter, control transfers to the next exception statement in the try..endtry block and this process repeats.
If the search for a matching exception encounters the generic exception statement (i.e., an exception statement without any parameters), then that exception handler takes control. The generic exception handler can determine the source of the exception by examining the exception number in the AX register.
If the search for an exception handler fails to find a matching exception or a generic exception handler (i.e., the search encounters the endtry statement), then the exception processing system searches for the current exception in the enclosing try..except block. If no such block exists, control transfers to the system default exception handler that prints a brief message and terminates the program.
If the exception handling system locates an appropriate exception handler, execution continues with the body of that exception handler (the code immediately following the exception statement). Execution continues until the exception handler encounters the next exception statement or the endtry statement. At that point the system removes the current exception frame from the stack (along with any data pushed on the stack after the exception frame) and executes the first statement following the endtry statement.
InitExcept InitEx24 . . . try ;Enclosing try..endtry block. try ;Current try..endtry block. . . . <Code that may produce an exception> . . . exception $Break <Code that handles this exception> mov ax, 1234 ;Pass control to an enclosing PassExcept ; try..except block. <presumably, some other exception handlers go here> . . . endtry ;End of nested try..endtry block. . . . exception 1234 ;The user defined exception we . ; pass from the try..endtry block <Appropriate 1234 handler> ; above. . . . exception ;Generic exception handler . . . endtry ;End of outside try..endtry block. . . . CleanUpEx
There are two possibilities for handling critical error exceptions: write your own INT 24h interrupt service routine (arguably the best solution) or write an exception handler within the try..endtry block. Writing your own Int 24h ISR is beyond the scope of this document. See a text such as "The Art of Assembly Language Programming" for more details on writing Int 24h ISRs.
This section will discuss the other alternative, writing your own exception handler in a try..endtry block. Given the nature of the Int 24h exception, there are some special concerns for those writing the $CritErr handler.
The first thing to note is that the exception dispatching mechanism passes additional information to the critical error handler. In particular, the ES:DI register pair points at a data structure that contains the following information:
DOS pushed these values onto the stack upon initial entry. By looking at these values you can determine the type of DOS call (using the value in AH) and any necessary parameters appearing in registers for that call.
Since you have access to the return address (CS:IP in the structure above), you can return control to the program at the point of the offending DOS call and resume execution at that point. This makes the Critical Error handler somewhat special since it is possible to resume execution unlike most other exception handlers.
You can also call DOS again, but be aware that if DOS generates another critical error there will be a reentrant call to your handler that will disturb the register values above (in particular, the return address values).
A typical Critical Error Exception handler should ask the user if they want to continue program execution ignoring the error, retry the operation, return a DOS failure code to the program, or quit the program. To retry the operation, you would save the application return address, load the registers with the values from the structure above, and then reissue the DOS call (int 21h). To ignore the error, you would simply return to the application (via the CS:IP address in the structure above) with the carry flag clear and other appropriate return values in the registers. To fail, you would return to the program (through CS:IP above) with the carry flag set and an appropriate error code in AX. To abort the program, you should call CleanUpEx to restore the system interrupt vectors and then return control to DOS (e.g., by using the ExitPgm macro).
Note that the exception handling system makes a quick call to DOS in the internal Int 24h handler. This is because DOS is unstable until someone makes a call to DOS with a function number greater than 12. The internal int 24h ISR call DOS and request the current state of the break flag in order to get DOS into a consistent state. This should have zero impact on your exception handler.