Q
I want to implement an exception handler that can be
used as the default exception handler for each thread. The exception
handler should record two kinds of information: variable information
provided by the failing application and system information such as the
name of the routine where the exception occurred and a (symbolic) call
tree of how it got to that point in the code.
Ari Erev via the Internet
A
This is an excellent question, because to answer it I'll
have to cover a variety of topics that readers have expressed interest
in. In fact, there's so much to cover that I've split the answer into
two parts. This month, I'll set up a basic framework for intercepting
unhandled exceptions and writing a report file with some basic
information. I'll include some simple code for walking the stack on the
Intel platform, which is something I'm asked about often. Next month,
I'll go into the various types of debug information and describe what
they're used for. From there, I'll show you how to use IMAGEHLP.DLL to
access debug information and walk the stack in a portable manner.
The
first question that needs to be addressed in writing this code is how
to receive control when an unhandled exception occurs. By unhandled I
mean that none of the program's installed exception handlers (for
example, _try blocks in C++) have elected to handle the exception. The
exception is destined to go to the operating system, which will render
the death penalty to the errant process. In Windows®
3.x, you could request to be notified of certain exceptions that
occurred in any task in the system with the InterruptRegister API from
TOOLHELP.DLL. Diagnostic programs like Dr. Watson were written to sit in
the background and wait for "bad" exceptions to occur in other tasks.
When they occurred, the diagnostic program sprang into action and
recorded all sorts of information about the doomed task.
By design, Win32®
doesn't let you arbitrarily monitor exceptions in other processes. The
only exceptions your process can see are its own and exceptions for
which it is acting as a debugger. This latter case is how programs like
DRWTSN32.EXE and "just in time" debuggers work. Based upon the AeDebug
key in the registry, the system invokes a program (DRWTSN32.EXE, for
example) that attaches to a faulting task as a debugger. DRWTSN32.EXE
uses the magical powers of Win32 debuggers to probe the faulting process
and generate a report about the state of the process when it faulted.
Although
DRWTSN32.EXE and "just in time" debuggers are great for application
developers, users may not have them installed, and even if they do, the
information that something like DRWTSN32 provides may not be everything
they need to know. The solution is to dispense with the debugger
approach and grab the unhandled exception within your own code. With
this approach, you can tailor the exception report to include whatever
information you desire, and make that code a part of your application.
To
get back to the original question, the key to grabbing unhandled
exceptions is the SetUnhandledExceptionFilter API. Using this API, a
program can install a callback that will be invoked whenever a regular
exception handler (for example, a _try/_except block) doesn't handle an
exception. As I showed you in my January 1997 MSJ
article on structured exception handling, the callback is invoked as
part of KERNEL32's UnhandledExceptionFilter API. The parameter for your
callback function is a pointer to an EXCEPTION_POINTERS struct. The
information that can be obtained from this structure consists of
information about the exception and the CPU registers at the time of the
exception. As I'll soon show, there's quite a bit you can learn from
just this information.
Using
the SetUnhandledExceptionFilter API as the basis for what I needed to
do, I set out to design an exception reporting framework (see Figure 1).
The first criterion was that my framework shouldn't require any changes
to the application's source code. Likewise, it shouldn't require you to
haul around an additional DLL. This implies that the framework code
should be linked into your EXE. My second criterion was that the
framework should avoid using library calls from the application's
runtime library since the library's data might be trashed at the time of
an exception. Therefore, the framework uses only Win32 API calls and a
few "safe" C++ runtime library calls. For fancy output formatting, I
implemented a simple version of printf using the Win32 API. Finally,
because I'm somewhat of a programming masochist, I made the code
compilable as either ANSI or Unicode.
Fulfilling
the criterion of not requiring any source code changes was easy. I
defined a C++ class called MSJExceptionHandler. In the source file
containing the class's member functions, I also declared a single global
instance of
the class. By simply linking the resulting OBJ file into
my project, I've put the framework in place. I put the code that calls
SetUnhandledExceptionFilter in the class's
constructor. Since the single instance of the class is global in scope,
its constructor is called automatically when the program starts.
After
the MSJExceptionHandler constructor executes, you've got a callback
function, MSJExceptionHandler::MSJUnhandledExceptionFilter, that is
invoked whenever an unhandled exception occurs. The first order of
business in this function is to open up the file that the exception
report will be appended to. By default, the report file has the same
name as the executable file, but with an .RPT extension. If you don't
like this name, you can call MSJExceptionHandler::SetLogFileName to
override it. It's worth noting that I opened the file with the
FILE_FLAG_WRITE_
THROUGH attribute. In case the exception report generation code itself
blows up, at least the data already written should be safely on the
disk.
Assuming
the report file opened OK, the MSJUnhandledExceptionFilter method seeks
to the end of the file and calls the GenerateExceptionReport method.
GenerateExceptionReport encapsulates writing out whatever sort of
information you might want to the report file. Finally,
MSJUnhandledExceptionFilter closes the file and chains on to any
previously installed UnhandledException-
Filter functions.
Inside
the GenerateExceptionReport method, the code begins by printing a
banner and some basic information about the exception. First out of the
gate is the exception number, along with some descriptive text that
identifies the exception. Getting this text turned out to be an
interesting diversion. If you look at the end of the GetExceptionString
method, you'll see that the descriptive strings for each exception that
appear in the system's fault dialog are kept in the MessageTable
resource of NTDLL.DLL. Unfortunately, I wasn't able to get the
FormatMessage API to always give me the raw, unformatted strings,
despite my passing the FORMAT_MESSAGE_IGNORE_INSERTS flag. Thus, for
common exceptions, the GetExceptionString method returns the #define
name from WINBASE.H (for example, "ACCESS_VIOLATION"). If the exception
isn't a common one, my code uses FormatMessage on NTDLL's MessageTable
and hopes for the best.
After
the GenerateExceptionReport method emits the exception number and
description, the next step is to print out the faulting address. I first
print the linear address of the exception, which usually isn't that
helpful, but it's what you'd see in a debugger or the system fault
dialog. Following the linear address on the line is the logical address.
The logical address consists of the name of the EXE or DLL that the
linear address falls within, and the section number and offset within
that section. For example, the logical address
|