TRAP IN AND TRAP OUT by Ronald van der Kamp
How to fiddle with some low-level stuff
You readers, will all have heard by now about TRAPs that are in
use on our ST machines. Some of us, Modula programmers, will have
wondered what these are used for, because we do not have to know
in detail about our traps, the library modules take care of
difficult stuff like that. So in this artikel I will explain the
use of the traps by our operating system and how you can make
your own traps, why you should do it and for what kind of things
to use them.
2.Traps and the operating system.
Let us take for example the action of writing a string to the
midi port. After some searching you will find a procedure
'MIDIWS' in the extended BIOS module (with the name 'XBIOS'). In
the definition module the description is "PROCEDURE MIDIWS(VAR
string: ARRAY OF BYTE; length: CARDINAL);" so that this action is
very simple for us.
On another (lower) level we can see what kind of things should
be done in the implementation of this call. The manuals tell us
that we can write a string to the midi port by doing a trap
number 14, function number 12, with the parameters on the stack.
But what does this mean?
To explain this, I have first to tell you about our CPU , the
68000 and some of its workings.
3. The 68000 and Exception Processing.
When you walk undisturbed for 50.000 miles and then suddenly
falls into a trap, that is something very exceptional to you.
While the CPU is executing instructions, there can come from
outside (the I/O chips for instance) an interrupt; an external
exception. When a division by zero is attempted (and this is
impossible) then an internal generated exception is done. When
the instruction the CPU executes is a TRAP instruction, also an
internal exception is generated. What the CPU then does is the
following (as far as is needed to know at this point):
a. the contents of the CPU status register ( 16 bits) is put on
the supervisor stack;
b. the value of the CPU program counter register (32 bits) is
placed on the supervisor stack;
c. a memory location is calculated as follows: 128+(trapnumber
times 4); so for trap number 11 the address is 172(decimal);
d. the CPU goes into supervisor mode (if he was not already) and
fetches the address (of 4 bytes length), that stands in the
calculated memory position, into the program counter register
and start executing the code from this point.
By using traps it is possible to write software that will
function o.k. with different versions of the TOS. (I know of 4
versions now, but maybe there are more) Although the routines
that cater for the traps may be in different places in memory
(depending on your TOS version), the address of the start of a
traphandler code is always to be found in the same (low) memory
location. ( see point c. above)
4. Inheritance of Traps.
When the TOS system is booted, the following traps are set and
usable: Trap #1 for GEMDOS calls, Trap #2 for GEM/GSX, trap #13
for the BIOS and trap #14 for the XBIOS. These correspond with
our TDI library modules with the names GEMDOS,( nothing for
#2),BIOS and XBIOS. As soon as a modula program starts, he will
find these traps available (he inherits them).
As you know, any modula program needs at least one other link
file with the name GEMX. (and accessories need a different
version of this LNK file). This GEMX module contains the Modula-
run-time-system that has some initial actions to perform, before
your own code is executed. One of its actions is installing the
traps numbers 5 (for IOTRANSFER), number 7 (for TRANSFER) and
number 8 for the runtime-errors. (For version 2 of TDI Modula
there is an error in the manual; not trap #6 but #5 is used for
So, as long as there are no accessories or other
'concurrent' programs in the machine (as for instance programs in
the vertical blank list) that meddle with traps, all goes well.
5.Trap installation and use.
Doing a Trap is very simple in principle. Say, want want our own
Trap #11 handling. Activating the trap can be done with the
"CODE(4E40h + 11);"
What we need is also a procedure as:
BEGIN (* do your trap actions here*)
CODE(4E73h); (* RTE=ReTurn from Exception*)
to perform our actions when the trap is activated.
Now things become less simple. First we shall want to pass some
values to a traphandling procedure, just as you pass values to
every normal procedure with a parameter list. The parameter
passing as done in the Modula code is not compatible to the
mechanism of the exception processing of the CPU.
The accepted method for passing values to procedures is by
putting them 'on the stack' before the procedure call and have
the procedure 'read them from the stack'.
The activation of our trap with one parameter of WORD size ( the
value 15 in this case) would be done with:
SETREG(D7,15)(* with CONST D7=7; according to TDI convention*)
CODE(3F07h);(* MOVE.W d7,-(A7) =put word on stack*)
CODE(4E4Bh);(* TRAP #11 =do the trap now*)
CODE(548Fh);(* ADDQ.L #2,A7 = clean the stack (2bytes=1word)*)
This way of passing parameters makes things for the reciever
(the traphandling procedure) rather complicated.
First of all the traphandler should not have any entry code with
him. So use the compiler option (*$P- *) to stop the generating
of entry and exit code by the compiler.
The procedure header should not have a parameter list, and there
should also be no variables declared local within the handler (No
VAR statement). Remember that by using the (*$P- *) compiler
option, no exit code is generated for the procedure, so the last
executed statement should be ( in our case ) a RTE.(and not an
RTS (ReTurn from Subroutine) as would be normal.)
As told before, the CPU puts itself into supervisor mode when
the exception processing is initiated. So from the first
statement of the traphandler to the last ( the RTS) the CPU is by
default in supervisor mode. In our case this means trouble
because the stackpointer register A7 is a little bit 'dual'.
When we talk about address register A7 and the CPU is in
supervisor mode, the register A7 denotes the Supervisor Stack
Pointer, and when in user mode this register A7 contains the User
Stack Pointer. When the CPU is in supervisor mode there are
privileged instructions like 'MOVE USP,A0' that let you get the
value of the User Stack Pointer, while A7 contains the Supervisor
Stack pointer. It stands to reason that in user mode you cannot
get at the supervisor stack.
We have said that it is a nice thing to be able to pass values
to the traphandler; values that are 'put on the stack'. Now it is
considered a good programming technique not to make any
restrictions on the status of the CPU when a trap is initiated.
The program that activates the traphandler will normally be in
user mode, but it is not right to suppose that it is always the
case. So how can the traphandler know in what state the program
was that activated him?
As told before, when the CPU does an exception processing, the
contents of the status register is put on de supervisor stack.
And this stack is pointed to by register A7 when the traphandler
Before we illustrate this article with some lines of Modula
statements, a small remark first.
In the traphandling procedure there will always be a use of
some CPU registers. When we exit the traphandler we should
restore the registers to the values they contained when the
traphandler was activated. (and also the stackpointers should be
6.How we make a traphandler.
First we take care of the saving of used register from the
traphandler. We define:
TYPE regStack = RECORD
heap: ARRAY [0..127] OF ADDRESS; (*stackspace*)
SP : ADDRESS (* our own StackPointer*) END;
VAR regSaved : regStack;
then there is some initialisation needed:
WITH regSaved DO SP := ADR(SP) END;
and so our own stack is now ready to be used.
The traphandler now starts with:
BEGIN (* in super mode*)
SETREG(A0,regSaved.SP); (* set our own stackpointer*)
CODE(48E0h,047Eh);(* MOVEM.L D5/A1-A6 -(A0) *)
(* these registers will be used in this handler *)
regSaved := REGISTER(A0); (* set our own stackptr *)
Where A0 equals 8 according to the TDI convention of numbering
the registers for the REGISTER and SETREG functions. In this way
the register contents we are going to destroy, are kept safe by
copying them to our own stack, so we are able to reconstruct them
We know that there is an exception frame on the supervisor stack.
If we define
TYPE exceptionFrame = RECORD
Stat : statusRegister;
PC : ADDRESS END;
TYPE statusRegister = SET OF status;
TYPE status = (carry,overflow,zero,negative,extend,
Further on we declare:
VAR toExceptionFrame : POINTER TO exceptionFrame;
and now we can get the pointer to the exception frame with:
toExceptionFrame := REGISTER (A7); (* A7=15 according to TDI *)
If we now ask:
IF supervisor IN toExceptionFrame^.stat THEN
we know if the function frame, containing the parameters we pass
to the traphandler, is on the supervisor stack, or in other words
: if the activator was in super mode or user mode and so placed
its info on de super- or userstack.
If the last statement is true then we perform the actions:
CODE(204Fh);(* MOVE.L A7,A0 *)
CODE(0D0FCh,TSIZE(exceptionFrame));(* ADDA.W size,A0 *)
because the stack grows downward, we add (6 bytes) to the copy of
the super stack pointer in CPU register A0. In the other case we
can get the function frame more simple:
CODE(4E68h);(* MOVE USP,A0 a priveleged instruction *)
Now we have to save the pointer to the function frame:
toFunctionFrame := REGISTER(A0);
In our example the function frame is:
TYPE functionFrame = RECORD fNr : CARDINAL END;
and a declaration of
VAR toFunctionFrame : POINTER TO functionFrame;
to keep the address of the frame.
Now that we have the pointer to the functionFrame we can read
our parameters from the function frame with statements like:
yyy := toFunctionFrame^.fNr;
By having the values of the given parameters to our disposal, all
needed actions of the traphandler can be done, but take care not
to change A7 (SSP) or USP. Do not suppose there is much space
left on either of these two stacks. Every call to a procedure
will use up at least 4 bytes on the stack (for the return
addres), so do not nest too many calls!
You are strongly adviced to take some actions to set all the
stacks o.k. before exiting the traphandler (with an RTE
instruction). The following path should be taken:
in case the function frame is on the supervisor stack, it should
be removed and the exception frame should be shifted 'upward'
with the needed amount (= the length of the function frame). With
a copy of the exception frame available, the actions are simple:
VAR savedExcFrame : exceptionFrame;
and the stack is cleaned with the statements:
If supervisor IN toExceptionFrame^.stat THEN
savedExcFrame := toExceptionFrame^;
CODE(0DEFCh,TSIZE(functionFrame));(* ADDA.W size,A7 *)
toExceptionFrame := REGISTER(A7);
toExceptionFrame^ := savedExcFrame;
CODE(4E68h);(* MOVE USP,A0 *)
CODE(0D0FCh,TSIZE(functionFrame));(* ADDA.W size,A0 *)
CODE(4E60h);(* MOVE.L A0,USP *)
All kind of other actions can be done now, but do not end simply
with and RTE; restore first all the registers we had in use. (
see the start of the traphandler) with:
CODE(4CD8h,7E20h);(* MOVEM.L (A0)+,D5/A1-A6 *)
savedD5 := REGISTER(D5);(* sorry, next action destroys him*)
regSaved.SP := REGISTER(A0);
CODE(4E73h); (* RTE *)
Do not let your understanding of this piece of code be clouded by
the D5 stuff; a habit of the generation of code by the compiler
is to use D5 as intermediate for even the most simple
assignments. The central point of the code is to fill all
registers of the CPU with values they contained when the handling
of our trap was started. A copy of these values was stored in our
own stack. The stackpointers A7 (SSP) and USP are already earlier
cleaned up, so an RTE is possible.
7.The installation of a Trap
In the module 'BIOS' we find a procedure to install a trap. Of
course, strictly spoken, we can put the adress of our traphandler
in the right memory position (172 for Trap #11), but this is very
much bad programming practice. Always use a system function call,
because you never are sure whether your idea of trapinstallation
is what the system itself does. The statement becomes:
CONST trap11VectorNr=43; (* = 172 DIV 4 *)
Mostly you will want a construction in which a program is
activated at boot-time ( what can be done by putting the .PRG
file in the AUTO folder of the boot-disk), then installs the
traphandler, and deactivates itself, keeping the code of the
traphandler in memory. In this case you must take care of ending
the installation program not in the normal way ( that is with a
call to the GEMDOS.Term ) but with a call of:
whereby numBytes gives the number of bytes of memory that are
kept (should be as much as the program needs for itself)
counting from the start of the program code. ExitCode is 0 (zero)
for no-error.(otherwise >0)
This termination call will deactivate the program and keep the
memory block intact by not giving it back to the free memory
space. So later programs and data will never overwrite this
program code. The reason for using this call is that you must not
end up in a situation where the traphandler is noted down as
being available, while the code of the traphandler is gone to the
8. Redefinition of available (system) traps.
When you wish to redirect all the output of the printer port to
say the Midi port, you will need an own traphandler, that takes
the place of the system function GEMDOS.PrnOut. This function is
implemented as Trap #1 with function number 5 and the character
to print is on the stack ( in a WORD, because the stack serviced
by CPU register A7 works in units of WORDs).
The program that installs the traphandler has to save the
address of the trap number 1 that is already in existence ( the
normal system trap). This is done with:
oldTrap1 := BIOS.GetException(trap1VectorNr);(* vector=33*)
and in the traphandler we do a test after we have found the
IF toFunctionFrame^.fNr <> 5 THEN (* nothing for me to do *)
SETREG(A0,regSaved.SP);(* restore the registers *)
CODE(4CD8h,7E20h);(* MOVEM.L (A0)+,D5/A1-A6 *)
savedD5 := REGISTER(D5); (* next statement uses D5 *)
regSaved.SP := REGISTER(A0);(* set stackpointer back*)
SETREG(D5,savedD5); (* D5 got smashed *)
SETREG(A0,oldTrap1);(* the normal system trap *)
CODE(4ED0h);(* JUMP(A0) *)
......my trap handling actions
Make sure that, when you trap through to the old system trap
handler, the whole situation should be restored that was in
existence when the traphandler was activated.
When you want to have some extra built-in functions in your ST
so that it looks like an extended TOS is invented, or when you
need one or more operating system functions to behave different,
you should construct your own traphandler and make a program to
install that traphandler in the operating system. There is a
listing of a trap test program written in Modula with this
article and a ready to run version (=compiled and linked) of the
test program will be somewhere on this disk.
When you make the traphandler and starts with the rather minimal
source code of the test program, it will not be unduly difficult
to make a usefull traphandler. The TDI post-mortem debugger is
not a useable tool for debugging traphandlers. So build your
traphandlers 'bit by bit', otherwise it becomes very frustating
to develop this kind of software.
(c) Stichting Modula Nederland (april 1987)
Bakkersteeg 9 A
2311 RH LEIDEN
The text of the articles is identical to the originals like they appeared in old ST NEWS issues. Please take into consideration that the author(s) was (were) a lot younger and less responsible back then. So bad jokes, bad English, youthful arrogance, insults, bravura, over-crediting and tastelessness should be taken with at least a grain of salt. Any contact and/or payment information, as well as deadlines/release dates of any kind should be regarded as outdated. Due to the fact that these pages are not actually contained in an Atari executable here, references to scroll texts, featured demo screens and hidden articles may also be irrelevant.