home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Amiga Elysian Archive
/
AmigaElysianArchive.iso
/
prog
/
utils
/
trapsnap.lha
/
Trap.doc
< prev
Wrap
Text File
|
1990-07-11
|
16KB
|
306 lines
(The following article about trap handling and "TrapSnapper" appeared
in The Transactor, Volume 8 Issue 01)
TrapSnapper: Adding A Trap Handler to Your C Programs
by Chris Zamara and Nick Sullivan
That bug in your program could cost you a visit from the Guru...
TrapSnapper helps keep the old geezer at bay!
If you've spent any amount of time with the Amiga, you are probably
not unfamiliar with the following message: "Software Error - Task Held.
Click on Cancel to Reset/Debug". This is an annoying message. If you accept
the invitation to "Click on Cancel", you'll reset the machine, bringing
down not only the task that failed but any others you may have going at the
time. You can ignore the message, if you want (providing you have Workbench
up or access to an extra CLI), but the "Software Error" requester keeps
coming relentlessly back every time you swap disks or shuffle windows.
Furthermore, all visible traces of the program that died - like its windows
and screens - will still be around, getting in your way and using up
memory.
There's a way to deal with software errors, at least in your own programs,
but before we get into that let's look at what these errors are and why the
Amiga handles them as it does.
Tasks, Traps and Exceptions
As you probably know, the programs that you run on the Amiga are treated as
separate tasks under Exec, which is the name given to the set of operating
system routines that are responsible for multitasking, device, library and
I/O management. Only one task can have control of the CPU at any one
instant of course; that task is said to be in the "running" state. Other
tasks will at the same time be either "waiting" for their turn at the CPU,
or lying dormant ("sleeping") until they are awoken by an external event
such as a mouse movement, keypress, or timer signal. Sleeping tasks require
only a tiny proportion of processor time to service. If its wake-up event
never comes, such a task could sleep for ever without perceptibly affecting
the operation of the machine.
One difficulty with a multitasking system is that any task in the running
state can crash due to a bug in the program. Since there may be other,
viable, tasks in the system when this occurs, it is important that they
should be allowed to continue unhindered if at all possible. This is where
a Software Error differs from a Guru Meditation Error - Software Errors are
polite bugs that don't step on outside tasks; Gurus are barbarian bugs that
destroy everyone else along with themselves.
Software Errors are actually detected by the microprocessor itself,
not by the operating system. In 68000 jargon, they are called "exceptions",
for they are conditions that the processor cannot handle by ordinary means.
An example is the undefined result of a division by zero, using one of the
68000 divide instuctions DIVS or DIVU. If this operation is requested
(usually by accident), the 68000 does not know how to proceed, and invokes
an exception.
At this stage, several things happen. The 68000 goes into "supervisor"
mode, which enables the status register (SR) and several privileged
opcodes, and switches over to the supervisor stack from the user stack. Six
bytes of data are pushed onto the supervisor stack: a word containing a
copy of the Condition Code Register (CCR), and a long word containing the
value of the program counter, which for most exceptions is the
address of the instruction following the one that caused the exception. (In
the case of certain exceptions, other data are also pushed, but we'll get
to that later.) Finally, control is turned over to an exception-handling
routine that is accessed via a table of vectors keyed to the "exception
number" of the exceptional condition. For example, division by zero is
exception 5; hence, its exception routine is entered through vector number
5 of the table. It is at this point that Exec takes over.
Exec pushes a long-word on the stack corresponding to the exception vector
number - this is called the trap number (In Amiga jargon, the term
"exception" has been appropriated to another purpose, and exceptions are
known as "traps", from the exception-generating TRAP and TRAPV instructions
of the 68000 - see the list of trap numbers at the end of this article). It
then branches through a vector that is specific to the task that caused the
exception. Unless this vector has been changed by the task itself, it
points to the default trap handler. Since Exec cannot on its own know what
actions would be appropriate to take to rescue a given task from a given
exception, its default handler does nothing more than to put the task to
sleep (permanently), and put up the "Software Error" requester to tell the
user what is going on and provide him or her with the opportunity to reset
if desired. Any resources (primarily RAM) allocated to the errant task
remain allocated, and there is no reasonable way of getting them back.
However, it is not very difficult to create and enable your own
trap-handling routine in a program that you write, and there are
considerable advantages to doing so. For one thing, you can provide a
graceful exit in the event that your program bombs out; more importantly,
you can hand back your allocated RAM to the system, to be made available to
other tasks. You can close any windows or screens that the program may have
opened so that they don't continue to get in the way and use up memory. You
also avoid the "Software Error" message that will otherwise be haunting you
until you reset your machine. There is a slight risk involved: if the event
that caused the trap had trashed your handler code before the 68000
intervened, you're off to Guru City; this risk is small enough to be
acceptable.
We now have to consider how to write a handler routine that will get you
through the trap and into your deallocation code, and how to link this
routine into the system so that it will be invoked when a trap occurs. We
will outline what we believe to be the simplest approach to this problem in
the following sections.
Writing a Trap-Handler
As described above, the 68000's behaviour during a trap involves entering
supervisor mode, pushing the CCR and the program counter, and jumping into
the system handler code through a vector. If this were the Commodore 64
rather than the Amiga, the obvious approach would be to change that vector
to point to our own code. In a multitasking system, however, we can't take
that kind of liberty; if we did, ANY task that encountered a trap
would end up using the handler written for our task alone. (Besides, we'd
have to change not just one, but ALL the vectors in the table -
one for each kind of trap.) Instead, we have to use another vector, one
that is specific to our task. This is provided in the "Task Control Block",
a structure that Exec maintains for each task in the system. A task can get
a pointer to its task control block (a "Task" structure) by calling the
Exec function "FindTask()" with a parameter of zero. Once we have the
pointer to our task control block, we can change the member called
"tc_TrapCode" to point to the code to be executed when an exception
occurs.
Changing tc_TrapCode to point to our own routine is very simple.
Unfortunately, we can't leave it at that, because our trap code will be
executed from within a CPU exception, and trap routines, like interrupt
routines, are limited in their capabilites. For example, trap or interrupt
code can't call any function that requires multitasking to do its job -
this includes "printf()", commonly used in C programs to print text to the
console. Another problem is that during an exception, the stack pointer
(A7) points to the supervisor stack, not our task's private user stack, and
we don't want to go messing with the supervisor stack (usually). Finally,
an exception handler must end with an RTE instruction (ReTurn from
Exception), but we probably want to finish execution of our cleanup routine
with an 'Exit()', to remove our process (AmigaDOS's higher-level view of
our task) from the system.
As you may have guessed, now comes the fun part where you get to find the
solution to these problems. What we have to do is exit from our exception
code with an RTE instruction, and THEN have control passed to our
special cleanup and exit code. To do that, we have to "mess with the system
stack", which, as you may have heard before, you usually don't want to do.
In this case, the address of the program counter on the stack can be
modified so that when the RTE instruction is executed, the CPU will run the
program of our choice (the cleanup routine) instead of continuing with the
nasty code that caused the exception in the first place. Before the RTE, we
have to pull the trap number from the stack, which the system trap handler
put there for us. This tells us why the CPU generated the exception - the
list of possible trap numbers appears at the end of this article.
A small complication is that the exceptions for "bus error" (trap number 2)
and "address error" (trap number 3) push an extra 8 bytes of data onto the
supervisor stack. One way to handle this in the trap code is by checking
the trap number and advancing the stack pointer past the 8 bytes for
trap numbers less than 4.
Another issue that should be addressed by a truly general trap handler is
the fact that other members of the 68000 microprocessor family arrange
their stacks differently on an exception. Some Amiga users are replacing
their 68000s with a 68010 or even a 68020 for added speed. The Amiga's
operating system is designed to work with these CPUs, and so should
application software, if possible. The trap code could check the CPU type
(Exec provides a field for this purpose in the ExecBase structure) and
adjust the stack pointer accordingly.
So, to recap the above, the following steps are required to have your task
clean up and leave gracefully when a CPU exception occurs:
1) Call FindTask() to get a pointer to the task's "Task" structure.
2) Change the "tc-TrapCode" member of the Task structure to point to a short
machine language routine that does the following:
2 (i) Change the program counter on the stack to point to your
"clean up and exit" routine.
(ii) pull the trap number from the stack
(iii) perform an RTE instruction to exit the exception handler and pass
control to the clean up function.
Your "clean up and exit" function should free any memory your task
allocated, close any screens, windows, fonts, libraries, devices, etc. that
it opened, and then call "exit()" (from C) or the equivalent to end
and kill the task.
Our Solution: TrapSnapper
Lucky for you, all of this has been done for you in the short program
presented here called "TrapSnapper". TrapSnapper is set up for C-language
trap handling, which takes a bit more set-up than doing it in assembler,
since there is some code that HAS to be in assembler.
All you need to do to put a decent trap-handler in your program is:
1) Put a structure template declaration at the start of your file or in
a header file that you #include (see listing)
2) Declare an instance of that structure at the top of your file and
initialize it with 15 words making up a short machine language routine.
(see listing)
3) Call the function SetTrap() early in the program's execution
to initialize the trap handler.
The C code presented here is a simple program that shows how to do
this, and demonstrates the effectiveness of the trap handler by generating
two kinds of CPU exceptions that would normally result in a Software Error.
The program itself just takes the argument supplied on the command line
(when the program is invoked from the CLI) and converts it to an integer.
If the value is 1, the program forces an address error by attempting to
read a word (16 bits) from an odd address. If the value is not 1, the
program tries to divide the value into 100 - causing a divide by zero
exception, of course, if the value entered was zero.
If this program was compiled without the call to SetTrap(), it would cause
a software error if it was run and given zero or one as an argument. As it
stands, with the TrapSnapper in place, it just prints a warning message and
the trap number, and exits gracefully, removing itself from the
system without a trace.
Here's how the trap code works in C. The intial trap code must be in
assembler so that we can access the stack pointer A7 directly and perform
an RTE instruction. To do this in C, the machine code is set up as a static
array of words and a pointer to this array is put into the Task structure's
"tc_TrapCode" member. The machine code needs a pointer to the clean up code
(the CleanUpAndExit() function in this example), and a place to store the
trap number. These long-words are stored immediately before the machine
code itself through the use of the "TrapData" structure defined at the
beginning of the program. Include this structure template declaration at
the top of your file or in a header file that you #include.
The assembly code appears as comments in the C listing for TrapSnapper. The
code is fairly straightforward: it pulls the trap number from the stack and
stores it in the MyTrap structure where the C code can get at it; adjusts
the stack pointer if the trap number was less than 4 to allow for bus and
address errors; then replaces the program counter on the stack with the
address of the clean-up routine (which was put in the MyTrap structure by
the SetTrap() function). Finally, it ends with an RTE instruction. As you
may recall, we mentioned that the exception stack frame was CPU-dependent,
and a general trap-handler should work for the 68010 and 68020 as well as
the 68000. Well, we're good at giving advice, but this trap-handler isn't
that general. Sorry, 68010/68020 users, if this routine doesn't work on
your machine.
An instance of a TrapData structure (MyTrap) is then declared and
initialized - this is where the machine language is created. The pointer to
the clean-up code will be put into this structure by the SetTrap()
function.
The SetTrap() function finds the pointer to the task's Task control block,
then puts a pointer to the trap machine code into the "tc_TrapCode" member.
Finally, it puts a pointer to the function called CleanUpAndExit() into the
TrapData structure called MyTrap.
The CleanUpAndExit() function is where all of the clean-up code for the
program goes. In this case, it just prints a message and performs the C
function exit() (The DOS Exit() function could be used instead). It also
prints the exception that was encountered by looking at the TrapNum member
of the MyTrap structure. In a more typical program, the CleanUpAndExit
function would close any Intuition Screens and Windows that the program had
opened, free any memory it had allocated (including graphics memory like
rasters), and close open fonts, devices, and libraries. In short, anything
that the program would do to clean up after itself before it exits should
be done in CleanUpAndExit().
Whether you fully understand the details of the trap handler or not, you
can easily add it to your own C or assembler programs. Every program should
have its own trap handler to spare the user from the plague of the Software
Error when things go wrong. You might not be able to get all the bugs out
of your programs, but with TrapSnapper, at least you can make them less
harmful.
List of Trap Numbers:
2 - Bus Error
externally generated signal from Amiga Hardware
3 - Address Error
word or longword instruction attempted at odd address
4 - Illegal Instruction
a meaningless op-code was encountered
5 - Division by zero
the source operand of a DIVS or DIVU instruction was zero
6 - CHK instruction
operand of a CHK instruction fell outside of specified bounds
7 - TRAPV instruction
overflow (V) set when a TRAPV instruction was executed
8 - Privilege violation
a supervisor-state operation was attempted in user state
9 - Instruction trace
generated after each instruction while in trace mode
11 - 1010 Emulator
an op-code starting with the bit-pattern 1010 was encountered
12 - 1111 Emulator
an op-code starting with the bit-pattern 1111 was encountered
32 through 47 - TRAP instructions