Amiga Checkmark
Amiga Enforcer V37.73
by Michael Sinz (MKSoft Development)
Copyright 1992-2001 - All Rights Reserved


Michael Sinz
MKSoft Development
Copyright 1992-2001
All Rights Reserved

Enforcer Archive

Contact me

Source code for the main Enforcer tool:

Source code for the MMU tool:

Source code for SegTracker:

Source code for FindHit:

Source code for RebootOff:

Source code for Move4K:

Source code for LawBreaker:

Amiga Checkmark


Enforcer Enforcer NAME Enforcer V37 - An advanced version of Enforcer - Requires V37 SYNOPSIS Enforcer - A tool to watch for illegal memory accesses FUNCTION Enforcer will use the MMU in the advanced 680x0 processors to set up MMU tables to watch for illegal accesses to memory such as the low-page and non-existent pages. To use, run Enforcer (plus any options you may wish) If you wish to detach, just use RUN >NIL: <NIL: to start it. You can also start it from the Workbench. When started from Workbench, Enforcer will read the tooltypes of its icon or selected project icon for its options. (See the sample project icons) Enforcer should only be run *after* SetPatch. If SegTracker is running in the system when Enforcer is started, Enforcer will use the public SegTracker seglist tracking for identifying the hits. INPUTS The options for Enforcer are as follows: QUIET/S - This tells Enforcer not to complain about any invalid access and to just build MMU tables for cache setting reasons -- mainly used in conjunction with an Amiga BridgeBoard in a 68030 environment so that the system can run with the data cache turned on. In this case, RUN >NIL: Enforcer QUIET should be placed into the startup-sequence right after SetPatch. TINY/S - This tells Enforcer to output a minimal hit. The output is basically the first line of the Enforcer hit. (see below) SMALL/S - This tells Enforcer to output the hit line, the USP: line, and the Name: line. (This means that no register or stack display will be output) SHOWPC/S - This tells Enforcer to also output the two lines that contain the memory area around the PC where the hit happened. Useful for disassembly. This option will not do anything if QUIET, SMALL or TINY output modes are selected. STACKLINES/K/N - This lets you pick the number of lines of stack backtrace to display. The default is 2. If set to 0, no stack backtrace will be displayed. There is NO ENFORCED LIMIT on the number of lines. STACKCHECK/S - This option tells Enforcer that you wish all of the long words displayed in the stack to be checked against the global seglists via SegTracker. This will tell you what seglist various return addresses are on the stack. If you are not displaying stack information in the Enforcer hit then STACKCHECK will have nothing to check. If you are displaying stack information, then each long word will be check and only those which are in one of the tracked seglists will be displayed in a SegTracker line. The output will show the PC address first and then work its way back on the stack such that you can read it from bottom up as the order of calling or from top down as the stack-frame backtrace. AREGCHECK/S - This option tells Enforcer that you wish all of the values in the Address Registers checked via SegTracker, much like STACKCHECK. DREGCHECK/S - This option tells Enforcer that you wish all of the values in the Data Registers checked via SegTracker, much like STACKCHECK. DATESTAMP/S - This makes Enforcer output a date and time with each hit. Due to the nature of the way Enforcer must work, the time can not be read during the Enforcer hit itself so the time output will be the last time value the main Enforcer task set up. Enforcer will update this value every second as to try to not use any real CPU time. The time displayed in the hit will thus be exact. (Assuming the system clock is correct.) The date is output before anything from the hit other than the optional introduction string. DEADLY/S - This makes Enforcer a bit nasty. Normally, when an illegal read happens, Enforcer returns 0 as the result of this read. With this option, Enforcer will return $ABADFEED as the read data. This option can make programs with Enforcer hits cause even more hits. FSPACE/S - This option will make the special $00F00000 address space available for writing. This is useful for those people with $00F00000 boards. Mainly Commodore internal development work -- should only be used in that enviroment. VERBOSE/S - This option will make Enforcer display information as to the mapping of the I/O boards and other technical information. This information maybe useful in specialized debugging. LED/K/N - This option lets you specify the speed at which the LED will be toggled for each Enforcer hit. The default is 1 (which is like it always was) Setting it to 0 will make Enforcer not touch the LED. Using a larger value will make the flash take longer (such that it can be noticed when doing I/O models other than the default serial output) The time that the flash will take is a bit more than 1.3 microseconds times the number. So 1000 will be a bit more than 1.3 milliseconds. (Or 1000000 is a bit more than 1.3 seconds.) PARALLEL/S - This option will make Enforcer use the parallel port hardware rather than the serial port for output. RAWIO/S - This option will make Enforcer stuff the hit report (special IO) into an internal buffer and then from the main Enforcer process output the results via the RawPutChar() EXEC debugging LVO. Since the output happens on the Enforcer task it is possible for a hit that ends in a system crash to not be able to be reported. This option is here such that tools which can redirect debugging output can redirect the Enforcer output too. FILE/K - This option will make Enforcer output the hit report (special IO) but to a file insted of sending it to the hardware directly or using the RAWIO LVO. A good example of such a file is CON:0/0/640/100/HIT/AUTO/WAIT. Another thing that can be done is to have a program sit on a named pipe and have Enforcer output to it. This program can then do whatever it feels like with the Enforcer hits. (Such as decode them, etc.) *NOTE* It is not a good idea to have Enforcer hits go to a file on a disk as if the system crashes during/after the Enforcer hit, the disk may become corrupt. STDIO/S - This option will make Enforcer output the hit report (special IO) to STDOUT. This option only works from the CLI as it requires STDOUT. It is best used with redirection or pipes. BUFFERSIZE/K/N - This lets you set Enforcer's internal output buffer for the special I/O options. This option is only valid with the RAWIO, FILE, or STDIO options. The minimum setting is 8000. The default is 8000. Having the right amount of buffer is rather important for the special I/O modes. The reason is due to the fact that no operating system calls can be made from a bus error. Thus, in the special I/O mode, Enforcer must store the output in this buffer and, via some special magic, wake up the Enforcer task to read the buffer and write it out as needed. However, if a task is in Forbid() or Disable() when the Enforcer hit happens, the Enforcer task will not be able to output the results of the hit. This buffer lets a number of hits happen even if the Enforcer task was unable to do the I/O. If the number of hits that happen before the I/O was able to run gets too large, the last few hits will either be cut off completely or contain only partial information. INTRO/K - This optional introduction string will be output at the start of every Enforcer hit. For example: INTRO="*NBad Program!" The default is no string. PRIORITY/K/N - This lets you set Enforcer's I/O task priority. The default for this priority is 99. In some special cases, you may wish to adjust this. It is, however, recommended that if you are using one of the special I/O options (RAWIO, FILE, or STDIO) that you keep the priority rather high. If the priority you supply is outside of the valid task priority range (-127 to 127) Enforcer will use the default priority. NOALERTPATCH/S - This option disables the patching of the EXEC Alert() function. Normally Enforcer will patch this function to provide information as to what called Alert() and to prevent the Enforcer hits that a call to Alert() would cause. ON/S - Mainly for completeness. If not specified, it is assumed you want to turn ON Enforcer. QUIT=OFF/S - Tells Enforcer to turn off. Enforcer can also be stopped by sending a CTRL-C to its process. RESULTS When run, a set of MMU tables that map addresses that are not in the system's address map as invalid are installed. Enforcer will then trap invalid access attempts and generate a diagnostic message as to what the illegal access was. The first memory page (the one starting at location 0) is also marked as invalid as many programming errors cause invalid access to these addresses. Invalid addresses are completely off limits to applications. When an access violation happens, a report such as the following is output. 03-Apr-93 21:26:18 WORD-WRITE to 00000000 data=4444 PC: 07895CA4 USP: 078D692C SR: 0000 SW: 0729 (U0)(-)(-) TCB: 078A2690 Data: DDDD0000 DDDD1111 DDDD2222 DDDD3333 DDDD4444 DDDD5555 DDDD6666 DDDD7777 Addr: AAAA0000 AAAA1111 AAAA2222 AAAA3333 AAAA4444 AAAA5555 07800804 -------- Stck: 00000000 07848E1C 00009C40 078A30B4 BBBBBBBB BBBBBBBB BBBBBBBB BBBBBBBB Stck: BBBBBBBB BBBBBBBB BBBBBBBB BBBBBBBB BBBBBBBB 078E9048 00011DA8 DEADBEEF ----> 07895CA4 - "lawbreaker" Hunk 0000 Offset 0000007C PC-8: AAAA1111 247CAAAA 2222267C AAAA3333 287CAAAA 44442A7C AAAA5555 31C40000 PC *: 522E0127 201433FC 400000DF F09A522E 012611C7 00CE4EAE FF7642B8 0324532E Name: "New_Shell" CLI: "lawbreaker" Hunk 0000 Offset 0000007C LONG-READ from AAAA4444 PC: 07895CA8 USP: 078D692C SR: 0015 SW: 0749 (U0)(F)(-) TCB: 078A2690 Data: DDDD0000 DDDD1111 DDDD2222 DDDD3333 DDDD4444 DDDD5555 DDDD6666 DDDD7777 Addr: AAAA0000 AAAA1111 AAAA2222 AAAA3333 AAAA4444 AAAA5555 07800804 -------- Stck: 00000000 07848E1C 00009C40 078A30B4 BBBBBBBB BBBBBBBB BBBBBBBB BBBBBBBB Stck: BBBBBBBB BBBBBBBB BBBBBBBB BBBBBBBB BBBBBBBB 078E9048 00011DA8 DEADBEEF ----> 07895CA8 - "lawbreaker" Hunk 0000 Offset 00000080 PC-8: 247CAAAA 2222267C AAAA3333 287CAAAA 44442A7C AAAA5555 31C40000 522E0127 PC *: 201433FC 400000DF F09A522E 012611C7 00CE4EAE FF7642B8 0324532E 01266C08 Name: "New_Shell" CLI: "lawbreaker" Hunk 0000 Offset 00000080 25-Jul-93 17:15:06 Alert !! Alert 35000000 TCB: 07642F70 USP: 07657C10 Data: 00000000 DDDD1111 DDDD2222 DDDD3333 0763852A DDDD5555 DDDD6666 35000000 Addr: AAAA0000 AAAA1111 AAAA2222 AAAA3333 AAAA4444 0763852A 07400810 -------- Stck: 076385A0 00000000 0752EE9A 00002800 07643994 00000000 0762F710 076305F0 ----> 076385A0 - "lawbreaker" Hunk 0000 Offset 00000098 Here is a breakdown of what these reports are saying: In the first report, the first line is the date stamp. The first line of each report describes the access violation and where it happened from. In the case of a WRITE, the data that was being written will be displayed as well. If an instruction mode access caused the fault, there will be an (INST) in the line. The first line may also contain the BUS ERROR message. This will be displayed when an address that is valid in the system lists causes a physical bus fault during the access. This usually will happen with plug-in cards or when a hardware problem causes some form of system fault. Watch out, if this does show up, your system may be unstable and/or unreliable. The second line (starts USP:) displays the USER stack pointer (USP), the status register (SR:), the special status word (SW:). It then displays the supervisor/user state and the interrupt level. This will be from (U0) to (U7) or (S0) to (S7) (S=Supervisor) Next is the forbid state (F=forbid, -=not) and the disable state (D or -) of the task that was running when the access fault took place. Finally, the task control block address is displayed (TCB:) The next two lines contain the data and address register dumps from when the access fault happened. Note that A7 is not listed here. It is the stack pointer and is listed as USP: in the line above. Then come the lines of stack backtrace. These lines show the data on the stack. If the stack is in invalid memory, Enforcer will display a message to that fact. If SegTracker was installed before Enforcer, the "---->" lines will display in which seglist the given addresses are in based on the global tracking that SegTracker does. (See docs on SegTracker) If no seglist match is found, no lines will be displayed. One line will be displayed for each of the stack longwords asked for (see the STACKCHECK option) and one line for the PC address of the Enforcer hit. (The PC line is always checked for is SegTracker is installed.) The lines are in order: hit, first stack find, second stack find, etc. This is useful for tracking down who called the routine that caused the Enforcer hit. Next, optionally, comes the data around the program counter when the access fault happened. The first line (PC-8:) is the 8 long-words before the program counter. The second line starts at the program counter and goes for 8 long words. The last line displays the name of the task that was running when the access fault took place. If the task was a CLI, it will display the name of the CLI command that was running. If the access fault was found to have happened within the seglist of a loaded program, the segment number and the offset from the start of the segment will be displayed. (Note that this works for any LoadSeg()'ed process) Note that the name will display as "Processor Interrupt Level x" if the access happened in an interrupt. The other output that could happen is when a program or the OS calls the EXEC Alert function. Enforcer catches these calls and will display the alert information as seen above. (With the date and time if needed) WARNING Enforcer is for software testing. In this role it is vital. Software that causes Enforcer hits may not be able to run on newer hardware. (Enforcer hits of high addresses on systems not running Enforcer but with a 68040 will most likely crash the system) Future systems and hardware will make this even more important. The system can NOT survive software that causes Enforcer hits. However, Enforcer is NOT a system protector. As a side effect, it may well keep a system from crashing when Enforcer hits happen, but it may just as well make the software crash earlier. Enforcer is mainly a development and testing tool. Enforcer causes no ill effects with correctly working software. If a program fails to work while Enforcer is active, you should contact the developer of that program. NOTES This is Enforcer V37. Bryce Nesbitt came up with the original "Enforcer" that has been instrumental to the improvement in the quality of software on the Amiga. The Amiga users and developers owe him a great deal for this. Thank you Bryce! Enforcer V37, however, is a greatly enhanced and more advanced tool. Enforcer V37 came about due to a number of needs. These included the need for more output options and better performance. It also marks the removal of all kludges that were in the older versions. Also, some future plans required some of these changes... In addition, the complete redesign was needed in order to support the 68040. The internal design of Enforcer is now set up such that CPU/MMU specific code can be cleanly accessed from the general house keeping aspect of the code. The MMU bus error handling is, however, 100% CPU specific. Since AbsExecBase is in low memory, reads of this address are slower with Enforcer running. Caching AbsExecBase locally is highly recommended since it is in CHIP memory and on systems with FAST memory, it will be faster to access the local cached value. (In addition to the performance increase when running Enforcer) Note that doing many reads of location 4 will hurt interrupt performance. When the Amiga produces an ALERT, EXEC places some magic numbers into some special locations in low memory. The exact pattern changes between versions of the operating system. Enforcer will patch the EXEC function ColdReboot() in an attempt to "get out of the way" when someone tries to reboot the system. Enforcer will clean up as much as possible the MMU tables and then call the original LVO. When Enforcer is asked to quit, it will check to make sure it can remove itself from this LVO. If it can not, it will not quit at that time. If run from the shell, it will display a message saying that it tried but could not exit. Enforcer will continue to be active and you can try later to deactivate it. Enforcer will also patch the EXEC function Alert() in an attempt to provide better tracking of other events in the system. It is also patched such that dead-end alerts will correctly reset the system and be displayed. With this patch in place, the normal alerts will not be seen but will be replaced by the Enforcer output shown above. See LawBreaker for a more complete example of this. 68020 NOTES The 68020 does not have a built-in MMU but has a co-processor feature that lets an external MMU be connected. Enforcer MMU code is designed for use with 68851 MMU. This is the some-what 68030 compatible MMU by Motorola. Enforcer uses the same code for both the 68030 and the 68020/68851. For this reason, 68020/68851 users should see the 68030 NOTES section. 68030 NOTES The 68030 uses cycle/instruction continuation and will supply the data on reads and ignore writes during an access fault rather than let the real bus cycle happen. This means that on a fault caused by MMU tables, no bus cycle to the fault address will be generated. (For those of you with analyzers) In some cases, the 68030 will have advanced the Program Counter past the instruction by the time the access fault happens. This is usually only on WRITE faults. For this reason, the PC may either point at the instruction that caused the fault or just after the instruction that caused the fault. (Which could mean that it is pointing to the middle of the instruction that caused the fault.) Note that there is a processor called 68EC030. This processor has a disabled or defective MMU. However, it may function well enough for Enforcer to think it has a fully functional MMU and thus Enforcer will attempt to run. However, even if it looks like the MMU is functioning, it is not fully operational and thus may cause strange system activity and even crashes. Do not assume that Enforcer is safe to use on 68EC030 systems. 68040 NOTES Enforcer, on the 68040, *requires* that the 68040.library be installed and it requires an MMU 68040 CPU. The 68EC040 does not have a MMU. The 68LC040 does have an MMU and is supported. Enforcer will work best in a system with the 68040.library 37.10 or better but it does know how to deal with systems that do not have that version. Due to the design of the 68040, Enforcer is required to do a number of things differently. For example, the MMU page size can only be either 8K or 4K. This means that to protect the low 1K of memory, Enforcer will end up having to mark the first 4K of memory as invalid and emulate the access to the 3K of that memory that is valid. For this reason Enforcer moves a number of possible structures from the first 4K of memory to higher addresses. This means that the system will continue to run at a reasonable speed. The first time Enforcer is run it may need to allocate memory for these structures that it will move. Enforcer can never return this memory to the system. In addition to the fact that the 68040 MMU table size is different, the address fault handling is also different. Namely, the 68040 can only rerun the cycle and not continue it like the 68030. This means that on a 68040, the page must be made available first and then made unavailable. To make this work, Enforcer will switch the instruction that caused the error into trace mode and let it run with a special MMU setup. When the trace exception comes in, the MMU is set back to the way it was. Enforcer does its best to keep debuggers working. Note, however, that the interrupt level during a trace of a READ will end up being set to 7. This is to prevent interrupts from changing the order of trace/MMU table execution. The level will be restored to the original state before continuing. Since T0 mode tracing is also supported, there are also some changes in the way it operates. T0 mode tracing is defined, on the 68040, to cause a trace whenever the instruction pipeline needed to be reloaded. While on the 68020/030 processors this was normally only for the branch instructions, in the 68040 this includes a large number of other instructions. (Including NOP!) Anyway, if an Enforcer hit happens while in T0 tracing mode, the trace will happen even on instructions that normally would not cause a T0 mode trace. Since this may actually help in debugging and because it was not possible to do anything else, this method of operation is deemed acceptable. Another issue with the 68040 is that WRITE faults happen *after* the instruction has executed. (Except for MOVEM) In fact, it is common for the 68040 to execute one or more extra instructions before the WRITE fault is executed. This design makes the 68040 much faster, but it also makes the Program Counter value that Enforcer can report for the fault much less likely to be pointing to the instruction that caused it. The worst cases are sequences such as a write fault followed by a branch instruction. In these cases, the branch is usually already executed before the write fault happens and thus the PC will be pointing to the target of the branch. There is nothing that can be done within Enforcer to help out here. You will just need to be aware of this and deal with it as best as possible. Along with the above issue, is the fact that since a write fault may be delayed, a read fault may happen before the write fault shows up. Internally, enforcer does not do special processing for these and they will not show up. Since another hit was happening anyway, it is felt that it is best to just not report the hit. Along the same lines, the hit generated from a MOVEM instruction may only show as a single hit rather than 1 for each register moved. On the Amiga, MOVE16 is not supported 100%. Causing an Enforcer hit with a MOVE16 will cause major problems and maybe cause Enforcer or your task to lock. Since MOVE16 is not supported, this is not a major issue. Just watch out if you are using this 68040 instruction. (Also, watch out for the 68040 CPU bug with MOVE16) The functions CachePreDMA(), CachePostDMA(), and CacheControl() are patched when the 68040 MMU is turned on by Enforcer. These functions are patched such the issues with DMA and the 68040 COPYBACK data caches are addressed. The 68040.library normally deals with this, however since Enforcer turns on the MMU, the method of dealing with it in the 68040.library will not work. For this reason, Enforcer will patch these and implement the required fix for when the MMU is on. When Enforcer is asked to exit, it will check if it can remove itself from these functions. If it can not, it will ignore the request to exit. If Enforcer was run from the CLI, it will print a message saying that it can not exit when the attempt is made. 68060 NOTES Enforcer, on the 68060, *requires* that the 68060.library be installed. Due to the fact that various possible 68060.library versions may exist, Enforcer tries to not second guess it. Thus, Enforcer assumes that the 68060.library has all of the same functionality as V37.30 or better of the 68040.library. It turns out that some of the 68060 libraries do not have the same functionality of the 68040.library. One common library has elected not to handle Pre/Post DMA MMU table operations when Enforcer installs its MMU table. This results in some DMA/Cache interactions. Enforcer can not work around this problem safely. If you happen to have a 68060.library with version 2.1 (19.07.96) you may be able to patch it to not have this problem. At offset $09BE there should be the 4-byte sequence $20 6D 00 04 Changing this to $4E 7A 88 06 will let it handle Enforcer's MMU tables too. (The same patch may work in other versions of the library) For implementers of 68060.library, see my notes as to what had to be done in 68040.library for correct operation. Note that this does not mean that Enforcer needs this. The Amiga system needs this to operate correctly. Enforcer just may cause these problems to become more evident. The notes are only in the AmigaGuide version of the Enforcer documentation. The 68060 exception model is full-restart, which means that all instructions are re-run. Both reads *and* writes. This means that Enforcer can not tell you what the data that would be written is, unlike the 68040 and earlier CPUs. So, the output for a write will not include the data that was to be written. This does mean that faults happen before the instruction is executed (usually) and thus the reported PC will be more exact. This restart model also means that if an real bus-fault happens, Enforcer will be unable to do much other than let it happen. (The same is true for reads on the 68040) Enforcer maps all addresses as either valid based on system configuration or invalid. This is so that no address should cause a bus fault unless the system configuration is incorrect and an address that was marked valid actually causes a fault. Be sure to read the 68040 notes as the 68060 is a superset of much of these notes. Due to the complexity of emulating access to lower memory and the fact that the 68060 was introduced well after V39 kickstart, it is highly recommended that you use V39 or better with 68060 CPUs. This mainly has to deal with lower 4K of memory. As of V38 of exec.library, 68040/68060 processors would map out the lower 4K of RAM rather than just 1K. This was required since the newer CPUs did not have page sizes less than 4K. It turns out that some 68060 CPU cards also have other hardware on them. This is not a problem, unless this hardware does not autoconfigure. Enforcer needs to know about hardware in the system so it can map the MMU to that hardware. If the hardware is not true Amiga AutoConfig (as in no expansion.library entry) then Enforcer has no way of knowing it exists. A common location for such hardware control registers to be placed is in the reserved $00F00000 address range (known as F-Space). This 512K space was reserved for future Kickstart growth. It also has some magic in it so that you can wedge special startup routines there for things like 68060 cards. At least one vendor is doing all of this correctly and has even made sure that expansion.library knows about the hardware that is located in the $00F00000 address range. If, when running Enforcer, your machine does lock up *and* when you run Enforcer with the VERBOSE option it does not say that the $00F00000 address range is a "board address" then you may wish to try the FSPACE option to see if this is the reason. If this does not fix the problem, you more than likely have either a real bug or some other non-AutoConfig hardware in your system. WRITING DEBUGGERS If you wish to make a debugger that works with Enforcer to help pinpoint Enforcer hits in the application and not cause Enforcer hits itself, here are some simple tips and a bit of code. DEBUGGERS: TRAPPING A HIT To trap a hit requires a number of things to work. First, the debugger itself must never cause an Enforcer hit. For help on that, see the "DEBUGGERS: NOT CAUSING A HIT" Second, the debugger must be global. That is, you must be able to deal with a task getting a hit that is not the task under test. There are a number of simple ways to deal with this, and I will leave this up to the debugger writer. (One method will be shown below) Third, the debugger must start *AFTER* Enforcer starts. If it is started before Enforcer, the hits will not be trapped. (Note that this is not a problem) A very important point: The code needs to be fast for the special case of location 4. This is shown in the code below. It is very important that this be fast. Note that it is much prefered that debuggers use the method described below for trapping hits. It should be much more supportable this way as any of the tricky work that may need to be done in the hit processing will be handled by Enforcer itself. If you wish the hit decoded, you can capture the Enforcer output via a pipe or some other method (such as RAWIO) or you can leave that issue up to the user. Now, given the above, the following bits of code can be used to get the debugger to switch into single-step mode at the point of the Enforcer hit. You can also set some data value here to tell your debugger about this. ; ; The following code is inserted into the bus error vector. ; Make sure you follow the VBR to find the vector. ; Store the old vector in the address OldVector ; Make sure you already have the single-step trap vector ; installed before you install this. Note that any extra ; code you add in the comment area *MUST NOT* cause a bus ; fault of any kind, including reading of location 4. ; ; This is the common part... ; EnforcerHit: ds.l 1 ; Some private flag MyTask: ds.l 1 ; Task under test MyExecBase: ds.l 1 ; The local copy OldVector: ds.l 1 ; One long word ; ; Now, if you wish to only trap a specific task, ; do the check at this point. For example, a ; simple single-task debugger would do something ; like this: Common: move.l a0,-(sp) ; Save this... move.l MyExecBase(pc),a0 ; Get ExecBase... move.l ThisTask(a0),a0 ; Get ThisTask cmp.l MyTask(pc),a0 ; Are they the same? move.l (sp)+,a0 ; Restore A0 (no flags) bne.s TraceSkip ; If not my task, skip ; bset.b #7,(sp) ; Set trace bit... ; If you have any other data to set, do it now... ; Set as setting the EnforcerHit bit in your data... addq.l #1,EnforcerHit : Count the hit... ; TraceSkip: move.l OldVector(pc),-(sp) ; Ready to return rts ; ; This is the 68020/68030 version... ; NewVector030: cmp.l #4,$10(sp) ; 68020 and 68030 beq.s TraceSkip ; If AbsExecBase, OK bra.s Common ; Do the common stuff ; ; This is the 68040 version... ; NewVector040: cmp.l #4,$14(sp) ; 68040 beq.s TraceSkip ; If AbsExecBase, OK bra.s Common ; Do the common stuff ; ; This is the 68060 version... ; NewVector060: cmp.l #4,$08(sp) ; 68060 beq.s TraceSkip ; If AbsExecBase, OK bra.s Common ; Do the common stuff DEBUGGERS: NOT CAUSING A HIT In order not to cause Enforcer hits, you can do a number of things. The easiest is to test the address with the TypeOfMem() EXEC function. If TypeOfMem() returns 0, the address is not in the memory lists. However, this does not mean it is not a valid address in all cases. (ROM, chip registers, I/O boards) For those cases, you can build a "valid memory access table" much like Enforcer does. Here is the code from Enforcer for the base memory tables: /* * Mark_Address(mmu,start address,length,type) */ /* * Special case the first page of CHIP RAM */ mmu=Mark_Address(mmu,0,0x1000,INVALID | NONCACHEABLE); /* * Map in the free memory */ Forbid(); mem=(struct MemHeader *)SysBase->MemList.lh_Head; while (mem->mh_Node.ln_Succ) { mmu=Mark_Address(mmu, (ULONG)(mem->mh_Lower), (ULONG)(mem->mh_Upper)-(ULONG)(mem->mh_Lower), ((MEMF_CHIP & TypeOfMem(mem->mh_Lower)) ? (NONCACHEABLE | VALID) : (CACHEABLE | VALID))); mem=(struct MemHeader *)(mem->mh_Node.ln_Succ); } Permit(); /* * Map in the autoconfig boards */ if (ExpansionBase=OpenLibrary("expansion.library",0)) { struct ConfigDev *cd=NULL; while (cd=FindConfigDev(cd,-1L,-1L)) { /* Skip memory boards... */ if (!(cd->cd_Rom.er_Type & ERTF_MEMLIST)) { mmu=Mark_Address(mmu, (ULONG)(cd->cd_BoardAddr), cd->cd_BoardSize, VALID | NONCACHEABLE); } } CloseLibrary(ExpansionBase); } /* * Now for the control areas... */ mmu=Mark_Address(mmu,0x00BC0000,0x00040000,VALID | NONCACHEABLE); mmu=Mark_Address(mmu,0x00D80000,0x00080000,VALID | NONCACHEABLE); /* * and the ROM... */ mmu=Mark_Address(mmu, 0x00F80000, 0x00080000, VALID | CACHEABLE | WRITEPROTECT); /* * If the credit card resource, make the addresses valid... */ if (OpenResource("card.resource")) { mmu=Mark_Address(mmu,0x00600000,0x00440002,VALID | NONCACHEABLE); } /* * If CD-based Amiga (CDTV, A570, etc.) */ if (FindResident("cdstrap")) { mmu=Mark_Address(mmu,0x00E00000,0x00080000,VALID | NONCACHEABLE); mmu=Mark_Address(mmu,0x00B80000,0x00040000,VALID | NONCACHEABLE); } /* * Check for ReKick/ZKick/KickIt */ if ((((ULONG)(SysBase->LibNode.lib_Node.ln_Name)) >> 16) == 0x20) { mmu=Mark_Address(mmu, 0x00200000, 0x00080000, VALID | CACHEABLE | WRITEPROTECT); } SEE ALSO "A master's secrets are only as good as the master's ability to explain them to others." - Michael Sinz BUGS None?

