A Programmer's Eleven Commandments for Coexistent Vector Stealing
Or, Tried and True Techniques Used by the CodeHeads for Successfully Intercepting Vectors in the Midst of Numerous ST Vector Thieves.
Copyright 1990 John Eidsvoog and Charles F. Johnson (CodeHead Software)
Last revised: Wednesday, February 14, 1990 5:47:44 pm
We have prepared this document in the interest of attaining and furthering compatibility between resident programs and accessories for the Atari ST. Since the TOS operating system has no provisions for managing its interrupt and trap vectors, ST developers who need to intercept these vectors are forced to use the "trial and error" system to determine what works.
This is a very dangerous situation. More and more programs are appearing which enhance the ST's GEM operating system by patching into the vectors which handle system calls. Many of these programs work perfectly as long as no other resident programs are used, or as long as certain combinations of programs are used. But when these programs are released into the "real world," the conflicts quickly start showing up.
At CodeHead Software we've encountered more than our share of these types of problems, since almost all of our commercial products intercept one or more of the ST's system vector(s). From this boiling witches' brew of potential pitfalls, we've managed to distill some pragmatic methods that can alleviate most, if not all of the conflicts.
If you follow these guidelines when programming Atari ST applications which require the interception of system vectors, you will be compatible with _most_ of the programs currently in use. At the very least, your code will be compatible with all of the CodeHead Software products. If any program has general compatibility problems with other resident programs or accessories, it's very likely that the offending program is breaking one of the following Eleven Commandments:
I.
Always fall through to the previous address when your routine has completed its function. (The only exception to this rule is if your code replaces an entire system call; in this case, you'll probably want to terminate your routine with an RTE. Be aware that if you do this, any program which was previously installed in that vector will not "see" this call come through.) The "fall through" can be accomplished by storing the previous vector address two bytes past a JMP instruction; this approach solves any possible problems with pushing the return address on the stack (see Commandment V below), or destroying an address register to do an indirect JMP.
There are some cases where it doesn't make sense to fall through to a previous routine, such as when you replace the Alt-Help vector which performs a screen dump. Even here, however, it's a good idea to make allowances for other programs which may use the Alt-Help vector for purposes other than a screen dump...such as the Templemon and AMON debuggers. AMON avoids conflicts with other programs in the Alt-Help vector by requiring the user to press the left shift key in addition to Alternate and Help.
Another special case where falling through makes no sense is the ST's vertical blank queue list, which allows you to install a routine to be executed as a subroutine from the main system VBI. There are eight entries in the default queue list, and the correct way to install a routine in one is to search the list for a zero longword. When your VBI queue routine is finished, it may remove itself by clearing its entry in the list. (This is why it makes no sense to fall through to a previous queue entry -- that entry should have been zero when you grabbed it.) Even this mechanism is subject to abuse, however; an unfortunate number of programs simply stuff an address into one of the queue slots, without checking first to see if that slot has been taken. (A good example of this kind of vector abuse is the first version of STARTGEM.PRG.) Remember: when using the vertical blank queue list, always search the list for a zero entry in which to install your routine.
With more and more programs appearing that replace entire operating system functions, compatibility is going to become even more problematic. For example, clashes will occur because Program A needs to "see" a certain call being made, but Program B is intercepting the call, handling it, and returning to the caller. In this scenario, Program A will just stop doing anything since it will never see the call for which it's watching. Keep this in mind when you're writing code intended to replace an entire system call; and be sure to test your code with as many other resident vector-grabbers as possible.
II.
Never replace a vector after grabbing it, unless you're in a controlled situation where there is no chance that another program could intercept the same vector and fall through to your code. Here's an example of what can go haywire if you do replace a vector at the wrong time:
An early public domain ST program had a feature to select DESKTOP.INF files for different resolutions. The program grabbed the trap #1 vector (GEMDOS) and then used the Ptermres() call to make itself resident. Then, as a resident program, it monitored all GEMDOS calls, looking for the Fopen() call for the filename DESKTOP.INF. When that call was detected, the program replaced the system's filename ("DESKTOP.INF") with either LOW.INF, MEDIUM.INF, or HIGH.INF depending on the current resolution. Then it made the big mistake -- to remove itself, our example program took the address that it originally found in the trap #1 vector (when it first ran) and stored it back into the vector.
Why is this such a big mistake? Because other programs that can run AFTER our example program may also need to grab the trap #1 vector. If this happens, the next program to install itself in trap #1 will be CUT OUT of the chain of fall-throughs when our example program replaces the vector. If you're lucky, the only ill effect will be that one of your TSR's will suddenly stop working. If you're unlucky, the system will crash or hang. (It all depends on what the program that got cut out of the chain was doing with that vector.)
Oh, and by the way, our unnamed example program has since been updated to fix this thorny problem. The fix was simple; the program now remains in the trap #1 vector after replacing the system's DESKTOP.INF filename. After doing its job, the code does nothing but fall through to the previous vector.
If you are a resident program and you want to remove yourself, do it by setting a flag to bypass your code and fall through (see Commandment I.) Remember that some other program may run after yours and grab the same vector; in this case, the other program will be falling through to your code. If you remove yourself by replacing the original vector address, you'll also be removing everything else that ran after you.
III.
Don't use a "magic cookie" (the infamous Diablo emulator mistake). That is, if you are trying to find another program (or yourself), don't look for a "magic" word near the address in the vector that the program steals. This technique will fail as soon as some other program grabs the same vector; and this is exactly how the Diablo emulator (for the SLM804 laser printer) breaks. The Diablo emulator consists of two separate programs -- one that goes in an AUTO folder (the emulator code itself), and a configuration program that installs as a desk accessory. The AUTO program grabs the BIOS vector, so that it can redirect printer output to the laser via the DMA port. The desk accessory configuration program tries to find the AUTO program (every time it's activated) by looking for a "magic cookie" stored by the AUTO program in the location immediately before its BIOS interception code. Problem: if another program intercepts the BIOS vector AFTER the Diablo emulator AUTO program, the configuration accessory is unable to find the AUTO program (because the "magic cookie" is not where the accessory thinks it should be).
There are a number of ways to reliably find another program. One of the easiest is to make a "fake" call to one of the trap routines with an undefined function code. The ST's BIOS and XBIOS will ignore calls with undefined function codes, and simply return with no ill effects if the program you're searching for is not present. We suggest using unusual function codes, such as $4857 (for example), so that your code will not conflict with future additions to the BIOS or XBIOS functions. The receiving program can then return whatever kind of information you need from it (you've got lots of registers to use).
Here's an example (in assembly language) of some code that uses an undefined BIOS call to detect the presence of another program:
*-------------------------------------------------------------------
*
* The "target" program (the program being searched for) must intercept
* the trap #13 vector and examine the stack after each trap #13 call
* to see if the magic word function number is present. If it is, the
* target program should load the return value into d5 and perform an RTE.
*
moveq #0,d5 ; Clear d5 in preparation
move #$4857,-(sp) ; Magic word - undefined BIOS call
trap #13 ; Call BIOS
addq #2,sp ; Correct the stack
tst.l d5 ; If d5 is still zero, we didn't find anyone
beq.s notfound ; If non-zero, it's a returned value
move.l d5,returned ; Save the returned value somewhere
*-------------------------------------------------------------------
(NOTE: The version of TOS (1.6) that will be supplied with Atari's STE and TT machines has a new feature called the "Cookie Jar," which does not suffer from the problems described here. It provides a documented address where programs can search for "magic cookies"; it's a nice solution. Our only complaint with the "Cookie Jar" is that we wish it had been implemented three years ago.)
IV.
Do not try to monitor and maintain a vector from a vertical blank or other timed interrupt (in other words, don't keep watching it and replacing it if it changes). Think for a moment about what happens if two programs do this at the same time. (Ouch.) This extremely bad practice may seem to work when no other programs are using the same vector, but you will definitely have coexistence problems down the road. Don't do it.
V.
Do not use the (system) stack from an interrupt or trap vector. There is _very_ little stack headroom available in the location used by the operating system. A system stack overflow will cause crashes that can be extremely difficult to diagnose.
If you need to save registers during some vector-handling code, it's best to save them in a location in your own program, instead of on the system stack. For example:
*---------------------------------------------
movem.l d0-a6,-(sp) ; Don't do this!
*---------------------------------------------
movem.l d0-a6,regsave ; Do this instead.
*---------------------------------------------
VI.
Always restore all registers and the status register when your routine is finished. Don't even assume that you can destroy D0 or A0 because some programs (believe it or not) actually rely on them to return from a trap unchanged. (The exceptions to this rule are the BIOS and XBIOS vectors; the dispatching routines for these vectors always trash register A0, so it's safe to use A0 in a BIOS or XBIOS routine without saving it.)
VII.
Don't alter the processor state. That is, don't 'rte' into your own code in order to be in USER mode because other programs down the line may expect the machine to be in SUPERVISOR mode.
VIII.
When intercepting frequently called traps (such as trap #2), always use optimized assembly language routines to eliminate a slowdown in system operation. Don't make the "GDOS mistake".
IX.
Never assume something simply because it always "seems to be." This includes using "hard" addresses specific to a particular ROM, assuming that certain vectors will be pointing to ROM routines, assuming that 8 bytes into the GEM base page is pointing into the OS, or making _any_ decision based on an empirical condition.
X.
Use the source code provided below for maintaining the trap #2 vector from a resident program. This somewhat oblique method is required because the operating system stuffs its own address into the trap #2 vector (with no regard for what is there) after running a TOS program, and possibly at other times as well. (Yes, we are aware that this routine breaks Commandment IX.) The routine which handles trap #13 in this code also demonstrates a method to remain compatible with 68010/68020/68030 processors, by checking a new BIOS variable Atari has documented.
XI.
Commandment XI may be the most difficult one to follow. Have the wisdom to know when it's necessary to break any of the other commandments, and the responsibility to think through the consequences if you do. Some of these rules should _never_ be broken; others can be bent once in a while, as long as you carefully consider all the ramifications. Above all, just as in any other endeavor, you have to learn the rules and understand the reasons for their existence before you can get away with breaking them.
*****************************************
* *
* Intercept the trap #2 vector *
* *
* Code by Charles F. Johnson *
* *
* Includes ideas, techniques and *
* refinements by Bob Breum, *
* Chris Latham, and John Eidsvoog *
* *
* Last revision: 06/26/88 12:13:32 *
* *
*****************************************
.TEXT
* ------------------------
* Program initialization
* ------------------------
move.l #prog_end,d6 ; Get address of end of this program
sub.l 4(sp),d6 ; Subtract start of basepage - save in d6
move.l #not_auto,addrin ; Try to do an alert box
move #1,intin
move.l #f_alrt,aespb
move.l #aespb,d1
move #$C8,d0
trap #2
tst intout ; If intout is zero, we're in \AUTO
beq.s .start1
cmp #1,intout ; Install?
beq.s .0 ; Yes, continue
clr -(sp) ; Pterm0
trap #1 ; outta here
.0: pea prg_start(pc) ; Steal trap #2 right away if run from desktop
move #38,-(sp) ; Supexec
trap #14
addq #6,sp
move #1,prgflg ; Set flag indicating desktop load
bra.s .start2
.start1:
pea title ; Print title message
move #9,-(sp)
trap #1
addq #6,sp
.start2:
dc.w $A000 ; Don't you just love Line A?
move.l a0,line_a ; Save the address of the Line A variables
pea set_bios(pc) ; Appropriate the Trap #13 vector
move #38,-(sp)
trap #14
addq.l #6,sp
clr.w -(sp) ; Terminate and Stay Resident
move.l d6,-(sp) ; Number of bytes to keep
move #$31,-(sp) ; That's all folks!
trap #1 ; We are now happily resident in RAM
* -------------------------------
* Desktop vector initialization
* -------------------------------
prg_start:
move.l $88,t2_vec ; Set my fall throughs
move.l $88,aesvec
move.l #my_trap2,$88 ; Steal trap #2 (GEM)
rts
* -----------------------
* Steal the BIOS vector
* -----------------------
set_bios:
move.l $B4,t13adr ; Set Bios fall through
move.l #my_t13,$B4 ; Steal trap #13 (BIOS)
rts
* ------------------------
* Trap #13 wedge routine
* ------------------------
my_t13:
btst #5,(sp) ; Was the trap called from super or user mode?
beq.s t13_ex ; If from user mode, bail out
lea 6(sp),a0 ; Pointer to function code on stack
tst $59E ; See what _longframe has to tell us
beq.s notlng ; If _longframe is zero, it's a 68000
lea 8(sp),a0 ; Advance past the vector offset word
*** This section is based on the assumption that the OS always calls
*** BIOS setexec() immediately after obnoxiously grabbing back the trap
*** #2 vector with no warning whatsoever. Yes, this is an empirical
*** condition, which violates Commandment IX. (But there's no other
*** way to prevent that no-good, thieving TOS from ripping off the
*** vector while you aren't looking.)
notlng: cmp.l #$050101,(a0) ; Setexec call for critical error vector?
bne.s t13_ex ; Nope, exit
tst prgflg ; On the desktop? Or are vectors already set?
beq.s first_time ; No, skip ahead
do_crit:
move.l #my_trap2,$88 ; Pilfer trap #2
move.l $404,d0 ; Get current crit vector
move.l 4(a0),d1 ; Get address we're setting it to
bmi.s t13_x1 ; If minus, return old vector in d0
move.l d1,$404 ; Set that vector
t13_x1: rte ; We only get here if we're last in the chain
first_time:
tst.l 4(a0) ; Reading the vector?
bmi.s t13_ex ; Yes, let the system take care of it
move.l $4F2,a1 ; Get address of OS header (could be in RAM)
move.l 8(a1),a1 ; Get pointer to base of OS from header
cmp.l 4(a0),a1 ; Is the crit error routine below the OS?
bhi.s t13_ex ; Yes, bail out
move.l $14(a1),a1 ; Get address of end of OS (GEMDOS parm block)
cmp.l 4(a0),a1 ; Is it above the OS?
blo.s t13_ex ; Yes, exit stage left
*** This is a very important part of the code. In order to maintain the
*** correct vector chaining order when running at \AUTO time, it's necessary
*** that each program first fall through to the BIOS and RETURN TO ITS OWN
*** CODE, grabbing the trap #2 vector on the way back. This way, the order
*** that each program intercepts trap #2 is the same as the order in which
*** they run from the AUTO folder.
move #1,prgflg ; Set the 'first-time'/'desktop' flag
move.l 2(sp),retsav ; Save return address
move.l #t13_2,2(sp) ; Replace it with my own
t13_ex: jmp $DEADBEEF ; Go to the Bios and come back,
t13adr = t13_ex+2 ; maintaining the correct chaining order
t13_2: bsr prg_start ; Grab the trap #2 vector on the way back
move.l retsav(pc),-(sp) ; And return to the caller
rts
retsav: dc.l 0
*--------------------------------------------------------------------------
The techniques described here have worked successfully for us, both in our CodeHead Software products and our individual projects. However, we do not wish to appear as the final and absolute authorities on this subject. If you can find any flaws in our scheme, or perhaps enlighten us with a more efficient trick, we can be easily reached. The quickest way to get a reply is to leave a message in the CodeHead Category (#32) on GEnie or leave GEnie mail to C.F.JOHNSON or J.EIDSVOOG1. You may also call CodeHead Software at (213) 386-5735.
NOTES ON THE GERMAN "XBRA" PROTOCOL
For quite some time we've been hearing rumors about a new "standard" protocol devised in Germany, which supposedly can prevent some of the problems with conflicting vector-grabbers. It's called the "XBRA" protocol -- here's how it works:
When a program needs to intercept a trap or interrupt vector, it should put the previous vector address four bytes before the beginning of its routine, preceded by two longwords. The first longword before the address should be a unique identification code for your application. The second longword before the previous vector address should be the magic longword "XBRA" ($58425241). So, in assembly language, the code would look something like:
*----------------------------------------------------------------
dc.l 'XBRA' ; Magic longword signifying XBRA protocol
dc.l 'BRAT' ; Unique (hopefully) 4-byte ID
oldvec: dc.l 0 ; Put the previous vector address here
my_vector_routine: ; Your vector-handling code starts here
*----------------------------------------------------------------
In order for this protocol to really work, the vector interception code should also use the previous vector address stored in the XBRA structure to fall through to the previous routine. This way, if it's necessary to restructure the fall-through chain, any vector interception code will automatically start falling through to the new address.
*----------------------------------------------------------------
move.l oldvec(pc),-(sp) ; One way to fall through to the
rts ; address in an XBRA structure
*----------------------------------------------------------------
move.l oldvec(pc),jump+2 ; Another way to fall through:
jump: jmp $ADEADBEE ; by modifying a JMP instruction.
; This uses more memory, and may
; not work on a 68030 (without
; tweaking), but it doesn't use
; the system stack.
*----------------------------------------------------------------
The main use of XBRA seems to be to allow programs to unhook themselves from a vector chain; it provides a method whereby programs can walk through the chain of vectors, unhook themselves (or unhook other programs!) if necessary, and even restructure the whole chain. Again, it would have been nice if the XBRA protocol were proposed three years ago; if even one program in the chain is not following XBRA, the whole scheme is useless. And since there are _many_ programs that don't use XBRA, the scheme is of little use in the real ST world at the present.
Still, it doesn't take much effort to implement the XBRA protocol, so it may be a good idea to use it in any future vector-grabbing programs. If all programs used XBRA, _some_ of the problems with conflicting vector thieves could be eased. (Why does XBRA remind us of Esperanto, the United Nations-sponsored "international language" that was going to make it possible for all mankind to live in peace?)
(NOTE: In our opinion the XBRA protocol could be improved, by adding a JMP instruction to the XBRA structure immediately before the previous vector address. If the structure looked like this:
*----------------------------------------------------------------
dc.l 'XBRA'
dc.l 'BRAT'
jump: dc.w $4EF9 ; 680x0 absolute JMP instruction
oldvec: dc.l 0 ; Put the previous vector address here
my_vector_routine: ; Your vector-handling code starts here
*----------------------------------------------------------------
then a program could simply branch to the label "jump" to fall through to the previous vector-handling routine.
We must _emphasize_, however, that this is merely an observation on our part. Don't use this suggested extension to XBRA in your code, since the XBRA protocol does NOT support it as of this date.)
It should be pointed out that XBRA is not a panacea; the "Eleven Commandments" we've outlined here are still valid, even if you do employ the XBRA protocol in your code. In fact, since so many programs already exist that do not use XBRA, it's even more important not to rely on the XBRA protocol to solve your problems for you.
***********************************************************
* *
* This document is Copyright 1990 CodeHead Software. *
* All Rights Reserved. *
* *
* May be freely distributed as long as this ASCII text *
* file is complete and unaltered in any way. This *
* document MAY NOT be reprinted or used for commercial *
* purposes without express written permission from *
* CodeHead Software. *
* *
* If you wish to reprint this document, contact us at *
* the phone number given above for permission. *
* *
***********************************************************