home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
OS/2 Shareware BBS: 10 Tools
/
10-Tools.zip
/
except2.zip
/
EXCEPT.INF
(
.txt
)
next >
Wrap
OS/2 Help File
|
1994-03-25
|
55KB
|
1,552 lines
ΓòÉΓòÉΓòÉ 1. Introduction ΓòÉΓòÉΓòÉ
C Set/2, C Set++ and OS/2 provide a slightly different approach to handling
program exception conditions from what you may be used to with C/2 and OS/2
Version 1.x. All programmers know from experience that programs do generate
abnormal conditions, like invalid storage accesses. The C Set family of
compilers and OS/2 provide ways to gracefully handle these situations.
ΓòÉΓòÉΓòÉ 2. Handling of Abnormal Conditions ΓòÉΓòÉΓòÉ
From the application programmer's perspective, both C Set and the operating
system have the capability of detecting and reporting various abnormal
conditions. You, the programmer, have several options once the condition is
reported. You can:
1. Ignore the condition
In this case, C Set and/or OS/2 will take the default action. Usually,
this means that your process is terminated immediately. The C Set library
will shut down without automatically flushing your file buffers, and no
cleanup of your process is done.
2. Terminate the process
You can provide a handler for the condition which cleans up after your
process (e.g. deletes work files), and then terminates. This is a little
cleaner than the default action, but not much.
3. Terminate the thread
If an abnormal condition occurs within one thread of your process, you can
clean up just that thread, and terminate it. This was very difficult to do
on OS/2 Version 1.x, but is straightforward on OS/2 Version 2.x. From the
your application's user's point of view, what was a hard failure on OS/2
Version 1.x has now become a soft failure.
4. Terminate a thread, and restart it
While handling an abnormal condition, you may elect to terminate the thread
in which it has occurred, and start a new thread. This is an extension of
the concept of just terminating one thread.
5. Continue a thread at a known point
You can continue the thread that had the abnormal condition from a known
point. This was often nearly impossible on OS/2 Version 1.x. C Set and
OS/2 make this relatively easy.
Abnormal conditions can be reported to you in two ways. You can choose which
you want to use, and even use them in combination. One option is to use the
operating system's exception handling interface. Exception handling is not
easy to deal with, and provides you a large amount of information which is
generally useless to a C or C++ programmer. The C Set application libraries
(the single and multithreading libraries) provide a much simpler interface
called signal handlers.
Terminology Note: The terms "signal" and "exception" are not interchangeable.
A signal exists only within the C language. An exception
is generated by OS/2, and may be used by the C Set library
to generate a signal. Additional confusion is caused by
the use of the word "exception" in C++. This article does
not deal with C++ exceptions.
ΓòÉΓòÉΓòÉ 3. Signal Handlers ΓòÉΓòÉΓòÉ
The American National Standard for Information Systems - Programming Language C
(the ANSI standard) specifies that the C language must support signal handlers,
although it does not require that they be called when the operating system
detects an error. The application libraries of C Set are ANSI compliant, and
support signal handlers. The C Set subsystem library (selected by the /Rn
compiler option) does not. The subsystem library requires that you do all your
exception handling using the exception handler interface described later in
this article.
You can use the library function raise() to generate a signal, but the
usefulness of this is marginal. Signal handlers are much more useful if they
can respond to the operating system exceptions, such as in the C Set
application libraries. In most cases, this provides all the functionality
required, and has the benefits of being simpler to write, and being portable to
other platforms.
C Set provides a number of different signals, to allow you to differentiate
among error conditions. The signals and their correspondence to OS/2
exceptions are described in the IBM C Set User's Guide. You can select which
ones you want to handle, and which ones you want the C Set library to handle.
Specify how you want a signal is to be handled with the signal() library
function. There are three choices:
SIG_DFL: Let the C library use its default handling. For most signals, this
means that your process is terminated, with a message giving the
reason. A dump of the machine state is made to file handle 2 if the
library assumes that the cause is an error in your code. This is a
good choice for signals you expect never to occur, such as SIGILL.
Note: You can capture the dump in a file by redirecting file handle
2 from the command line, or by using the dup2() function to attach
file handle 2 to a disk file. This works even for PM programs. In C
Set++, you also have the option of setting the file handle using the
_set_crt_msg_handle() function.
SIG_IGN: Ignore the condition. The C library will attempt to carry on with
your program as though nothing has happened. Due to limitations in
the in the underlying microprocessor, this is not possible with many
exceptions. In that case, the C library will treat the signal as
though SIG_DFL was the handler.
your own handler function: Your function is called. The function has no
limitations, and may call any library function. If you choose this
option, the handling for the signal is reset to SIG_DFL before your
function is called, to prevent recursion. It is up to you to set the
signal handler back to your function with a call to signal().
Your handler function may end in one of the following ways:
1. It may call exit() or abort(). This will terminate your process in the
same way that it would if you called these functions outside a signal
handler.
2. It may call _endthread(). This terminates the current thread in a
multithreaded program. The process continues to run, minus this thread.
Of course, you must take steps to keep your process running without it.
Thread 1 of your program is special; doing this on thread 1 of your
program is equivalent to calling exit().
3. You may call longjmp() if you have previously called the setjmp()
function in this thread. This allows you to continue the thread
execution at a known point. setjmp() saves the state of the thread in a
buffer. When you call longjmp(), the state of the thread is reset to
that in the setjmp buffer, forcing execution to restart at the call to
setjmp().
4. You may return from the function. This indicates that you wish to
restart as though the signal had not occurred, similar to using SIG_IGN.
The C library will terminate your process if this is not possible. In C
Set/2 and C Set++, SIG_IGN is equivalent to the following user signal
handler:
int myhandler(int x) {
signal(x,myhandler); /* re-register handler */
}
ΓòÉΓòÉΓòÉ 3.1. A Simple Signal Handler ΓòÉΓòÉΓòÉ
Here's a simple program that has a useful function. If you give it a pointer,
and a size, it returns the number of bytes that you can access without
generating a signal. In other words, it tells you how much storage you can
access without problems.
#include <signal.h> /* for the signal function */
#include <setjmp.h> /* for the setjmp and longjmp functions */
#include <stdio.h> /* for the printf call */
static void mysig(int sig); /* the signal handler prototype */
static jmp_buf jbuf; /* work area to save machine state for longjmp function */
int chkptr(void * ptr, /* pointer to storage to check */
int size) /* number of bytes to check */
{
void (* oldsig)(int); /* where to save the old signal handler */
volatile char c; /* volatile to insure access occurs */
int valid = 0; /* count of valid bytes */
char * p = ptr; /* to satisfy the type checking for p++ */
oldsig = signal(SIGSEGV,mysig); /* set the signal handler */
if (!setjmp(jbuf)) { /* provide a point for the signal handler */
/* to return to */
while (size--) /* scan the storage */
{
c = *p++; /* check the storage */
valid++; /* then bump the counter */
}
}
signal(SIGSEGV,oldsig); /* reset the signal handler */
return valid; /* return number of valid bytes */
}
static void mysig(int sig) {
printf("Detected invalid storage address\n");
longjmp(jbuf,1); /* restart the function at the setjmp() call */
/* without restarting the while() loop */
}
Assuming that there is a problem accessing the buffer, let's follow the flow of
the execution:
1. We enter chkptr() with a buffer pointer in ptr and the expected size in
size.
2. The program registers the signal handler mysig(), saving the original
signal handler in oldsig. Since we're going to restore the old signal
handler before we return, oldsig can be a local variable in chkptr().
3. We now put a mark on the wall, so we can get back to chkptr() if a signal
occurs. We do it using setjmp(). setjmp() returns 0 if it is called in
this way. It always returns a non-zero value when is is re-entered via a
longjmp() call.
4. We now sit in a loop, examining each byte of the buffer, and incrementing
valid for each byte that we successfully copy to the variable c.
There are two important concepts here:
o The program maintains the count separate from the pointer, since, in the
expression c = *p++;, p may be incremented either before or after the
store to c. The ANSI standard requires only that the increment be
complete when the statement is complete. Since we don't know when the
increment occurs relative to the buffer access, we can't use the
difference between p and ptr to calculate the number of valid bytes. If p
were incremented as a separate statement, either before or after the
assignment, this problem goes away.
o The variable c has been defined as volatile. This keyword tells the
compiler that loads and stores to the variable have side effects, and that
it is not to remove them. We are depending on such a side effect (the
occurrence of a signal when we move data from the buffer). If we omit the
volatile keyword, the C Set compilers can optimize the while loop out of
existence, breaking the function. The optimizer will probably generate
similar code for the following two code fragments, which is definitely not
desirable in this function:
Original Code Optimized Equivalent
ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
char c, *p; char c, *p;
int valid,size; int valid,size;
while (size--) p += size;
{ valid += size;
c = *p++; c = *p;
valid++;
}
5. Since we have assumed that we cannot access all of the buffer, at some
point in the loop, p points to a storage location to which our process does
not have access. This causes an OS/2 exception to occur, which the C Set
library translates into a SIGSEGV signal. Since we made mysig() the
handler for this signal, the C library calls it, after setting the signal
handler for SIGSEGV to SIG_DFL.
6. mysig() is entered, dumps out a message, and branches back to the setjmp()
call in chkptr(). From the application programmer's point of view, this
looks very much as though setjmp() has just returned, with a return code of
1.
Note: We didn't reset the signal handler in mysig(). It isn't necessary,
since we only expect this signal to occur once.
7. Since setjmp() has returned 1, we don't re-enter the while loop, but go
straight to the call to signal(), where we reset the signal handler for
SIGSEGV to wherever it was when we entered chkptr.
8. We then return the valid count.
Yes - it's that simple! You can, of course, dress it up, extend the function,
make it reentrant, and so on, but the basic design is there. Please note that
this simple function is not usable as shown in a multithreaded program. The
static variable jbuf makes it not reentrant, unless you provide some sequencing
controls (such as a semaphore). We'll convert it to a multithreaded version in
a later section.
This example shows one very important point. Your program can receive a signal
(like the SIGSEGV signal), recover, and keep going! The function chkptr()
attempted an invalid memory access, and lived to tell the tale. The only real
requirement on you is that you must provide a point for the signal handler to
go to when the signal occurs.
ΓòÉΓòÉΓòÉ 3.2. Extending the Signal Handling Model ΓòÉΓòÉΓòÉ
So far, I've discussed a fairly normal C signal handling structure. Any C
compiler worthy of the name can do what I've shown as long as there is only one
thread of execution. C Set and OS/2 show their strengths in multithreaded
programs and DLL's. The standard ANSI signal handling model starts to show its
shortcomings under these conditions, so C Set had to provide some extensions.
I'll discuss what happens in a multithreaded program first.
ΓòÉΓòÉΓòÉ 3.3. Signal Handling in Multithreaded Programs ΓòÉΓòÉΓòÉ
Multithreaded programs can be viewed as tightly coupled independent programs.
They are tightly coupled, inasmuch as they share the same memory space. They
are independent inasmuch as the machine state (registers, stack, and
instruction pointer) is unique to each thread. It made more sense in the C Set
library design to treat threads as separate signal handling environments. To
you, the application programmer, this means that a signal handler registered on
thread x will only get signals generated by thread x. This is different from
C/2, where a signal handler applies to all threads, no matter which thread
registers it. This will require some code changes if you port from C/2 to C
Set/2 or C Set++. The C Set library design group did not make this change
lightly; you don't fix what isn't broken. The rationale was as follows:
1. A function like chkptr(), which we just examined, is almost as easy to
write in the multithreaded environment as it is in the single threaded
environment. This is no small gain in functionality.
2. You can predict exactly what the signal handler is going to be when a
signal occurs. It doesn't matter if another thread changes its signal
handler. It can't affect your thread.
3. Independent signal handlers on each thread matched much better to the
underlying OS/2 exception handling. Indeed, the correspondance is almost
one-to-one. This significantly reduced the complexity of the C Set library
design, thereby improving its reliability.
So the selected design provides a different signal environment for each thread.
Nothing is completely free. This requires you to do things a little
differently when you register signal handlers.
1. When you start up a new thread (using the _beginthread() function), all its
signal handlers are set to SIG_DFL. If you want signal handling in that
thread, you must call signal() from that thread to register them.
2. There are three signals which can only occur on the first thread of your
process. These are SIGINT, SIGBREAK, and SIGTERM. To handle these, you
must register the signal handlers for them on thread 1.
3. The raise() function raises the signal only to the signal handler
registered for the thread in which it is called. You can use this function
to signal your own conditions using the signals SIGUSER1, SIGUSER2, and
SIGUSER3, which C Set provides for just this purpose. You can also use
this function to generate the other signals to test your signal handlers.
Now, let's convert our simple signal handler example to a multithreaded
version. Because the signal handling on each thread is independent, it's easy.
We only need to fix the reentrancy problem with jbuf. There are many ways to
do this. We're going to assume that there is an array of pointers, indexed by
thread number, that we can use. Since thread numbers are reused by the
operating system, we should know how big an array we need. The changes are
easy to pick out. Here's our new and improved function:
#define INCL_BASE
#define INCL_NOPMAPI
#include <os2.h> /* for the doscall */
#include <signal.h> /* for the signal function */
#include <setjmp.h> /* for the setjmp and longjmp functions */
#include <stdio.h> /* for the printf call */
#include <stddef.h> /* for _threadid */
static void mysig(int sig); /* the signal handler prototype */
void * tss_array[100]; /* reserve space for 100 threads */
int chkptr(void * ptr, /* pointer to storage to check */
int size) /* number of bytes to check */
{
void (* oldsig)(int); /* where to save the old signal handler */
volatile char c; /* volatile to insure access occurs */
int valid = 0; /* count of valid bytes */
PTIB ptib; /* stuff to get the TIB pointer */
PPIB ppib;
PVOID * temp;
char * p = ptr; /* to satisfy the type checking for p++ */
jmp_buf jbuf; /* the jump buffer moves to automatic storage */
/* so that it is unique to this thread */
unsigned int tid = *_threadid; /* get the thread id */
/* create a thread specific jmp_buf */
tss_array[tid] = (void *)jbuf; /* save the pointer to the jump buffer */
/* in the thread specific storage array */
oldsig = signal(SIGSEGV,mysig); /* set the signal handler */
if (!setjmp(jbuf)) { /* provide a point for the signal handler */
/* to return to */
while (size--) /* scan the storage */
{
c = *p++; /* check the storage */
valid++; /* then bump the counter */
}
}
ptib->tib_arbpointer = temp; /* restore the user pointer */
signal(SIGSEGV,oldsig); /* reset the signal handler */
return valid; /* return number of valid bytes */
}
static void mysig(int sig) {
unsigned int tid = *_threadid; /* get the thread id */
/* find the thread specific jmp_buf */
printf("Detected invalid storage address\n");
longjmp((int *)tss_array[tid],1); /* restart the function at the setjmp() call */
/* without restarting the while() loop */
}
ΓòÉΓòÉΓòÉ 3.4. Signal Handling and DLL's ΓòÉΓòÉΓòÉ
To lay a foundation for understanding how signal handling and DLLs interact, I
must digress and discuss how C Set knows when to call a signal handler. The
only way that C Set knows that a problem has occurred is for it to get an
exception from the operating system. How this is done in the general case,
we'll discuss later. For now, it's enough to know that C Set must register its
exception handler (a function called _Exception()) with the operating system.
_Exception() then converts the exception into a signal number, and then looks
up the handling for the signal in the signal table. The signal table is part
of the library environment, and contains an entry for each thread and signal,
giving the function to handle that signal. The C Set library then calls the
appropriate signal handler. This all assumes that the C Set library receives
the exception, and can find the appropriate signal table, so it can call your
signal handler.
Using DLLs is painless if the C Set library assumptions are valid. You can
make that so by following a few rules. If you can meet all three of the
following conditions, you can partition your application into DLLs without
worrying about signal handling:
1. All your DLLs and the EXE are written using C Set
2. You have compiled them all with the /Gd+ switch, and are using the C Set
library DLLs.
3. Your functions may call functions in third party DLLs (DLLs provided
neither by you nor the C Set library, nor the operating system). They may
not call functions in your code. That's known as a callback.
These conditions make the DLL boundaries invisible to the signal handling. The
restrictions boil down to making sure that (from the C Set library's
perspective):
1. There is only one copy of the C library used by your complete program. Each
copy of the library has its own environment. Since the signal table is
part of that environment, the C Set library has no problem finding the
signal tables.
2. The C Set library exception handler will receive the exception.
So what do you do if you can't meet one of the restrictions? The following
should provide some guidance:
1. You may not ship the C Set/2 library DLLs with your application. This
doesn't create much of a problem. Just create your own version of the
library DLLs, and export the library entry points to all your other DLLs
and your EXE. The IBM C Set User's Guide tells you how. The key is that
your application should use only one copy of the C Set libraries. Your
application won't care if it's in a C Set/2 DLL, or one you create
yourself.
2. You may not ship the C Set++ library DLLs with your application without
renaming them. Use the DLLRNAME utility provided with C Set++ to rename
them.
3. The "no callback" restriction is impossible to meet in a PM program. You
can't guarantee that the C Set exception handler will get exceptions in a
callback from code that you do not control. It's easy to keep the C Set
library signal handling code happy. All we have to do is insure that the C
Set library exception handler gets the exception. The #pragma handler()
statement registers the C Set exception handler, solving the problem with
one line of code.
For instance, in a PM program, the window procedure is a callback function.
We don't know if PM has changed the exception handling environment, so we
need to register the C Set exception handler. If your window procedure is
MyWindowProc(), you code the following before the definition of
MyWindowProc().
#pragma handler(MyWindowProc)
This adds about 5 80386 instructions to your function to register and
deregister the C Set library exception handler. Remember that signal
handlers need to be registered for each thread. If the callback is not on
a thread for which you already have registered signal handlers, you'll also
have to register them.
4. Compiling without the /Gd+ option statically links the library to your EXE
or DLL (your executable). Each such executable has a separate library
environment, with its own signal tables, which requires its own C Set
library exception handler to be registered. A DLL built this way must be
treated as a "third party" DLL. You can make this work, but its not
recommended in any but the simplest case.
The simplest case is one where the statically linked executable calls only
functions in the operating system; it does not call functions in any of
your DLLs. This allows you to provide a localized solution; all we have to
do is treat all entry points to this executable the same way as we treated
callbacks. Just provide a #pragma handler() statement for each entry
point, and everything works fine. If you have more than one DLL like this,
you can treat each one independently, without reference to the others.
Anything other than the simplest case requires you to put #pragma handler()
statements at any DLL entry or callback point where the environment can
change. Keeping track of where the environment changes and which signal
handlers are valid in which environment can quickly become very difficult.
That's why it's not recommended.
ΓòÉΓòÉΓòÉ 3.5. How to Cause Problems ΓòÉΓòÉΓòÉ
The C Set signal handling is fairly robust, but as with all things in C, there
are ways to cause unexpected problems. The following is a short list of "ways
to shoot yourself in the foot":
1. You can register anything as a signal handler. The library won't care. No
problem will occur until the signal happens. A variant of this occurs in
the following situation:
If you load a DLL (using DosLoadModule() or _loadmod()), and register a
function in it as a signal handler, you must not unload the DLL without
changing the signal handler. The C Set signal handling won't know that the
DLL is no longer loaded when a signal occurs. The result will usually be a
simple abend, but if another DLL gets loaded into the same address range,
really strange things can occur.
2. Don't assume that a SIGSEGV signal always means that you have a bad data
pointer. It can also be caused by a wild branch if the address pointer
goes outside your code segment..
3. Don't assume the SIGILL signal will always occur when you take a bad
function call via a pointer. The pointer may point to something that is a
valid instruction stream.
4. Don't expect C Set or OS/2 to be silent about dereferencing a NULL pointer.
This is guaranteed to cause a SIGSEGV signal, unlike C/2, which only
generated a runtime warning as you exited the program.
5. When using longjmp() to leave a signal handler, you must take care to
insure that the jump buffer that you are using is valid. It must have been
created by the thread in which you are currently executing, and the point
in your program where it was created must still be on the call chain. The
C Set libraries check as best they can, and will terminate your process if
they can determine that the contents of the jump buffer are not valid.
Especially avoid calling setjmp() on one thread, and calling longjmp() with
the same jump buffer on another thread. The C Set library specifically
checks for this!
6. The C Set signal handling has one idiosyncracy. If you call gets(),
scanf(), or other console I/O library functions, and a SIGINT, SIGBREAK, or
SIGTERM signal occurs, you may think the behaviour unusual. You don't
receive the signal until after the library call completes. This was an
unavoidable side effect of protecting the C Set library internal data
structures. Since you are allowed to call any library function from within
a signal handler, there is a possibility of a library function being
unexpectedly re-entered by your signal handler. The library protects
itself by putting some of its code inside "must complete" sections. This
means that the C Set library holds off these signals until the "must
complete" section ends. You can do much the same thing yourself using the
operating system APIs DosEnterMustComplete() and DosExitMustComplete().
ΓòÉΓòÉΓòÉ 4. Exception Handlers ΓòÉΓòÉΓòÉ
As I noted before, it's not a good idea to use an exception handler if a signal
handler can do the job for you. Exception handlers are more complex to write,
and awkward to debug, but they're your only option if you use the C Set
subsystems library. After you have written your first exception handler, you
will find you have become well aquainted with the contents of the toolkit
header file BSEXCPT.H.
Exception handlers have two advantages over signal handlers:
1. You get more information. Much of it is useless to a C programmer, but
there are some things that are useful that you don't get from a signal
handler.
2. You can intercept any exception. The C Set library doesn't convert all the
exceptions into signals. Some it passes on to the operating system because
there is no appropriate C semantic for handling them. For example, it does
not handle any of the process termination exceptions.
Because exception handlers do provide function which is not available from the
C Set signal handlers, it is sometimes useful to use both. An exception
handler can be used with the C Set applications libraries if a few simple rules
are followed, as described later.
ΓòÉΓòÉΓòÉ 4.1. Types of Exceptions ΓòÉΓòÉΓòÉ
Exceptions break down into two major classes:
Asynchronous Exceptions: These exceptions are caused by actions outside your
thread of execution. There are only two:
1. XCPT_SIGNAL, which includes the keyboard signals (ctrl-break, etc), and
the kill process exception. These only appear on thread 1 of your
process, since all processes must have a thread 1.
2. XCPT_ASYNC_PROCESS_TERMINATE, which indicates that some thread in your
process has terminated the entire process. This exception may occur on
any thread.
Synchronous Exceptions: All other exceptions fall into this class. These
exceptions are caused by code executing on the thread that got the
exception.
Like the signal handlers, exception handlers are only called for exceptions
happening on the thread in which they are registered. Unlike signal handlers,
exception handlers have a hierarchy, which can be seen in the next section.
ΓòÉΓòÉΓòÉ 4.2. Registering an Exception Handler ΓòÉΓòÉΓòÉ
Exception handlers are registered using a structure called an
EXCEPTIONREGISTRATIONRECORD. The operating system finds them by following a
chain rooted in the TIB. Registering an exception handler is done by placing
the address of the exception handler and the chain pointer from the TIB in an
EXCEPTIONREGISTRATIONRECORD, The TIB is then updated to to point to the new
EXCEPTIONREGISTRATIONRECORD. The chain is shown in the following diagram:
The TIB
ΓöîΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÉ
Γöé . Γöé
Γöé . Γöé
Γöé . Γöé
Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ
Γöé chain pointer (tib_pexchain) Γö£ΓöÇΓöÇΓöÇΓöÉ
ΓööΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÿ Γöé
Γöé
ΓöîΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÿ
Γöé
Γöé The stack
Γöé ΓöîΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÉ
Γöé Γöé . Γöé
Γöé Γöé . Γöé
Γöé Γöé . Γöé
Decreasing Γöé Γöé Γöé
Memory Γöé Γöé EXCEPTIONREGISTRATIONRECORD 1 Γöé
Addresses Γöé Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ
Γöé Γöé Γöé pointer to handler (ExceptionHandler) Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ> Handler Function Handler Function
Γöé Γöé Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ
Γöé Γöé NULL <ΓöÇΓöñ chain pointer (prev_structure) Γöé<ΓöÇΓöÇΓöÉ
Γöé Γöé Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ Γöé
Γöé Γöé Γöé . Γöé Γöé
Γöé Γöé Γöé . Γöé Γöé
Γöé Γöé Γöé . Γöé Γöé
Γöé Γöé Γöé . Γöé Γöé
Γöé Γöé Γöé . Γöé Γöé
Γöé Γöé Γöé EXCEPTIONREGISTRATIONRECORD 2 Γöé Γöé
Γöé Γöé Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ Γöé
Γöé Γöé Γöé pointer to handler (ExceptionHandler) Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ> Handler Function
Γöé Γöé Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ Γöé
Γöé ΓööΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ>Γöé chain pointer (prev_structure) Γö£ΓöÇΓöÇΓöÇΓöÿ
Γöé Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ
Γöé Γöé . Γöé
Γöé Γöé . Γöé
V Γöé . Γöé
NOTE: Names in parentheses are the names of the fields in the structures
Each EXCEPTIONREGISTRATIONRECORD is chained to the next. When an exception
occurs, the operating system starts at the TIB, and goes to each
EXCEPTIONREGISTRATIONRECORD in turn, starting, in our example, with
EXCEPTIONREGISTRATIONRECORD 2. It calls the exception handler, whose address is
in the record, giving it the exception information. The exception handler can
then either handle the exception, or tell the operating system to pass the
exception to the next handler in the chain. If the last exception handler in
the chain does not handle the exception, the operating system takes its default
action. The last exception handler is recognized by the NULL in its chain
pointer. The operating system is very picky about where the
EXCEPTIONREGISTRATIONRECORDs are in memory. They must be on the stack, and
each record, starting from the TIB must be at a higher address than the
previous one.
Terminology Note: The term "unwind" when applied to an exception handler refers
to forced deregistration. Suppose that I wish to longjmp() to a
point in the code before the point at which I registered exception
handler 1 in the above diagram. This means the stack pointer is
going to be moved, and EXCEPTIONREGISTRATIONRECORD 1 is now at a
lower address than the ESP register (the stack grows downward). This
is definitely an unstable state of affairs, so the operating system
handles it by providing the API DosUnwindException() to "unwind" the
exception handlers. When it is called, the operating system calls
each exception handler whose EXCEPTIONREGISTRATIONRECORD is about to
be removed, telling it that it is being unwound. When the exception
handler returns, its EXCEPTIONREGISTRATIONRECORD is then removed from
the chain. Since DosUnwindException() requires that you provide a
machine state (which is not available in a high level language like
C), the C Set library function longjmp() calls it for you.
There are several ways to attach an EXCEPTIONREGISTRATIONRECORD to the chain.
The most obvious way is to do all the code to link it in yourself, since you
can get the TIB address yourself. That will work, but the operating system
provides APIs to do the same thing. The APIs are shown in the following
example:
#define INCL_BASE
#include <os2.h>
/* the prototype for the exception handler */
APIRET APIENTRY MyExceptionHandler(EXCEPTIONREPORTRECORD *,
EXCEPTIONREGISTRATIONRECORD *,
CONTEXTRECORD *,
PVOID);
int myfunction(.....
EXCEPTIONREGISTRATIONRECORD err = { NULL,MyExceptionHandler };
DosSetExceptionHandler(&err); /* register */
.
.
.
DosUnsetExceptionHandler(&err); /* deregister */
}
For the time being, ignore the details of the exception handler prototype.
We'll look at its interface later. The actual registration is simple. Define
your EXCEPTIONREGISTRATIONRECORD, load it up, and then call the APIs to
register and deregister it.
You can choose to register the exception handler over only a part of your
function. However, you must remember to deregister the exception handler
before you leave. If you don't, the next exception you get on this thread may
cause strange and unexpected results. If the function has many return
statements, it's easy to miss one. A "single entry/single exit" programming
style is recommended.
This type of registration can be used to register more than one exception
handler in a function. Just insure that the EXCEPTIONREGISTRATIONRECORDs are
in the correct order on the stack.
The C Set compiler provides another way to register an exception handler. It
will execute much faster, since it puts the registration and deregistration
code inline to the function. It also makes use of the fact that the TIB is
always at location FS:0000, so the total extra generated code is 5
instructions. It is slightly less flexible, since it registers the exception
handler on function entry, and deregisters it on exit; you can't register over
part of a function. It is an extension of how we registered the C Set library
exception handler.
#pragma map(_Exception,"MyExceptionHandler")
#pragma handler(myfunction)
int myfunction(.....
1. Your exception handler, MyExceptionHandler() must have external linkage;
you can't make it a static function. For you ex-assembler programmers,
that means it must be a PUBLIC symbol. The compiler must assume that
MyExceptionHandler() is the name of your exception handler. It can't even
verify that this is the name of a function, much less one of the correct
type. Be careful!
2. The #pragma map() statement tells the compiler to map the name _Exception
to MyExceptionHandler. Any external reference in this module to _Exception
will now be converted to a reference to MyExceptionHandler.
3. #pragma handler(myfunction) would normally tell the compiler to generate
code to register the exception handler _Exception for function
myfunction(). Since we have mapped the name, the compiler will actually
use MyExceptionHandler as the exception handler name. Remember, the
EXCEPTIONREGISTRATIONRECORD is on the stack. Bad things happen if we leave
this function without deregistering it, so the compiler does the right
thing, and generates code to deregister it as the function returns.
Note: This method can handle only one exception handler in a module, since it
changes the name of the exception handler registered by #pragma handler(). It
may require you to place functions in separate modules to get the exception
handling you want.
ΓòÉΓòÉΓòÉ 4.3. The Exception Handler Interface ΓòÉΓòÉΓòÉ
Now we know how to register an exception handler, we'll look at how OS/2 calls
the exception handler, and what it tells us about the exception. You'll find
declarations of the exception handling interface structures in the toolkit
header file BSEXCPT.h. All exception handlers must have the following
prototype:
#define INCL_BASE
#include <os2.h>
APIRET APIENTRY MyExceptionHandler(EXCEPTIONREPORTRECORD *,
EXCEPTIONREGISTRATIONRECORD *,
CONTEXTRECORD *,
PVOID);
Only the first three arguments are of any concern. The last one is a pointer
used by the operating system.
APIRET: A standard definition used in the toolkit header files. If you return
from the exception handler, you must return one of two values:
XCPT_CONTINUE_SEARCH: This means that you have not handled the exception,
and want the operating system to pass the exception to the next
handler on the chain.
XCPT_CONTINUE_EXECUTION: This means that you have fixed up the exception
condition, and want the operating system to resume execution of
your application at machine state given in the CONTEXTRECORD.
APIENTRY: This defines the function linkage. The toolkit headers have defined
this as _System linkage. Use the defined value to protect yourself
against any operating system changes.
EXCEPTIONREPORTRECORD *: This is a pointer to a structure which contains high
level information about the exception.
EXCEPTIONREGISTRATIONRECORD *: This is a pointer to the record that registered
this exception handler. The address is always on the stack. If you
your exception handler was registered with DosSetExceptionHandler(),
you can make the EXCEPTIONREGISTRATIONRECORD part of a larger
structure, and access the information in that structure from inside
the exception handler.
CONTEXTRECORD *: This is a pointer to a structure which contains the thread
state at the time of the exception. i.e. all the registers, the
state of the 80387, and the flags. In general, C can't make much use
of this, since its use requires knowledge of the 80386 instruction
stream for your application. If any handler returns
XCPT_CONTINUE_EXECUTION, the machine state is reloaded from this
structure. Don't modify it unless you intend to return
XCPT_CONTINUE_EXECUTION.
PVOID: This is a pointer which the OS/2 exception handler requires you to
pass back unchanged. The operating system documentation is silent as
to its meaning.
The operating system gives us quite a bit of information about the exception.
Most of it concerns the machine state at the time of exception. Since we're in
a high level language, it's meaningless to us, since we can't relate it to our
high level language constructs. However, there are some useful nuggets of
information in the EXCEPTIONREPORTRECORD that we can use. This structure looks
like this:
struct _EXCEPTIONREPORTRECORD
{
ULONG ExceptionNum; /* exception number */
ULONG fHandlerFlags; /* exception handler flags */
struct _EXCEPTIONREPORTRECORD *NestedERR; /* used only if a nested exception */
PVOID ExceptionAddress; /* EIP at which exception occurred */
ULONG cParameters; /* Size of Exception Specific Info */
ULONG ExceptionInfo[EXCEPTION_MAXIMUM_PARAMETERS]; /* Exception Specfic Info */
};
1. We get an exception number in the ExceptionNum field. Unlike the C Set
signals, we get to see all the exceptions, including the ones that C Set
ignores and lets the operating system handle. There are some useful
operating system exceptions which we can get no other way:
XCPT_PROCESS_TERMINATE: This exception tells us that our process is about to
end. It's useful, because it is indicates that this thread has
called DosExit(). Until you leave your exception handler, your
thread continues as though DosExit() had not been called.
XCPT_ASYNC_PROCESS_TERMINATE: This exception tells you that some other
thread in your process has called DosExit(), and that the
XCPT_PROCESS_TERMINATE exception that results has been passed
through its the exception handlers. In other words, your thread
is being told that it is terminating. You can decide not to
terminate the thread, since this exception can be returned as
"handled".
XCPT_ACCESS_VIOLATION: This exception is analagous to the SIGSEGV signal.
You do get extra information: the ExceptionInfo field gives you
the address that generated the exception, and the type of access
(read/write).
XCPT_GUARD_PAGE_VIOLATION: This exception tells you that your thread has
attempted to access a memory page marked as a "guard page". See
the OS/2 documentation on the DosAllocMem API. Unless you've used
this API yourself to allocate memory, it means that your
application has accessed a guard page on the stack. Normally,
this is of no consequence. You pass the exception on to the
operating system, and let it grab another 4K of committed memory
for your thread, and move the guard page. But if you know how big
your stack is, you can stop things gracefully if you are running
out of stack. Remember, the operating system needs about 1.5
Kbytes to dispatch an exception when you calculate how much stack
you have left.
XCPT_UNABLE_TO_GROW_STACK: This exception means you're in deep trouble. The
operating system attempted to move your stack's guard page, and
found that there was no uncommitted memory to move it into. If
you compiled your code with the /Gs option, you should have about
500 bytes of stack for your exception handler. If you didn't use
the /Gs option, you may not get this exception; your process may
terminate with an operating system trap.
We can alos create our own exception numbers and use them for signalling.
The OS provides the DosRaiseException() API to raise exceptions. You are
not limited to the ones the operating system defines. You can create your
own, and use your own exception handler to catch them.
2. The fHandlerFlags can tell us some things about how the exception occurred,
and what we are permitted to do about it. The following bits are found
here:
EH_NONCONTINUABLE: This means that you cannot continue execution of this
thread once you leave the exception handler. Returning
XCPT_CONTINUE_EXECUTION if this bit is set is an error. You can
also set this bit, even if you do not handle the exception,
thereby making any exception noncontinuable. You cannot reset the
bit; it's "sticky".
EH_UNWINDING: This means that someone has longjmp()ed over this exception
handler, and it is about to be deregistered. If your function
uses a mutex semaphore, this is a good point to hook in to release
them.
EH_EXIT_UNWIND: This flag means that a DosExit call has been made, the
exception has been passed back to the operating system, and this
is your last chance to do anything before your exception handler
is deregistered.
EH_NESTED_CALL:This tells you that you were handling an exception, when
another one occurred. Be careful if you see this one! Each
exception uses up about 1.5 Kbytes of stack. If you nest
exceptions too deep, you'll run out of stack.
3. The OS tells us where the instruction address at which the exception
occurred in the ExceptionAddress field. You usually can't determine which
function caused the problem at run time without special preparations.
4. For some exceptions, such as XCPT_ACCESS_VIOLATION, the ExceptionInfo field
may contain additional information, such as the address at which the memory
access failed. The number of bytes of information is found in the
cParameters field.
The CONTEXTRECORD structure is of limited use to a high level programmer. It
contains the state of the thread. When you return XCPT_CONTINUE_EXECUTION, the
thread is restarted at the machine state stored in this structure.
1. If you are smart enough, you can fix some of the exceptions and continue
execution from where the exception happened. The C Set library only does
this for the asynchronous exceptions. For synchronous exceptions,
resumption of execution requires knowledge of the application and
modification of the CONTEXTRECORD. Don't attempt this unless it's the only
solution to your problems, since it is extremely difficult to get the
exception handler code to be correct for all possible conditions.
2. You have all the information to do a stack trace in the CONTEXTRECORD. You
may think that this allows us to change the machine state, since we can
find the return addresses from functions this way. Don't do it! Because
the calling conventions used by C Set and OS/2 specify that some of the
machine registers are preserved across calls, it is almost impossible to
reconstruct these registers by traversing the stack. So this isn't likely
to help us recover from the problem, but could dump out some useful debug
information. You are guaranteed that a 32 bit stack always looks like this:
Γöé . Γöé
A Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ
Γöé Γöé Return Address Γöé Γöé
Γöé Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ Γöé
ΓööΓöÇΓöÇΓöÇΓöÇΓöñ EBP Γöé<ΓöÇΓöÇΓöÇΓöÉ Γöé
Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ Γöé Γöé
Γöé . Γöé Γöé Γöé
Γöé . Γöé Γöé Γöé Stack grows
. Γöé Γöé this way
. Γöé Γöé
. Γöé Γöé
Γöé . Γöé Γöé Γöé
Γöé . Γöé Γöé Γöé
Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ Γöé Γöé
Γöé Return Address Γöé Γöé Γöé
Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ Γöé Γöé
ΓöîΓöÇΓöÇΓöÇ>Γöé EBP Γö£ΓöÇΓöÇΓöÇΓöÇΓöÿ Γöé
Γöé Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ Γöé
Γöé Γöé . Γöé Γöé
Γöé Γöé . Γöé Γöé
Γöé . V
Γöé .
Γöé .
Γöé Γöé . Γöé
Γöé Γöé . Γöé
Γöé Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ
Γöé Γöé Return Address Γöé
Γöé Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ
ΓööΓöÇΓöÇΓöÇΓöÇΓöñ EBP Γöé<ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ EBP from exception context record
Γö£ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöñ points here
Γöé . Γöé
Γöé . Γöé
Be careful when traversing the stack using the EBP chain. The stack may
have been damaged (i.e. the chain points to outer space!). Also, if there
are 16 bit calls on the stack, you will not be able to trace over them.
Now we have some idea what information we are given in an exception handler,
what does one look like? Let's take our little chkptr() example, and do it
with an exception handler. It going to get a lot bigger! This is the main
program:
#define INCL_DOS
#define INCL_NOPMAPI
#include <os2.h> /* for the doscall */
#include <stdlib.h> /* for the signal function */
#include <setjmp.h> /* for the setjmp and longjmp functions */
#include <stdio.h> /* for the printf call */
#include <stddef.h> /* for _threadid */
void * tss_array[100]; /* array for 100 thread specific pointers */
APIRET APIENTRY MyExceptionHandler(EXCEPTIONREPORTRECORD *,
EXCEPTIONREGISTRATIONRECORD *,
CONTEXTRECORD *,
PVOID);
#pragma map(_Exception,"MyExceptionHandler")
#pragma handler(chkptr)
int chkptr(void * ptr, /* pointer to storage to check */
int size) /* number of bytes to check */
{
volatile char c; /* volatile to insure access occurs */
int valid = 0; /* count of valid bytes */
char * p = ptr; /* to satisfy the type checking for p++ */
jmp_buf jbuf; /* the jump buffer moves to automatic storage */
/* so that it is unique to this thread */
PTIB ptib; /* stuff to get the TIB pointer */
PPIB ppib;
PVOID * temp;
unsigned int tid = *_threadid; /* get the thread id */
/* create a thread specific jmp_buf */
tss_array[tid] = (void *) jbuf;
if (!setjmp(jbuf)) { /* provide a point for the signal handler */
/* to return to */
while (size--) /* scan the storage */
{
c = *p++; /* check the storage */
valid++; /* then bump the counter */
}
}
ptib->tib_arbpointer = temp; /* restore the user pointer */
return valid; /* return number of valid bytes */
}
The main function really hasn't changed much; you should recognize everything.
All we've done is add in the linkage to our exception handler. The signal
handler is now history. But this is what the exception handler (doing exactly
the same function) looks like:
APIRET APIENTRY MyExceptionHandler(EXCEPTIONREPORTRECORD * report_rec,
EXCEPTIONREGISTRATIONRECORD * register_rec,
CONTEXTRECORD * context_rec,
PVOID dummy)
{
unsigned int tid = *_threadid; /* get the thread id */
/* check the exception flags */
if (EH_EXIT_UNWIND & report_rec->fHandlerFlags) /* exiting */
return XCPT_CONTINUE_SEARCH;
if (EH_UNWINDING & report_rec->fHandlerFlags) /* unwinding */
return XCPT_CONTINUE_SEARCH;
if (EH_NESTED_CALL & report_rec->fHandlerFlags) /* nested exceptions */
return XCPT_CONTINUE_SEARCH;
/* determine what the exception is */
if (report_rec->ExceptionNum == XCPT_ACCESS_VIOLATION) {
/* this is the only one we expect */
printf("Detected invalid storage address\n");
longjmp((int *)tss_array[tid],1); /* restart the function at the setjmp() call */
/* without restarting the while() loop */
} /* endif */
return XCPT_CONTINUE_SEARCH; /* we don't handle this exception */
}
That's a lot bigger than that simple three line signal handler we had. Let's
look at what it does:
1. On entry we check the exception flags first. The EH_EXIT_UNWIND flag
indicates that this thread is terminating, and that this is our last chance
to do anything. Since we don't care, we return XCPT_CONTINUE_SEARCH, which
tells the operating system to pass the exception to the next exception
handler.
2. Next we look at the EH_UNWINDING flag. This tells us that we are being
removed as an exception handler. Again, we don't care, since this also
means that chkptr() is being removed from the stack.
3. Then we check to see if this exception occurred inside an exception
handler. The EH_NESTED_CALL flag indicates this. Since we check this flag,
it means that chkptr() cannot be used by an exception handler. If we don't
check the flag, there is the possibility of recursive exceptions until we
run out of stack. This example shows the check, but you don't have to put
it in.
4. Now we've finished the preamble, which should be in all exception handlers.
We can look at the exception number! A good rule of thumb is, "only check
for exceptions that you expect". This protects you against possible new
exception numbers. If we got the only expected exception, which is
XCPT_ACCESS_VIOLATION, we do exactly what we did in the signal handler. We
print a message, and longjmp(). This terminates the exception handler for
us.
There is (theoretically), another way. If we knew the format of the jump
buffer (which we don't), we could have changed the machine state to match
it, and then returned XCPT_CONTINUE_EXECUTION. This would have had the
same effect. However, using longjmp() is a lot easier.
5. Finally, if we get to the end of the exception handler, we must then tell
the operating system to pass this exception on to the next exception
handler. We haven't handled it, so we return XCPT_CONTINUE_SEARCH.
A word of warning: don't return XCPT_CONTINUE_EXECUTION from an exception
handler unless you know that the thread can continue execution. This means
that you either know that the exception can be restarted (one of the
asynchronous exceptions), or you have changed the exception context record so
that it can. Otherwise, you will probably hang your process by generating a
new exception each time you exit your exception handler.
ΓòÉΓòÉΓòÉ 4.4. Exception Handling for DLLs and Multiple Threads ΓòÉΓòÉΓòÉ
Since the OS/2 exceptions are thread based, multithreaded programs are handled
just like we did with signals. The only thing we have to watch for is
non-reentrancy problems, like our jump buffer.
Exception handling in DLLs is actually cleaner than with signals. Since
exception handlers don't use the C Set library, we are not dependant on any
library environment, and can always ignore DLL boundaries. However, if you are
writing a "system" DLL, one that is used by a number of different applications,
it is a good idea to protect yourself by registering an exception handler at
each entry point to it. That way, you control what happens during an
exception. If you don't register the exception handlers, your using
application will get the exception. What happens then is not under your
control.
ΓòÉΓòÉΓòÉ 4.5. Applications of Exception Handlers ΓòÉΓòÉΓòÉ
Since exception handlers can do a number of things that the C Set signal
handlers cannot, let's look at a couple of useful ideas:
1. Since an exception handler is called during an unwind operation, we have a
way to determine that a function is longjmp'ed over. This can be extremely
useful in a multithreaded program that uses semaphores. Suppose we have
the following code:
jmp_buf(jbuf);
HMTX sem;
int x(.... ){
.
.
.
setjmp(jbuf);
.
.
.
y();
.
.
.
}
void y(... ) {
.
.
.
DosRequestMutexSem(sem,-1);
.
.
z();
.
.
DosReleaseMutexSem(sem);
.
.
.
}
void z(... ) {
.
.
.
longjmp(jbuf);
.
.
.
}
This looks rather innocuous, but demonstrates a problem. When we take the
longjmp() in function z(), we leave the semaphore sem in on "owned" state.
This can cause problems in other threads in our program, since they may no
longer be able to obtain ownership of this semaphore. If we register an
exception handler for function y(), it can recognize the unwind that occurs
as a result of the longjmp(), and release the semaphore.
Note: z doesn't have to call longjmp() itself. longjmp() could have been
called from inside a signal or exception handler called as a result of an
exception inside z. The problem is still the same.
2. Since we can intercept a termination exception, we can hold off the
processing of it until we have cleaned up. For example, in a "system" DLL,
we can update tables, and generally bring the operations to a clean halt.
We probably can't do this using DLL initialization and termination, since
this type of DLL often has global initialization and termination, which is
not invoked until the last caller has terminated.
ΓòÉΓòÉΓòÉ 4.6. Ways to Cause Problems ΓòÉΓòÉΓòÉ
The exception handling structures are more fragile than those used for signal
handling. Exception handlers are subject to all the problems of signal
handlers, and a few new ones of their own:
1. Forgetting to deregister an exception handler will usually crash your
process. Debugging this situation can be difficult. If you suspect it,
look at the TIB and exception handler chain using the debugger. The TIB is
at offset 0 from the FS register.
2. Forgetting to check the flags in an exception handler may look like a good
way to save excution time. Remember that even a simple exception handler
like the one for chkptr() can get unwound by an subsequent exception
handler. Always check the flags.
3. Trying to do too much in one exception handler. Exception handlers are
simpler to write, and easier to maintain, if you limit what they can do. A
"does eveything" exception handler is going to be huge.
4. Having a "default exception handler". Always check for and handle only
those exceptions you expect. The operating system may generate new
exceptions on the next release, or you may forget and create your own
exceptions.
ΓòÉΓòÉΓòÉ 4.7. Impossibilities ΓòÉΓòÉΓòÉ
As powerful as the OS/2 exception handling is, there are two situations that
you cannot do anything about.
1. If you have less than 1.5 Kbytes (approximately) of stack left, the
operating system can't call your exception handler. If you generate an
exception in this case, the operating system takes over, and terminates
your process. It isn't even guaranteed that it will give you a message
saying why.
2. If you run out of space on your swapper file, nothing, and I mean nothing
is guaranteed to work. The behaviour of the operating system becomes
undefined.
ΓòÉΓòÉΓòÉ 4.8. Exceptions in library functions ΓòÉΓòÉΓòÉ
The C Set subsystem library has no exception handling built in. You may treat
its library as an extension of your own code for the purposes of exception
handling.
The story is different in the C Set applications libaries. The exception
handling behaviour of the functions in these libraries splits into three
categories:
Critical functions: Many of the functions in the library are not reentrant,
including most I/O functions and all memory allocation functions. A
full list is in the IBM C Set User's Guide. Exceptions occurring in
these functions indicate that the library environment is probably
damaged. That means that you have inadvertently modified the
library's environment, either by storing into it yourself, or by
calling a library function with an invalid argument. In either case,
the C Set library cannot guarantee continued correct operation; the
critical function will treat this as a terminal error, and its
exception handler will call DosExit(). The handler is registered by
the library function itself; your exception handlers will not receive
the original exception. They will only receive the termination
exception.
Math functions: All the functions in the header file math.h are defined as math
functions. These functions are completely reentrant. However,
exceptions in them should be handled by the C Set library exception
handler. Math functions generate floating point exceptions when
domain and range errors are encountered. The C Set library exception
handler can recognize these specific exceptions, and fix them up.
Other functions: The remainder of the library has no special exception handling
considerations. You may treat exceptions occurring in it as though
they occurred in your own code.
ΓòÉΓòÉΓòÉ 4.9. Coexistence with C Set exception handling ΓòÉΓòÉΓòÉ
Since the C Set signal handling is built on top of the operating system's
exception handling, it's relatively easy to get your signal handlers to coexist
with the C Set application libraries. There are two simple rules:
1. The math functions (defined in math.h) require special exception handling
to completely maintain the semantics of C. Therefore, it is necessary for
the C Set library exception handler to handle any floating point exceptions
originating in these functions. This is only a problem if you have
registered your own exception handler, and wish to call one of these
functions. In this case, ensure that one of the following two C Set
library exception handlers is registered when calling math functions:
_Lib_except(): This is a very limited exception handler. It handles
floating point exceptions which have occurred in math functions,
and nothing else. It recognizes which math function caused the
exception, performs fixup, and continues your program's execution.
_Exception(): This is the full C Set library exception handler, intercepting
most exceptions, providing signal handling and default fixups. It
will call _Lib_except() for the math functions, but it will also
handle exceptions occurring outside the math functions.
2. If you have registered your own exception handler, any exception you handle
will not be seen by a signal handler. The ones you don't handle will fall
through to the next exception handler. If that one is the C Set exception
handler, it will call a signal handler. If you want to completely
re-establish signal handling, you must register the C Set library exception
handler _Exception.
ΓòÉΓòÉΓòÉ 5. Floating Point Exceptions ΓòÉΓòÉΓòÉ
Floating point exceptions are a major subject on their own, and require
in-depth knowledge of the 80387 to really understand. I intend only to give
some simple pointers on the subject, and will have to assume that you are
familiar with the terminology of IEEE floating point numbers.
In general, floating point exceptions cannot be retried without significant
knowledge of the 80387 chip and your application. The C Set library design
team decided that this was beyond the scope of what they were willing to
attempt. So, in general, the library treats floating point exceptions as a
terminating condition. You do have some control, however. The C Set library
function, _control87(), affects the floating point exceptions. It allows you
to mask the individual exceptions that the 80387 can generate, and let the
80387 take a default fixup action. You call it as follows:
oldstate = _control87(value,mask)
You can look up the complete function description in the IBM C Set User's
Guide. The bit masks that affect exception handling are defined in float.h.
Unless otherwise noted, exceptions are masked on (enabled). The fixups
described are the ones performed by the 80387 if the exception is masked off.
The bit masks have the following meanings:
EM_INVALID: Mask off exceptions resulting from invalid operations to the
80387. It's not usually a good idea to mask this exception,
since it indicates that something is seriously wrong. It can
be caused by attempting to operate on certain invalid
floating point numbers, such as NaNS (a signalling NaN - not
a number). It may also be caused by problems with the
80387's stack, which you can cause that by omitting a
prototype for a function that returns a floating point
number. The 80387 fixup is a result of NaNQ (a
non-signalling NaN).
EM_DENORMAL: Mask off exceptions relating to the use of denormalized
floating point numbers. The 80387 fixup is to use them
"as-is", and allow gradual underflow. This exception is not
meaningful when using C Set, and is masked off by default..*
EM_ZERODIVIDE: Mask off the divide by zero exception. The 80387 fixup is a
result of infinity.
EM_OVERFLOW: Mask off the overflow exception. The 80387 fixup is a result
of infinity.
EM_UNDERFLOW: Masks off the underflow exception. The 80387 fixup is either
a denormal number or zero.
EM_INEXACT: Mask off the exception which indicates that precision has
been lost. This exception will occur on most 80387 floating
point operations, and is really only useful when doing
integer arithmetic on the 80387. The C Set compiler uses the
80387 for floating point arithmetic only, so this exception
is not meaningful, and should be left masked off (its default
state). The 80387 fixup is to ignore it.
Each of these flags corresponds to a unique floating point exception. You can
mask them off individually. To prevent the floating point underflow exception,
you would code:
oldstate = _control87(EM_UNDERFLOW,EM_UNDERFLOW)
To turn it back on, you code:
oldstate = _control87(0,EM_UNDERFLOW)
Note: Since the functions in math.h use the 80387, you should ensure that the
80387 control word is in its default state when they are called. Otherwise, the
C Set library may not get the exceptions it needs to completely fulfill the
requirements of the standards. You can reset the 80387 control word to its
default state with the _ fpreset() library function. Note that the 80387 state
is unique for each thread. Changing the 80387 controls in one thread does not
affect any other thread.
ΓòÉΓòÉΓòÉ 6. Restricted OS/2 APIs ΓòÉΓòÉΓòÉ
The OS/2 APIs described in this section affect one of the following aspects of
signal and exception handling:
1. thread switching
2. OS/2 exception processing
3. C Set library behaviour
You should be aware of how these APIs can affect your program.
ΓòÉΓòÉΓòÉ 6.1. DosKillThread() ΓòÉΓòÉΓòÉ
This API is not recommended for use with the C set compiler. It causes an
immediate XCPT_PROCESS_TERMINATE exception on the thread being terminated (the
equivalent of a call to DosExit() on that thread). Under most circumstances,
this causes the thread being killed to terminate immediately. This may result
in the C Set library hanging because a semaphore was not properly released.
Handling an XCPT_PROCESS_TERMINATE exception is generally very difficult, as it
cannot be continued. You should therefore assume, unless specifically told
otherwise, that any subsystem that you are using (including the operating
system itself) cannot survive having a thread killed via this API.
Alternatives to this API all involve having the thread check, at appropriate
points in its execution, whether or not it should kill itself. This is more
work, but will result in a more reliable application.
ΓòÉΓòÉΓòÉ 6.2. DosExit() ΓòÉΓòÉΓòÉ
This API comes in two flavours:
1. DosExit(0,x).
This exits a thread. The library does not get a chance to clean up its
thread specific storage, which may result in a subsequently started thread
inheriting the (no longer initialized) storage. Thread specific storage
contains the following items
o The signal handler functions.
o The random number seed.
o The static pointer used by the strtok() function.
o The errno value.
o The value pointed to by the "hp2._threadstore() function (C Set++ only).
You should use _endthread(), or fall out the bottom of the origninal thread
function.
2. DosExit(1,x).
This terminates the process. The library does not get the chance to
properly clean up its environment, and flush file buffers. Functions
registered with atexit() and onexit() will not be run.
Yo should use exit() or abort(), or fall out the bottom of main(). These
do much the same thing, and they're portable too!
ΓòÉΓòÉΓòÉ 6.3. DosUnwindException() ΓòÉΓòÉΓòÉ
This API will unwind exception handlers from the stack and set the machine
state. In C, you usually do not have the information required to use the API
properly. Its use is not recommended under C Set. The longjmp() library
function does essentially the same thing in a portable way.
ΓòÉΓòÉΓòÉ 6.4. DosSetSignalExceptionFocus() ΓòÉΓòÉΓòÉ
C Set applications assume that they have the focus for signal exceptions.
Removing the focus may prevent you from getting SIGINT and SIGBREAK exceptions
from the keyboard.
ΓòÉΓòÉΓòÉ 6.5. DosAcknowledgeSignalException() ΓòÉΓòÉΓòÉ
The signal functions of C Set assume that they are the ones responsible
acknowledging signal exceptions. You should only use this function in an OS/2
exception handler that you write yourself.
ΓòÉΓòÉΓòÉ 6.6. DosCreateThread() ΓòÉΓòÉΓòÉ
This API doesn't register an C Set library exception handler for the new
thread, or handle the thread setup properly. Use _beginthread() if possible.
If not, the function to be run in the new thread should look like the
following:
/* register the C/C++ exception handler */
#pragma handler(threadfn)
void threadfn(ULONG arg) {
_fpreset(); /* reset the 80386 exception handler */
.
.
.
_endthread(); /* exit with _endthread() so the library */
/* data areas are cleaned up */
}
Threads started this way must always be terminated with _endthread().
ΓòÉΓòÉΓòÉ 6.7. DosEnterMustComplete() ΓòÉΓòÉΓòÉ
This API is used to hold off asynchronous exceptions, including
XCPT_ASYNC_PROCESS_TERMINATE. You must call DosExitMustComplete() to reenable
asynchronous exceptions. This API also blocks the XCPT_ASYNC_PROCESS_TERMINATE
exception, which occurs on process termination. i.e. If a thread in your
process is in a complete section, the other threads cannot terminate the
process until that thread exits its must complete section.
ΓòÉΓòÉΓòÉ 6.8. DosEnterCritSec() ΓòÉΓòÉΓòÉ
This API prevents thread switching within your process. For each call to
DosEnterCritSec(), you must call DosExitCritSec(). Keep critical sections
short, and avoid putting calls that may block inside them. Use these APIs only
when a mutex semaphore cannot be used.
It is not advisable to call any operating system API or any C/C++ library
function between the calls to DosEnterCritSec() and DosExitCritSec(), as your
process may hang.
ΓòÉΓòÉΓòÉ 6.9. DosSuspendThread() ΓòÉΓòÉΓòÉ
This API causes the thread to stop at an arbitrary point. If the thread owns a
semaphore, you may be exposed to a deadlock. Use DosResumeThread() to restart
the thread.
It is not advisable to call any operating system API or any C/C++ library
function between the calls to DosSuspendThread() and DosResumeThread(), as your
process may hang.
ΓòÉΓòÉΓòÉ 7. Conclusion ΓòÉΓòÉΓòÉ
Signal and exception handling under C Set and OS/2 is powerful. Of the two,
signal handling is easier to use, and easier to port. Exception handling is
more difficult to use, and more powerful, but is non-portable.
ΓòÉΓòÉΓòÉ 8. Bibliography ΓòÉΓòÉΓòÉ
American National Standard for Information Systems - Programming Language C, X3
Secretariat: Computer and Business Equipment Manufacturers Association,
Washington, D.C. Document Number: X3J11/90-13, dated Feb. 14, 1990.
IBM C Set User's Guide, Armonk, N.Y. Document Number S10G-4444, dated April
1992.
Systems Application Architecture, Common Programming Interface, C Reference -
Level 2, International Business Machines Corp., Armonk, N.Y. Document Number
SC09-1308-01, dated Dec. 1989.
OS/2 Programmer's Reference, International Business Machines Corp., Armonk,
N.Y.
80386 Programmer's Reference Manual, Intel Corporation, Santa Clara, Calif.,
dated 1986.
80387 Programmer's Reference Manual, Intel Corporation, Santa Clara, Calif.,
dated 1987.
i486 Microprocessor Programmer's Reference Manual, Intel Corporation, Santa
Clara, Calif., dated 1990.