FindHit FindHit NAME FindHit - A tool that can locate the source file and line number that a SegTracker report happened at. SYNOPSIS FindHit will read the executable file and if there is debugging information in it, will try to locate the source file and line number that correspond to the Enforcer hit HUNK/OFFSET. FUNCTION FindHit uses the Lattice/SAS/MetaScope standard 'LINE' debug hunk to locate the closest line to the hunk/offset given. Note that this can only happen if the executable has the LINE debugging turned on. (The LawBreaker program has this such that you can test this yourself.) In SAS/C 6.x, you need to compile with DEBUG=LINE or better and do not use the link option of NODEBUG. In SAS/C 5.x, you need to compile with -d1 or better. Note that FindHit works with the old SAS/C 5.x 'SRC ' debugging information too. This is required for -d2 or higher debugging support. However, I do not have 'SRC ' hunk documentation and thus FindHit may be very specific to the SAS/C 5.x version of this hunk. In DICE (2.07 registered being the one I tried) the -d1 debug switch also supports the 'LINE' debug hunk and works with FindHit. In HX68 and CAPE, you need to add the DEBUG directive to the assembly code program. (See LawBreaker source) For other languages, or other versions of the above, please see the documentation that comes with the language. INPUTS FILE/A - The executable file, with debugging information. OFFSETS/A/M - The HEX offset (with or without leading $) If a hunk number other than the default is needed, it is expressed as hunk:offset. The default hunk is that of the last argument or hunk 0 if no hunk number has been given. For example: 12 $22 $3:12 22 4:$12 32 $0:$32 will find information for: hunk $0, offset $12 hunk $0, offset $22 hunk $3, offset $12 hunk $3, offset $22 hunk $4, offset $12 hunk $4, offset $32 hunk $0, offset $32 EXAMPLE FindHit FooBar $0342 $1:4F2 3:$1A 2C badcode.c : Line 184 No line number information for Hunk $1, Offset $4F2 badcode2.c : Line 12 badcode2.c : Line 14 See the Enforcer documentation about issues dealing with the exact location of the Enforcer hit. The line given may not be exactly where the hit happened. The way I use this is to always have line debugging turned on when I compile. This does not change the quality of the code and takes only a small amount of extra disk space. However, what I do is to link the program twice: Once to a file called program.ld which contains all of the debugging information. Then, I link program.ld to program, stripping debug information. The command line for SLINK or BLINK is as follows: BLINK program.ld TO program NODEBUG I keep both of these on hand; with program being the one I distribute and use. When a hit happens, I can just use program.ld with FindHit to get the line number and source file that it happened in. This way you can distribute your software without the debugging information and still be able to use FindHit on the actual code. (After all, that link command does nothing but strip symbol and debug hunks) NOTES Note that this program does nothing when run from the Workbench and thus does not have an icon. SEE ALSO "Programming is like sex - one mistake and you have to support it for life." - Michael Sinz BUGS

