Skip to main content
© Photon Storm (Ilkke & Rich)

TRAP IN AND TRAP OUT by Ronald van der Kamp

How to fiddle with some low-level stuff

1. Introduction

 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 
IOTRANSFER.)
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 
statement:

 "CODE(4E40h + 11);"

What we need is also a procedure as:

PROCEDURE NewTrap11;
BEGIN (* do your trap actions here*) 
....
CODE(4E73h); (* RTE=ReTurn from Exception*)
END NewTrap11;
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 
becomes active.
  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 
set o.k.)
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;
and declare:

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:

(*$P- *)
PROCEDURE NewTrap11;
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 
later on.

We know that there is an exception frame on the supervisor stack. 
If we define 

TYPE exceptionFrame = RECORD
                        Stat  :   statusRegister;
                        PC : ADDRESS END;

with

TYPE statusRegister = SET OF status;

whereby

TYPE status = (carry,overflow,zero,negative,extend,
               r1,r2,r3,i1,i2,i3,r4,r5,supervisor,trace);

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:

ELSE
  CODE(4E68h);(* MOVE USP,A0 a priveleged instruction *)
END; (*if*)

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:
declare:

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;

ELSE
  CODE(4E68h);(* MOVE USP,A0 *)
  CODE(0D0FCh,TSIZE(functionFrame));(* ADDA.W size,A0 *)
  CODE(4E60h);(* MOVE.L A0,USP *)
END;(*if*)

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:

SETREG(A0,regSaved.SP);
CODE(4CD8h,7E20h);(* MOVEM.L (A0)+,D5/A1-A6 *)
savedD5 := REGISTER(D5);(* sorry, next action destroys him*)
regSaved.SP := REGISTER(A0);
SETREG(D5,savedD5);
CODE(4E73h); (* RTE *)
END NewTrap11;

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:

BIOS.SetException(trap11VectorNr,NewTrap11);

whereby

 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:

GEMDOS.TermRes(numBytes,exitCode)

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 
dogs.

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 
functionframe:

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) *)
ELSE
  ......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.

9. Conclusion

  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 Netherlands

Disclaimer
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.