In my article, "Exception Handlers and Windows Applications" (Dr. Dobb's Sourcebook of Windows Programming, Fall 1994), I discussed issues relative to Windows exception handlers, including the System VM, DPMI, and numerous protected-mode concepts such as selectors. In doing so, I presented TrapMan, a Windows debugging tool for analyzing exceptions in Windows applications. In this article, I'll enhance TrapMan by adding features to the trap handlers for displaying exception registers, dumping the exception stack, identifying the faulting application, and more.

A (usually) nonfatal exception is Interrupt 11 (Trap B) or Segment Not Present. The Windows kernel processes this message when demand loading segments of Windows applications. TrapMan watches these segments because it can be useful when you need to wait until a segment is loaded to set a breakpoint for debugging. It also gives you some idea of how often Windows environments are processing exceptions under the covers, even in small applications. Note that Interrupt 11 is very different from Interrupt 14 (Page Fault). Page Faults are handled by WIN386, while Segment Not Present faults are handled by KRNL386.EXE and friends. I will not discuss Page Faults here.

TrapMan is a Windows application that should run in any protected-mode version of the 16-bit Windows environment, including Win-OS/2 2.1 and 2.11. I've even run it under NT's WOW, the multithreaded DOS-box subsystem for 16-bit apps.

Additionally, if you wish to debug a currently faulting application caught in one of TrapMan's handlers, you will need to be running with a debugger capable of processing unowned Int 3hs in code, as Trapman uses the Intel INT3 instruction to return control to a waiting debugger. I prefer Nu-Mega's (Nashua, NH) Soft-Ice for Windows for debugging DOS-based versions of the Windows environment and the OS/2 kernel debugger for debugging OS/2-based versions (I use both on almost a daily basis). Unlike some debuggers, both of these can handle an Int 3h instruction that they themselves did not place in the code.

Intel documentation often refers to exceptions as "interrupts," Windows refers to them as "faults," and OS/2, as "traps." Interrupt is followed by a decimal number, trap, by a hex number, and fault is usually preceded by the name of the exception. In other words, Interrupt 13, General Protection Fault, and Trap D are all the same thing when discussing exceptions. For this article, I'll generally use the Windows versions of these names to avoid questions about the base of numbers given.

I developed TrapMan with Microsoft C 6.x, the Windows 3.1 SDK, and a MASM 5.1-compatible assembler. It will run under Windows 3.0 but requires COMMDLG for its SAVEAS and OPEN dialogs. TrapMan will show you how to use DPMI calls to replace the default Windows and Win-OS/2 handlers in order to provide an application-specific level of depth in debugging information while running under retail Windows or Win-OS/2. All of the source code, including related files and executables, is available electronically; see "Availability," page 3.

Fatal exceptions such as Stack Faults generally cause the operating system to terminate the task producing the exception. A nonfatal exception permits the task to continue at the instruction causing the fault after the operating system has processed the fault so that the current instruction will no longer cause an exception. An example would be a Segment Not Present fault. It is possible to write an exception handler in C, as in Listing One. This handler simply calls the Windows API DebugBreak() to interrupt to a waiting debugger (if available), and then calls FatalExit() to exit the faulting task. Notice that the handler does not attempt any access to program data. You can see why by looking at the mixed listing file created by the compiler for the handler in Listing Two. Notice that no segment registers are set in this routine. While CS is set through the action of calling this handler, no other segment registers are valid. These segment registers are set during C initialization and are not normally changed during the "life" of a Windows program. If the handler attempted to access C data, the handler would very likely GPFault (as DS is a random, possibly completely invalid value), which would cause the handler to be called repeatedly. (For more details, see Windows Internals, by Matt Pietrek, Addison-Wesley, 1993.)

The HANDLER example in the Windows SDK shows one way to make sure DS is valid. HANDLER sets an interrupt handler and guarantees accessibility to DS by exporting the interrupt handler. You need to be sure that your C compiler is generating correct code for a Windows prolog so that the Windows loader will set DS correctly (or you may need to call MakeProcInstance() yourself to force DS to be correct).

The easier way to make sure DS is valid is simply not to use DS! This is the technique used in TrapMan's handlers, which store information that they need to access at exception time inside the handler code segments. In other words, TrapMan is self-modifying code (even though the changes are mostly data). As code segments are shared across multiple instances, any modifications made by the second or greater instances would modify the data for all instances, including the first; therefore, only one instance of TrapMan is permitted.

In order to make TrapMan's exception handlers as flexible as possible, I wrote the handlers in assembly language. The other modules (window functions and the like, the scaffolding of TrapMan) are written in C to make that code as uncomplicated as possible.

Lastly, TrapMan makes extensive use of 16-bit DPMI services to monitor exceptions, and will only work in those systems that supply a 16-bit DPMI host of at least the 0.90 level (as do all 16-bit versions of Windows and Win-OS/2). Of course, such techniques should also work in protected-mode DOS applications, provided a DPMI host meeting these requirements is available, but such applications will not be discussed in this article.

While Windows (and Win-OS/2) already supply their own exception handlers, these handlers do not help you gather information on the fault. In many cases, only the address of the faulting instruction is available. While helpful, a raw address is not very useful when taken out of context. Wouldn't it be nice to have registers, flags, or a stack dump--even without a debugger? TrapMan will help you do this. Is it dangerous to set exception handlers directly from an executable? The Windows 3.1 Guide to Programming states:

Because interrupts can occur at any time, not just during the execution of the application that is using the device, device interrupt-handling code must be in a fixed segment.

The danger is that the code or data needed for the exception handler might have been discarded. While Windows 386 Enhanced mode and OS/2 both support paging, their underlying Windows systems do not. Windows and Win-OS/2 use segment-level linear memory; code and data are loaded on a per segment basis (via Interrupt 11, Segment Not Present fault).

The Intel documentation specifically states that any two Contributory Exceptions (Divide By Zero, Segment Not Present, Stack Fault, or GP Fault) will generate a Double Fault, and the processor will enter shutdown mode. (See the i486 Processor Programmer's Manual, Table 9-4.) In other words, if an application GPFaults and the processor attempts to demand load the handler segment, a double fault would result.

If it really worries you, put your exception handlers in a DLL with FIXED segments as Microsoft requires. Another possibility is to page lock the memory via GlobalPageLock(), but this function is only available in Windows Enhanced mode and is not available in all versions of the Windows operating environment. I have left TrapMan's handlers in an application instead of a library for simplicity's sake, with the code PRELOAD and NONDISCARDABLE. (For a very readable discussion on memory-segment attributes like PRELOAD, refer to Pietrek.) You'll see sections of TrapMan's handlers where the handlers check for code movement (because application code is always MOVEABLE), but this isn't too time-consuming.

Should you use exception handlers all the time?

No. It is probably best to leave the current Windows handlers alone, especially if you are shipping a retail version of your application. The Windows handlers, while not useful for debugging, are generic and work well with all Windows applications.

If you are writing a debugging tool such as TrapMan, you will probably want to replace the Windows handlers. TrapMan will (at the user's option) either replace the Windows handler or hook it. Replacing a Windows exception handler means that only TrapMan's handler will be active. Hooking the Windows handler means that TrapMan will call the original Windows handler after first preprocessing the Windows exception. Of course, replacing a Windows or Win-OS/2 handler does not remove the handler from memory; the DPMI host simply calls us instead of them.

If you write a regular (nondebug) application, you must be careful to only install your handlers by user choice. It should then be perfectly okay to ship exception handlers in your application (in testing TrapMan, I've run into at least one mainstream Windows application that did so). However, I would certainly not leave them in by default.

I envision the following scenario: A user calls your support line to report a problem; the support team has the customer turn on exception handling and reproduce the problem. The customer ships you a file with exception information. You fix the bug! Easy, huh? Exception handling is not something you'd want to leave on all the time. If everybody replaced the Windows handlers unnecessarily, who knows what would happen?

One of the first things TrapMan does at startup is set the default values for this debugging session (see Listing Three). This routine sets up handlers for the exceptions our user wishes to watch. These values are currently hard-coded, but the code could easily be changed to read defaults from an .INI file. The options are as follows:

Save trap settings. Exiting TrapMan will cause this session's settings to become the default.

Nuke app. Trapping applications will be terminated. Don't turn this option off unless you are calling the Windows handlers, or a faulting application will fault endlessly as the trap handlers aren't recovering from the trap.

Call PrevHandler. Calls the handler of a fault that was active when TrapMan added its own handlers. This usually means the handlers in the Windows kernel will be called. Note that this is post-processing. TrapMan's handlers will have already processed the exception by the point at which we call the previous handler.

Break on fault. TrapMan will attempt to break to a debugger via Int 3h at fault time. Application fault registers are preserved except for CS:IP and SS:SP (which are available on the DPMI exception frame).

Beep on fault. Beeps to let you know that a fault has occurred. This reminds you to look at your debugger. If you're using the Break on Fault option, TrapMan will be unable to paint the edit control with the debug information until you've released your debugger with a GO command.

Intercept OutputDebugString(). Allows TrapMan to avoid the need for a serial connection to write debug information with OutputDebugString()--no more CANNOT WRITE TO DEVICE AUX messages! Note that this is a replacement and not a hook; the original Windows kernel routine is not called (although the original call will be restored if you uncheck this option from the menu).

Add CRLF to ODS() strings. Adding a carriage return/line feed to OutputDebugString() arguments improves readability in the edit control. If the arguments already have CRLFs, then this option is unnecessary and should not be used.

DebugBreak() on <PrntScrn>. This would be a nice hook into a debugger, but I haven't had time to implement it.

The handlers for exceptions that TrapMan will watch are also installed at this point. Set default handlers for GPFault, Stack Fault, Invalid Op Code Fault, and Divide By Zero. Additional exceptions can be handled at user request. All program options are set in standard fashion with SendMessage() using the appropriate WM_COMMAND for the option. TrapMan also sets two global variables here that are handles to the Trap and Options menus for use during WM_COMMAND processing, which requires access to TrapMan's menu to check and uncheck options. This WM_COMMAND processing is a standard method of simulating user menu input in Windows programs. It allows you to use the same logic to set internal variables from within the program as you use to process external user requests.

For debugging purposes, TrapMan also can launch a single application from the command line. For convenience, TrapMan uses standard C argument processing to extract these values (see Listing Four). This code may be specific to your C library startup source code. Please check your compiler for more information. The current implementation works with Microsoft C 6.x.

Using SendMessage() guarantees that our exception handlers are set before any application the user gave on the command line is launched (thus, any faults that occur on launch of the application are caught by TrapMan). You should do something similar in your application to ensure that your handlers are available as soon as needed. Remember that SendMessage() is processed through an immediate call to your window procedure, while PostMessage() messages are handled later.

As mentioned previously, handlers store information in their code segments that is needed during exception processing. You'll find TrapMan's SetVars() routine in Listing Five. Currently, TrapMan creates and stores a DS alias to our HANDLER code segment and also stores the DS value for TrapMan. Both of these values will be accessed via CS from within exception handlers.

Fatal exceptions include GPFault, Stack Fault, Invalid Op Code fault, and Divide by Zero. Note that the code in TrapMan's fatal exception handlers could be rewritten to take up less space, if necessary. You could assign specific entry points to each fatal exception and have them display exception-specific information (a text string, for example). The specific entry points could then jump to a generic handler for the rest of the exception. TrapMan's handlers are small enough that I felt that optimizing them would make them harder to understand, and only slightly more efficient.

Fatal exception handlers begin with a call to the TELLDEBUGGER macro (see Listing Eight). This macro is responsible for the majority of information output to the user. For the moment, I'll discuss the Invalid OpCode exception handler found in Listing Six. The TELLDEBUGGER macro first saves the processor flags and calls the SAVEREGS macro, which saves all general 16-bit registers except the SP, IP, and CS registers. The SP register will be preserved through the normal maintenance of the stack in the handler; the CS and IP registers are not saved due to their nature--CS can only be changed through RET/ JMP/CALL instructions and the like. One reason for using the SAVEREGS macro is to save the registers in a more understandable format than the PUSHA instruction, which saves the general-purpose registers in the order of AX, CX, DX, BX according to the Intel i486 programmer's guide. This macro saves ten (decimal) words on the stack. The registers are restored by a call to the UNSAVEREGS macro.

Next, the TELLDEBUGGER macro checks to see if TrapMan is to call the Windows MessageBeep() function to notify the user of an error. If the wBeepOnTrap flag is set, then MessageBeep() is called. After this, the trap message is displayed in the edit control. (This is done by calling our replacement procedure for the Windows OutputDebugString() API, which can be called regardless of whether or not we are currently replacing the OutputDebugString() API.) In this case, the message is "Trap 6!".

At this point, the TELLDEBUGGER macro isolates the exception to a task. This is done through a call to GetCurrentTask(). The GetCurrentTask() API returns a Task Data Base (TDB). We'll extract the Module Data Base (MDB) from the TDB and then extract the fully qualified path to the executable, which is then displayed in the edit control. (See Undocumented Windows, by Andrew Schulman et al.)

Next, the TELLDEBUGGER macro displays the DPMI exception frame in the edit control (through a call to _PrintOutFaultFrame()). Note that the argument to this procedure is a near pointer to the beginning of the fault frame (which is assumed to be on the stack and thus relative to SS).

At this point, the TELLDEBUGGER macro restores the application registers through a call to UNSAVEREGS in preparation for dumping the registers to the edit control. Of course, as the act of dumping the registers might destroy some of them, we do an immediate SAVEREGS before calling _PrintOutPointerData() to display the application stack. This procedure simply takes a far pointer (which must be valid!) that will be displayed in the edit control. We then call the UNSAVEREGS macro, restore the processor flags, and the TELLDEBUGGER macro is done.

This is the most complicated portion of the handler; at this point only a few things remain to be done. For starters, the macro BREAKIFUSERWANTS is called. This macro will result in an Int 3h instruction (to break to a waiting debugger) if the Break On Fault option is checked. Then the value of Call Prev Handler is tested; if checked, the previous handler is called and our processing of this exception ends. Finally, if we did not need to call the previous handler, then TrapMan is responsible for bringing down the faulting task. It does this by a call to the NUKEAPPCHECK macro, which checks the state of the wNukeApp variable. The NUKEAPPCHECK macro will call FORCEAPPEXIT to bring down the current task if the wNukeApp variable is set. It does this by resetting the Faulting CS and Faulting IP fields of the DPMI exception frame to point to the ThisAppIsHistory() procedure in TrapMan. The task will be terminated when control is returned to DPMI.

Be careful! TrapMan will permit you to disable faulting-application termination without calling a previous handler (in other words, both the Call Prev Handler and Nuke App settings are unchecked). If an application faults while these settings are in effect, the faulting application will fault continuously. Someone must always process the exception! GPFaults (and most other exceptions) are restartable. Upon your handler's return to DPMI, Windows will begin execution at the current CS:IP, which will be whatever instruction is faulting, unless a handler resets it.

Nonfatal exceptions, which are normal in the course of program execution, can be thought of as "requests for work" by the operating system. Two normal requests for work would be Interrupt 14 (Page Fault) and Interrupt 11 (Segment Not Present). While 16-bit Windows doesn't really concern itself with page faults (which are the responsibility of a ring 0 VxD under DOS Windows or the OS/2 kernel under OS/2), Segment Not Present faults are frequent. Problems in demand loading segments lead to the infamous "SEGMENT LOAD FAILURE" message from the Windows kernel.

Nonfatal exceptions require different processing than fatal exceptions. TrapMan does not process these nonfatal exceptions itself. The hooks are only for informational purposes. The original (Windows or Win-OS/2) handler is always called, and all registers (including flags) must be preserved in our handlers for these exceptions.

As an example of how a nonfatal handler might be written, let's take a look at TrapMan's Segment Not Present fault (Trap B) handler (see Listing Seven). The first important section of code resets the base of the data alias to the HANDLER code segment. You ensure that the alias points to the same address (that is, has the same base address) as the HANDLER code segment by simply setting the base of the alias to that of the code segment. (This is only necessary because our code is in an executable and cannot be FIXED. If Windows decides to move the location of the HANDLER segment after SetVars() allocates the alias, then the alias will be out of sync with the original selector, and nothing good will result.)

Unlike other (fatal) fault handlers, this nonfatal fault handler does not begin with a call to the TELLDEBUGGER macro (Listing Eight). The TELLDEBUGGER macro dumps out information to the user such as fault location, stack, and DPMI-exception frame. In the case of a nonfatal exception, most of this information is not necessary, and only part of the TELLDEBUGGER function is used (in-line) in this procedure.

The most important portion of the handler is that it is responsible for copying the value of _Prev11 (a variable in TrapMan's auto DS) to the variable MyFarProc in HANDLER's CS. This is necessary because only CS will be valid when you call the previous Windows handler to process the Segment Not Present fault. All other registers will be those of the application causing the fault. None of this would be necessary at fault time if _Prev11 and other variables were CS variables and directly accessible to the handlers; see Listing Nine.

After all of this, TrapMan passes the nonfatal exception on to Windows by jumping to the contents of MyFarProc (which contains the address of the appropriate Windows or Win-OS/2 handler) with the jmp dword ptr cs:MyFarProc instruction.

There are two important points here:

All registers at this point (except CS:IP, which will be set by the JMP instruction) must be set at the values they contained when DPMI called TrapMan.

The stack pointer (SP) must point to the beginning of the exception frame from DPMI on entrance to the native handler (this is why you must JMP to the previous handler; a CALL FAR PTR would have pushed the return address of the handler onto the stack below the DPMI exception frame and the native handler would have failed).

Where to Go from Here?

You'll probably notice that Windows parameter-validation faults are caught by TrapMan as straight GPFaults. By default, debug information is taken, and the application causing the parameter-validation fault is terminated. For now, if you need to bypass parameter-validation errors (by passing them on to the Windows kernels), simply make sure that Call Prev Handler is checked. Windows or Win-OS/2 will then process the parameter validation normally.

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task.
However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Video

This month's Dr. Dobb's Journal

This month,
Dr. Dobb's Journal is devoted to mobile programming. We introduce you to Apple's new Swift programming language, discuss the perils of being the third-most-popular mobile platform, revisit SQLite on Android
, and much more!