LawBreaker LawBreaker NAME LawBreaker - A quicky test of Enforcer SYNOPSIS This is a quick test of Enforcer and its reporting abilities. FUNCTION This program is used to make sure that Enforcer is correctly installed and operating. LawBreaker works from either the CLI or Workbench. It will try to read and write certain memory areas that will cause an Enforcer hit or four. LawBreaker will also do an Alert to show how Enforcer reports an Alert. Note that the LawBreaker executable has debugging information in it (standard LINE format debug hunk) such that you can try the FindHit program to find the line that causes the hit. INPUTS Just run it... RESULTS When running Enforcer, you will see some output from Enforcer. Output on a 68030 machine would look something like this: 25-Jul-93 17:15:04 WORD-WRITE to 00000000 data=0000 PC: 0763857C USP: 07657C14 SR: 0004 SW: 04C1 (U0)(-)(-) TCB: 07642F70 Data: DDDD0000 DDDD1111 DDDD2222 DDDD3333 0763852A DDDD5555 DDDD6666 DDDD7777 Addr: AAAA0000 AAAA1111 AAAA2222 AAAA3333 AAAA4444 0763852A 07400810 -------- Stck: 00000000 0752EE9A 00002800 07643994 00000000 076786D8 000208B0 2EAC80EE Stck: 487AFD12 486C82C4 4EBA3D50 4EBAEA28 4FEF0014 52ACE2E4 204D43EC 88BC203C ----> 0763857C - "lawbreaker" Hunk 0000 Offset 00000074 Name: "Shell" CLI: "LawBreaker" Hunk 0000 Offset 00000074 25-Jul-93 17:15:04 LONG-READ from AAAA4444 PC: 07638580 USP: 07657C14 SR: 0015 SW: 0501 (U0)(F)(-) TCB: 07642F70 Data: DDDD0000 DDDD1111 DDDD2222 DDDD3333 0763852A DDDD5555 DDDD6666 DDDD7777 Addr: AAAA0000 AAAA1111 AAAA2222 AAAA3333 AAAA4444 0763852A 07400810 -------- Stck: 00000000 0752EE9A 00002800 07643994 00000000 076786D8 000208B0 2EAC80EE Stck: 487AFD12 486C82C4 4EBA3D50 4EBAEA28 4FEF0014 52ACE2E4 204D43EC 88BC203C ----> 07638580 - "lawbreaker" Hunk 0000 Offset 00000078 Name: "Shell" CLI: "LawBreaker" Hunk 0000 Offset 00000078 25-Jul-93 17:15:04 BYTE-WRITE to 00000101 data=11 PC: 0763858A USP: 07657C14 SR: 0010 SW: 04A1 (U0)(F)(D) TCB: 07642F70 Data: 00000000 DDDD1111 DDDD2222 DDDD3333 0763852A DDDD5555 DDDD6666 DDDD7777 Addr: AAAA0000 AAAA1111 AAAA2222 AAAA3333 AAAA4444 0763852A 07400810 -------- Stck: 00000000 0752EE9A 00002800 07643994 00000000 076786D8 000208B0 2EAC80EE Stck: 487AFD12 486C82C4 4EBA3D50 4EBAEA28 4FEF0014 52ACE2E4 204D43EC 88BC203C ----> 0763858A - "lawbreaker" Hunk 0000 Offset 00000082 Name: "Shell" CLI: "LawBreaker" Hunk 0000 Offset 00000082 25-Jul-93 17:15:04 LONG-WRITE to 00000102 data=00000000 PC: 07638592 USP: 07657C14 SR: 0014 SW: 0481 (U0)(-)(D) TCB: 07642F70 Data: 00000000 DDDD1111 DDDD2222 DDDD3333 0763852A DDDD5555 DDDD6666 DDDD7777 Addr: AAAA0000 AAAA1111 AAAA2222 AAAA3333 AAAA4444 0763852A 07400810 -------- Stck: 00000000 0752EE9A 00002800 07643994 00000000 076786D8 000208B0 2EAC80EE Stck: 487AFD12 486C82C4 4EBA3D50 4EBAEA28 4FEF0014 52ACE2E4 204D43EC 88BC203C ----> 07638592 - "lawbreaker" Hunk 0000 Offset 0000008A Name: "Shell" CLI: "LawBreaker" Hunk 0000 Offset 0000008A 25-Jul-93 17:15:06 Alert !! Alert 35000000 TCB: 07642F70 USP: 07657C10 Data: 00000000 DDDD1111 DDDD2222 DDDD3333 0763852A DDDD5555 DDDD6666 35000000 Addr: AAAA0000 AAAA1111 AAAA2222 AAAA3333 AAAA4444 0763852A 07400810 -------- Stck: 076385A0 00000000 0752EE9A 00002800 07643994 00000000 0762F710 076305F0 ----> 076385A0 - "lawbreaker" Hunk 0000 Offset 00000098 Now, using FindHit, you would type: FindHit LawBreaker 0:82 and it will tell you the source file name and the line number where the hit happened. See the FindHit documentation. NOTES If enforcer is not running, the program should not cause the system to crash. It will, however, write to certain areas of low memory. Also, it will cause read access of some addresses that may not exist. This may cause bus faults. SEE ALSO "Programming is like sex - one mistake and you have to support it for life." - Michael Sinz BUGS There are 4 known Enforcer hits in this code and 1 alert, however, they will not be fixed. ;^)

MMU MMU NAME MMU - A 68040/68060 MMU Mapping Tool SYNOPSIS MMU is a tool for 68040/060 systems to display and/or change the MMU configuration contained within. MMU will display the tables as compressed as possible, noting address ranges that are mapped the same and only displaying one line for the whole range. INPUTS ADDRESS/K - Hex address to display (if not given, all) SIZE/K - Hex address size of address range READWRITE - Change address range to ReadWrite READONLY - Change address range to ReadOnly VALID - Change address range to VALID INVALID - Change address range to INVALID CACHE - Change address range to CACHE enable NOCACHE - Change address range to CACHE disable COPYBACK - Change address range to non-serialized NOCOPYBACK - Change address range to serialized GLOBAL - Change address range to GLOBAL LOCAL - Change address range to LOCAL SUPERVISOR - Change address range to SUPERVISOR-only USER - Change address range to USER access VERBOSE - Show the changes as they are made NOSHOW - Do not show the whole table RESULTS On my A3000 with a 68040 installed and Enforcer running, the MMU output looks as follows: (No options) Current 68040 MMU table setup: $00000000-$00000FFF->$00000000: Local, User, Invalid, Read-Only, Serialized $00001000-$001FFFFF->$00001000: Global, User, Valid, Read/Write, Serialized $00200000-$00BBFFFF->$07542000: Local, User, Invalid, Read-Only, Serialized $00BC0000-$00BFFFFF->$00BC0000: Global, User, Valid, Read/Write, Serialized $00C00000-$00D7FFFF->$07542000: Local, User, Invalid, Read-Only, Serialized $00D80000-$00DFFFFF->$00D80000: Global, User, Valid, Read/Write, Serialized $00E00000-$00E8FFFF->$07542000: Local, User, Invalid, Read-Only, Serialized $00E90000-$00EAFFFF->$00E90000: Global, User, Valid, Read/Write, Serialized $00EB0000-$00EFFFFF->$07542000: Local, User, Invalid, Read-Only, Serialized $00F00000-$00FFFFFF->$00F00000: Global, User, Valid, Read-Only, Copyback $01000000-$073FFFFF->$07542000: Local, User, Invalid, Read-Only, Serialized $07400000-$07F7FFFF->$07400000: Global, User, Valid, Read/Write, Copyback $07F80000-$FFFFFFFF->$07542000: Local, User, Invalid, Read-Only, Serialized WARNING This tool is a hack and does not have any safeguards on the options. Use at your own risk. SEE ALSO "A master's secrets are only as good as the master's ability to explain them to others." - Michael Sinz BUGS None?

Move4K Move4K NAME Move4K - Moves as much out of the lower 4K of RAM as possible SYNOPSIS On 68040 systems, as much of the lower 4K of CHIP RAM as possible is removed from system use. FUNCTION On 68040 systems the MMU page sizes are 4K and 8K. Enforcer uses the 4K page size. Since watching for hits of low memory is a vital part of Enforcer, this means that the first 4K of RAM will be marked invalid. On current systems, only the first 1K of RAM is invalid and thus 3K of RAM in that first 4K will end up needing to be emulated in Enforcer. In order to reduce the overhead that this causes (and the major performance loss) this program will try to move as much from that first 4K as possible and make any of the free memory within the first 4K inaccessible. Enforcer itself also has this logic, but it may be useful to be able to run this program as the first program in the Startup-Sequence (*AFTER* SetPatch) to try to limit the number of things that may use the lower 4K of RAM. INPUTS Just run it... Can be run from CLI or Workbench RESULTS Any available memory in the lower 4K of CHIP RAM is removed plus a special graphics buffer is moved if it needs to be. After running this program you may have a bit less CHIP RAM than before. You can run this program as many times as you wish since it only moves things if it needs to. NOTES This program will do nothing on systems without a 68040. It does not, however, check for the MMU and thus it will move the lower 4K even if the CPU is not able to run Enforcer. V39 of the operating system already does have the lowest MMU page empty and thus this program will effectively do nothing under V39. SEE ALSO "Eloquence is vehement simplicity" BUGS None.

RebootOff RebootOff NAME RebootOff - A keyboard reset handler to turn off Enforcer SYNOPSIS This is a simple utility that will turn off Enforcer when a keyboard reset happens. FUNCTION This utility uses the feature of the A1000/A2000/A3000/A4000 Amiga systems to turn off Enforcer when the user does a keyboard reset (ctrl-Amiga-Amiga). This utility requires that your Amiga supports (in hardware) the keyboard reset system. The reason this was written was such that Enforcer could be "quit" just before you reboot your Amiga 3000. This way it will let the kickstart not need to be reloaded and thus let utilities such as RAD: work across reboots. Note that this does *not* help in the case where the Amiga reboots under software conditions. It is only for keyboard resets. INPUTS Just run it from either the CLI or Workbench. It installs a handler and exits. On a keyboard reset, it will turn Enforcer off before it lets the reset continue... (max time of 10 seconds) RESULTS Installs a small reset handler object and task into the system. About 3700 bytes needed the first time it is run. NOTES If Enforcer is not running, nothing will happen at Reset time. If Enforcer can not quit, the reset system will continue to try to quit Enforcer until the hardware timeout happens... SEE ALSO From the home of the imaginary deadlines: "It will take 2i weeks to do that project." - Michael Sinz

SegTracker SegTracker NAME SegTracker - A global SegList tracking utility SYNOPSIS A global tracking utility for disk loaded files including libraries and devices. If placed in the startup-sequence right after SetPatch, it will track all disk loaded segments (other than those loaded by SetPatch) FUNCTION SegTracker will patch the DOS LoadSeg(), NewLoadSeg(), and UnLoadSeg() functions in order to track the SegLists that are loaded. SegTracker keeps these seglist stored in a "safe" manner and even handles programs which SegList split. The first time the program is run, it installs the patches and semaphore. After that point, it just finds the semaphore and uses it. When SegTracker is installed, it will scan the ROM for ROM modules and add their locations to the tracking list such that addresses within those modules can be identified. Note that the offsets from the module is based on the location of the module's ROMTAG. The NOROM option will prevent this feature from being installed. By using SegTracker, it will be possible to better identify where Enforcer hits come from when dealing with libraries and devices. Basically, it is a system-global Hunk-o-matic. External programs can then pass in an address to SegTracker either via the command line or via the given function pointer in the SegTracker semaphore and get back results as to what hunk and offset the address is at. To work with the function directly, you need to find the the semaphore of "SegTracker" using FindSemaphore(). The structure found will be the following: struct SegSem { struct SignalSemaphore seg_Semaphore; SegTrack *seg_Find; }; The function pointer points to a routine that takes an address and two pointers to long words for returning the Segment number and Offset within the segment. The function returns the name of the file loaded. Note that you must call this function while in Forbid() and then copy the name as the seglist may be UnLoadSeg'ed at any moment and the name string will then no longer be in memory. typedef char (* __asm SegTrack(register __a0 ULONG Address, register __a1 ULONG *SegNum, register __a2 ULONG *Offset)); The above is for use in C code function pointer prototype in SAS/C 5 and 6. INPUTS SHOW/S - Shows all of the segments being tracked. DUMP/S - Displays all of the segment elements being tracked. NOROM/S - Tells segtracker not to scan ROM when it is installed, thus not adding ROM addresses to the tracking list. FIND/M - Find the hex (in $xxxxx format) address in the tracked segments. Multiple addresses can be given. Options are not available from Workbench as they require the CLI. However, you can run SegTracker from Workbench to install it. EXAMPLE USAGE /* * A simple program that will "find" given addresses in the SegLists * This program has been compiled with SAS/C 6.2 without errors or * warnings. * * Compiler options: * DATA=FARONLY PARAMETERS=REGISTER NOSTACKCHECK * NOMULTIPLEINCLUDES STRINGMERGE STRUCTUREEQUIVALENCE * MULTIPLECHARACTERCONSTANTS DEBUG=LINE NOVERSION * OPTIMIZE OPTIMIZERINLOCAL NOICONS * * Linker options: * FindSeg.o TO FindSeg SMALLCODE SMALLDATA NODEBUG LIB LIB:sc.lib */ #include <exec/types.h> #include <exec/execbase.h> #include <exec/libraries.h> #include <exec/semaphores.h> #include <dos/dos.h> #include <dos/dosextens.h> #include <dos/rdargs.h> #include <clib/exec_protos.h> #include <pragmas/exec_sysbase_pragmas.h> #include <clib/dos_protos.h> #include <pragmas/dos_pragmas.h> #include <string.h> #include "FindSeg_rev.h" #define EXECBASE (*(struct ExecBase **)4) typedef char (* __asm SegTrack(register __a0 ULONG, register __a1 ULONG *, register __a2 ULONG *)); struct SegSem { struct SignalSemaphore seg_Semaphore; SegTrack *seg_Find; }; #define SEG_SEM "SegTracker" #define TEMPLATE "FIND/M" VERSTAG #define OPT_FIND 0 #define OPT_COUNT 1 ULONG cmd(void) { struct ExecBase *SysBase; struct Library *DOSBase; struct RDArgs *rdargs; ULONG rc=RETURN_FAIL; struct SegSem *segSem; char **hex; LONG opts[OPT_COUNT]; SysBase = EXECBASE; if (DOSBase = OpenLibrary("dos.library",37)) { memset((char *)opts, 0, sizeof(opts)); if (!(rdargs = ReadArgs(TEMPLATE, opts, NULL))) { PrintFault(IoErr(),NULL); } else if (CheckSignal(SIGBREAKF_CTRL_C)) { PrintFault(ERROR_BREAK,NULL); } else if (segSem=(struct SegSem *)FindSemaphore(SEG_SEM)) { rc=RETURN_OK; if (opts[OPT_FIND]) { for (hex=(char **)opts[OPT_FIND];(*hex);hex++) { char *p; ULONG val; ULONG tmp[4]; ULONG c; val=0; p=*hex; if (*p=='$') p++; /* Support $hex */ while (*p) { c=(ULONG)*p; if ((c>='a') && (c<='f')) c-=32; c-='0'; if (c>9) { c-=7; if (c<10) c=16; } if (c<16) { val=(val << 4) + c; p++; } else { val=0; p=&p[strlen(p)]; } } /* * Ok, we need to do this within Forbid() * as segments can unload at ANY time, including * during AllocMem(), so we use a stack buffer... * */ Forbid(); if (p=(*segSem->seg_Find)(tmp[0]=val,&tmp[2],&tmp[3])) { char Buffer[200]; stccpy(Buffer,p,200); tmp[1]=(ULONG)Buffer; VPrintf("$%08lx - %s : Hunk %ld, Offset $%08lx",tmp); /* * Now get the SegList address by passing the * same pointer for both hunk & offset. Note * that this is only in the newer SegTrackers * To test if this worked, check if the result * of this call is either a hunk or an offset. */ (*segSem->seg_Find)(val,&tmp[0],&tmp[0]); /* * This "kludge" is for compatibility reasons * Check if result is the same as either the hunk * or the offset. If so, do not print it... */ if ((tmp[0]!=tmp[2]) && (tmp[0]!=tmp[3])) { VPrintf(", SegList $%08lx",tmp); } PutStr("\n"); } else VPrintf("$%08lx - Not found\n",tmp); Permit(); } } } else PutStr("Could not find SegTracker semaphore.\n"); if (rdargs) FreeArgs(rdargs); CloseLibrary(DOSBase); } else if (DOSBase=OpenLibrary("dos.library",0)) { Write(Output(),"Requires Kickstart 2.04 (37.175) or later.\n",43); CloseLibrary(DOSBase); } return(rc); } NOTES The earlier this command is run, the better off it will be in tracking disk loaded segments. Under debug usage, you may wish to run the command right *AFTER* SetPatch. Some things may not call UnLoadSeg() to free their seglists. There is no way SegTracker can follow a seglist that is not unloaded via the dos.library call to UnLoadSeg(). For this reason, SegTracker adds new LoadSeg() segments to the top of its list. This way, if any old segments are still on the list but have been unloaded via some other method they will not clash with newer segments during the find operation. Note that the resident list is one such place where UnLoadSeg() is not called to free the seglist. Thus, if something is made resident and then later unloaded it will still be listed as tracked by SegTracker. In order to support a new feature in CPR, the SegTracker function got a "kludge" added to it. If a segment is found, you can then call the function again with the same address but with having both pointers point to the same longword of storage. By doing this, the function will now return (in that longword) the SegList pointer (CPTR not BPTR) of the file that contains the address. The reason this method was used was so it would be compatible with older SegTracker versions. In older versions you would not get the result you wanted but you would also not crash. See the example above for more details on how to use this feature. The SegTracker FIND option has been expanded to include this information. Due to the fact that I am working on a design of a new set of debugging tools (Enforcer/SegTracker/etc) I do not wish to expand the current SegTracker model in too many ways. SEE ALSO "Programming is like sex - one mistake and you have to support it for life." - Michael Sinz BUGS

Amiga Checkmark

Best viewed with any browser
There have been  Counter  visitors to this page.