E X C E P T I O N - H A N D L E R D E T A I L S ------------------------------------------------------------------------------ Abstract: This document defines the exception handler mechanism of the mmu.library and how to make use of it. Please study it carefully before you try to implement an mmu.lib exception handler. For details about the concepts of a "context", read the mmu.doc. ------------------------------------------------------------------------------ What is a bus error exception: A bus error can be understood as a sort-of "interrupt" which is triggered by either a software fault or a hardware signal related to acessing memory or external devices in an unsupported fashion, like accessing a non-available memory location or writing to a write-protected memory region. All bus errors are re-directed to the MMU.library, but some of them remain unhandled and are passed over to the exec.library. Some of the remaining errors are handled automatically by the library, and those that remain will cause your "context hooks" to be called, as installed by the "AddContextHook" and "AddMessageHook" vectors of the mmu.library. The members of the MC680x0 family generate so called "bus error" for the following situations: - True physical bus errors: The CPU tried to access an external device, but this request was aborted by pulling the "bus error input" control line (TEA) of the CPU low. These bus errors are generated by an external bus control logic, avail- able for some Amiga models, on access of non-available memory. - Access errors: These errors are generated by the MMU for a non-valid access of a memory "page". As far as the MMU is concerned, complete memory is split into adjacent pages of a fixed size, usually of size 1K or 4K, and the MMU holds access rules, the so called "properties" for each of these pages. If these rules are violated, an access error is generated. The following violations may arise: The CPU tried to access a page of memory marked by the MMU as "non-resident", or tried to write to a memory block which is write- protected, or tried to access a memory block in user mode which is marked as "supervisor only" access. Of all of these bus errors, the mmu.library handles only the part which it is "interested in". True physical bus errors are not handled by the MMU.library and passed thru to the exec exception handler - which again, calls the exception handler of the task that caused the exception. Additionally, the MMU.library does not try to fix bus errors caused by so called "locked transfers", i.e. the instructions "TAS","CAS" and "CAS2". These instructions are only useful for multi-processor machines and not supported by the Amiga hardware architecture anyways. A legal Amiga program *must not* use them. Furthermore, the "MOVE16" instruction accesses of the 040 and 060 processors are not handled, too, for the same reason. All these errors are passed thru to exec, and remain unhandled. They will usually force a fatal software failure. An additional special case - which might be interesting for the experts - is writing to memory by "instruction function code". This is only possibly by loading this function code to the "dfc" register and then using the privileged "moves" instruction to write instruction data. This is unsupported, too. Just write instructions with "move", then flush the cache. This is, too, what the dos.library does when loading code. All remaining accesses are handled by the MMU library, and are first filtered again. The MMU library distinguishes: - User mode and supervisor mode accesses. All exec tasks MUST run in user mode, and this is the "default" situation. The user mode exceptions are handled according to the "MMU context" they are generated by, or, equivalently, the context the current task is attached to. All supervisor mode accesses are managed by one single context, the "supervisor context". - Data vs. instruction accesses. Whether the bus error happened due to faulted access of data, or whether the CPU tried to fetch instructions, and this instruction fetch faulted. Instruction write accesses, as they might have been generated by a "moves", might or might not be handled, this is implementation specific. The CPU itself never generates these accesses when fetching code, the only possibility is a "moves" instruction. - Read vs. write accesses. Whether data was about to be read by the CPU, or the CPU tried to write out data. Instruction accesses by the CPU are always of reads. Accesses to the stack, as for subroutine calls or "link" instructions are regarded as data accesses, too. (Read about special caveats about the stack, below!) Amongst all these types, the MMU library handles certain special cases automatically. You never see these faults from your context hooks, they are completely transparent and handled by the library as a special service. - AbsExecBase reads. To be able to mark the first memory page as "invalid", the MMU library filters out accesses to AbsExecBase itself in the first stage of the exception handler, to emulate them as fast as possible. The library will try to emulate the most popular instructions in software, for optimal performance. However, still try to avoid reading AbsExecBase in your code. Make a copy to a static library base on startup, and then use this backup copy whenever possible. Most modern C compilers do that automatically anyways, but assembly language authors should keep this in mind. DON'T (slow!) DO (fast) move.l AbsExecBase,a6 move.l mn_SysBase(a4),a6 In case the special threatment of AbsExecBase is not desired, set the MCXTAG_EXECBASE tag value of the CreateMMUContext() call to FALSE. This might be interesting for emulators, for example. Remember that interrupt code is handled by the supervisor context, AND NOT BY YOUR USER CONTEXT. Hence, accessing AbsExecBase from within interrupts is always valid, simply because the supervisor context is always build by the library itself, and this flag is always set on creation of this context. DO NOT touch the supervisor context unless you know precisely what you're doing. - Accesses above 0x0400 in the first memory page. The mmu.library emulates accesses above addresses 0x0400 to the first page in memory. This is again to allow setting the first page to "invalid" for tracing access errors in the autovector region. Accesses to addresses above and including 0x400 in the first page are therefore always legal. This feature can be disabled again with the MCXTAG_EXECBASE flag, see above. AVOID THESE ACCESSES, THEY ARE EMULATED IN SOFTWARE AND VERY SLOW. Starting with Os 3.0, exec tries to help these MMU programs by starting chip memory at 0x1000, not at 0x0400 as usual, if a 040 or 060 CPU is available. This means that for page sizes of 4K for the 040 and 060, and for page sizes of 1K for the 030 and 68851/020 MMU, no accesses need to be emulated. Please keep this in mind when using the MMU because THE EMULATION CODE IS SLOW. Try to use these "natural" page sizes whenever possible. If not possible, try to relolate the libraries and buffers in this memory region, usually some graphics.library buffers, the expansion library base, and - possibly - the exec library. Trying to execute code in this memory region does work, but is really, really slow. Each instruction access has to go thru the mmu.library exception handler, so feel prepared that your computer runs in "slo-mo mode" if you don't follow these rules. - Writes to MAPP_ROM pages. The mmu.library tries to cancel writes to pages marked as "ROM". Writes to these pages will generate an access error, which is "ignored" by the mmu.library. Hence, memory regions marked as "ROM" will get an effective "silent" write protection. However, keep in mind that a write to "ROM" area is still slow because it includes running the exception handler. This goes even more for the 060 CPU because it includes copying the "ROM" to a dummy writable backup page into which the write is then redirected. Special care must be taken for "misaligned" writes, i.e. writes that hit the "border" of a ROM region and go partially into valid RAM, and partially into "ROM" regions. The MMU.library DOES NOT guarantee that the valid part of the access is performed! It might abort the complete write! This shouldn't be much of a problem because writes to ROM should only happen due to faulty software, and it should only do good to abort these writes completely. To say that again: All the cases above are handled by the library trans- parently, you don't see them from your context hook. However, the following cases ARE seen from your hook function: - Access to MAPP_SWAPPED pages. If a read or write hits a page marked as swapped, the library calls all hooks in its "swapped" hook list. For details what happens then, see below. Again, special care must be taken if the access is "misaligned", i.e. hits more than one page at once. In this case, the library checks if both accesses are allowed, i.e. go either to valid pages, or to swapped pages. In case any of the accesses is invalid, the "segmentation fault" hook list, see below, is called. - Any other access, as there are accesses to "MAPP_INVALID" pages or to supervisor only pages in user mode, or write accesses to write protected pages are passed over to the "segmentation fault" hook list. How your hook function is called: --------------------------------- The mmu.library keeps two priority sorted lists for each MMU context, one for the "swapped" hook list for pages currently "swapped out", and another for the "segmentation fault" hook lists. The mmu library first determinates which list has to be used to handle the access, and then tries to find the right handler. The following algorithm is used: - Try the handler of higher priorities first; - Check whether this hook is activated or not, skip it if it is deactivated; - Check whether the hook is "task specific", i.e. whether the MADTAG_TASK tag was given at the time the hook was added with AddContextHook(). If the hook is task specific, check whether the faulty task is identical to the task handled by the hook and skip the handler if this is not the case. - As soon as a match has been found, call the routine at the address provided by the MADTAG_CODE tag. This should be an assembly routine, mainly for speed reasons. Furthermore, parameters are passed in registers, similar to the exec.library interrupts, and return values have to be provided in the d0 register and in the "Z" bit of the processor. You're called like this: Register a0 contains a pointer the exception data structure, discussed below. Registers a1 and a4 contain the data provided by the MADTAG_DATA tag on creation. Register a6 contains the pointer to the mmu.library base. Register a5 contains the pointer to your code. You may alter registers d0-d1, a0-a1 and a4-a5. They will be restored by the library automatically. All other registers MUST BE preserved. In case you want make use of the exception data structure, please keep in mind that it is only valid as long as your exception hook executes. That means that you possibly have to make a copy of it for later use. Remember, too, that you're called in supervisor mode, with all interrupts disabled. DO NOT ACCESS ABSEXECBASE IN YOUR CODE, use either a private copy, or the copy in the ExceptionData structure below. Accessing AbsExecBase MIGHT BE FATAL! Additionally, THERE IS NO DIRECT ACCESS to the CPU specific exception stack frame. Exception hooks are - and that's the great point about the mmu library - CPU indepentent. All data you need is provided in the exception data structure, DO NOT MAKE ANY ASSUMPTIONS which CPU you're running at, or how the stack frame looks like. This is the matter of the library. On return, you should set d0 to 0 and set the zero flag to indicate that your exception handler was able to handle the fault and return to the library with a plain "RTS". In that case, the library will no longer look for more handlers. If you haven't been able to handle the fault, return 1 in d0 and clear the zero flag (i.e. return a "ne" condition in the CCR register). In case the library did not find any hook that claimed to be able to repair the fault, it will redirect control back to the exec exception handler (which will, by default, run the GURU). BE WARNED! Another bus error within the bus error handler IS FATAL AND WILL CAUSE A GURU! Within the exception handler, YOU MUST ENSURE that all memory accesses are valid or you're lost. The library function GetPagePropertiesA() is ideal for this purpose: First, it is safe to get called within interrupts and bus errors, it is rather optimized and you find all information required to call it in the ExceptionData structure. The ExceptionData structure --------------------------- This structure is the key to the powers of the MMU. Your handler is called with a pointer in *a0 to this structure: struct ExceptionData { struct Task *exd_Task; struct MMUContext *exd_Context; ULONG *exd_Descriptor; ULONG *exd_NextDescriptor; APTR exd_FaultAddress; APTR exd_NextFaultAddress; ULONG exd_UserData; ULONG exd_NextUserData; ULONG exd_Data; APTR exd_ReturnPC; ULONG exd_Flags; ULONG exd_Properties; ULONG exd_NextProperties; UBYTE exd_internal; UBYTE exd_FunctionCode; UBYTE exd_Level; UBYTE exd_NextLevel; ULONG exd_DataRegs[8]; ULONG exd_AddrRegs[7]; UWORD *exd_SSP; UWORD *exd_USP; struct ExecBase *exd_SysBase; struct MMUBase *exd_MMUBase; }; Let's discuss the fields in this structure: exd_Task contains the pointer to the task structure of the task that caused the exception. If this hook was added to the supervisor context, this field is meaning less and must be left alone. exd_Context contains the ID of the MMU context responsible for the fault. This is always the context the hook was added to. exd_Descriptor contains a pointer to the true hardware MMU descriptor which handles the fault. This is usually of less interest except for special purposes. In case of an indirect descriptor, this points NOT to the final page descriptor, but to the indirect descriptor. exd_NextDescriptor In case of a misalgined access, i.e. an access that spawns two pages because it hit a page boundary, this is the descriptor for the end address of the access, and exd_Descriptor is the descriptor for the first address of the access. If the access is aligned, both pointers are identically. NOTE THAT THIS DESCRIPTOR NEED NOT TO POINT TO A HIGHER ADDRESS. An example for a "backwards" misaligned access is the "movem.l regs,-(ax)" instruction. exd_FaultAddress The address that faulted, the start address of the access. exd_NextFaultAddress The end address of the access that faulted, inclusive. For a long word access, this would be exd_FaultAdress + 3, for a byte access, this would be identically to exd_FaultAdress. Note that it may well be that exd_NextFaultAddress is a SMALLER address than exd_FaultAdress. This might, for example, happen for a movem with pre-decrement addressing mode, i.e. a "movem.l d0-d7/a0-a6,-(a7)". Exception handlers must be aware of this special situation. exd_UserData The user data provided for invalid and swapped pages, as defined by SetPropertiesA() and SetPagePropertiesA(). This will be NULL in case no user data is available. exd_NextUserData In case of a misalgined access, the user data of the second page that was involved in the exception. If the access was aligned, this is identical to exd_UserData. exd_Data If the access was a write access and the EXDF_WRITEDATAUNKNOWN flag (see below) is cleared, then this long word will contain the data the CPU tried to write out. The data is right-justified in this field, i.e. bytes will use bits 7..0, words will use bits 15..0 and long word accesses will use the complete field. The size is available by subtracting the exd_FaultAddress and exd_NextFaultAddress fields. On movem faults, the complete write data is unfortunately not available. You may see movem faults as a series of long word writes on some CPUs, while this instruction is atomic on other CPUs. There's nothing the MMU library can do for you here. This data should be used only for debugging purposes and "Enforcer like" applications, and for applications where you can guarantee that movems do not occur, i.e. emulators that threat "computer hardware" by the MMU and might read the data written to the emulated hardware right here. To allow the MMU library to fetch write data, you *MUST* set the "MAPP_REPAIRABLE" bit of pages you want write data for. In all other cases, you *might* get write data, but this is a matter of "good luck". (Or, in other words, I don't want to document the messy details.) Please do only set this bit in case you really need the write data because it means much more work for the library and much "higher magic" to get it, especially for the 060 CPU. In case you want to allow the CPU to continue execution and you somehow managed to write out the data itself (or you want to ignore the faulty access), set the EXDF_WRITECOMPLETE bit of the flags field below. In case you do not set this bit, the CPU will attempt to re-run the faulty instruction. In case you haven't been able to repair the fault using some other techinque (as for example by swapping in the faulty page), your exception handler will be called again. In case of a read fault, you may place the data to be read back in this field and set the EXDF_READBACK flag in the flags value below. The MMU library will then attempt to place this value in the input pipe of the CPU and to repair the faulty access by providing this dummy data. The data has to be placed right-justified in this field again. Certain restrictions arise, again: First, a movem might show up as several exceptions, hence you might be able to provide data for each register loaded, or it might show up as one single exception. In this case, all registers will be loaded with the same value. (It was hard enough to emulate this, so please don't complain...) Instruction faults can't be repaired like this. (I would have been able to provide this exclusively for the 030 and the 020, but I choose not to do so for orthogonality). Don't try it, the mmu.library will call the exec exception handler directly in case you attempt this. exd_ReturnPC The program counter of the instruction that caused the fault. This is only an approximate value due to the instruction prefetch feature of all CPUs of the 680x0 family. In case a branch was performed after the faulty instruction was loaded, and before it was detected, this PC might be completely useless. However, the program is guaranteed to continue "at the right place". In case you set the EXDF_CALL flag, this field contains the address of a small procedure which will be called in user mode, in the context of the task that caused the exception. This routine could, for example, send a message out to a "daemon" to get the fault repaired and could run into a WaitPort() to get an answer from the daemon. The user routine has access to all registers of the faulty program, and must therefore preserve all registers unless it attempts to alter them "on purpose". The exception data IS NO LONGER AVAILABLE, neither in the mmu library base nor in any register. If you want to make use of it, make a copy of it in the exception handler to a private place and access it in this user routine. Alternatively, use the "message hook" mechanism of the library which provides all this for you already if you don't want to get a headache. The user routine should then return with a "RTS" to the library code (note that the stacked PC points to some code in the library, not to the faulty code itself! It's truely deep magic to restore all this information) which will then rerun the faulty instruction. If you haven't been able to repair the cause of the fault, the exception hook will be called again! exd_Flags A combined input/output flags long word. Amongst private flags you'd better not care about, the following input flags are available: EXDF_WRITE a write fault if set. If reset, a read fault. EXDF_INSTRUCTION a fault on fetching an instruction. As a special case, this *could* be a write fault if someone tried to write out instruction data with an instruction function code using the "moves" instruction. This is actually unsupported by the library and should not be tried. EXDF_WRITEPROTECTED failed due to write protection of the destination. EXDF_SUPERVISOR failed due to supervisor only protection of the source/destination. EXDF_WRITEDATAUNKNOWN the write data was lost, exd_Data is invalid. Remember that you MUST set the MAPP_REPAIRABLE property flag for those pages you want write data for, and you should only set this bit if you really really want them. It means quite a lot of trouble for the library to provide the write data, hence things are not getting faster with it. EXDF_MISALIGNED The access was misaligned, i.e. more than one page was involved in the access. Prepare to swap in more than one page, for example. The following flags can be set by your code to let the library know what to do about the fault: EXDF_READBACK abort a faulty read and provide the exd_Data word as input for the CPU. Do not try to rerun the access. EXDF_WRITECOMPLETE abort a faulty write, do not try to rerun it. EXDF_CALL call routine whose address is in exd_ReturnPC in user mode in the context of the task that caused the exception. This routine will be run as "sub- routine" of the task that faulted, and might be used to sent out a message and wait for its reply to get the missing page fixed by another supervisor task. THIS FUNCTION IS NOT AVAILABLE for the supervisor context. The mmu.library will guru in case you try to. Especially, the message hook mechanism described below uses this flag. Note that the library does not check for you whether Wait()-ing is useful here, as for example if the task is in Forbid state. It's the matter of the exception hook to check this. The default library message exception hook does this, for example. exd_Properties The property flags of the page that was responsible for the fault. exd_NextProperties In case of a misaligned access, the flags of the second page involved in the access. exd_internal Leave this alone. exd_FunctionCode The function code of the access. The following values should be expected: For the user context hooks: 1 User data access 2 User code access For the one and only supervisor context and its hooks: 5 Supervisor data access 6 Supervisor code access All remaining function codes relate to physical bus errors and should not be passed thru to your context hook. exd_Level The level of the MMU tree at which the access fault happend and at which the MMU descriptor resides. This NEED NOT to be the "page level" for early termination descriptors or invalid descriptors at a higher level. Experts should note that this is not even guaranteed for the 040 or 060! Furthermore, in case of an indirect descriptor, this is the level of the pointer pointing to the final page descriptor, not the level of the real page descriptor. This is advanced information you usually should not care about. Level A of the MMU tree is encoded as "0", level B as "1" and so on. Note that this numbering is different from the MC680x0 internal counting. exd_NextLevel In case of a misaligned access, the level of the second descriptor involved in the fault. exd_DataRegs A copy of the data registers at the time the fault happened. These images are copied back as soon as the exception terminates, hence a "higher magic" exception handler could alter these... exd_AddrRegs A copy of the adress registers, but without the stack pointer. exd_SSP The supervisor stack pointer. This points directly to the processor specific exception stack frame. The first UWORD of this stack frame is the copy of the status register at the time of the fault. BE WARNED! Everything else is processor specific, the mmu.library might also want to modify this exception stack frame, so hands off! exd_USP The user stack pointer. exd_SysBase A pointer to the library base of the "exec.library". DO NOT ACCESS ABSEXECBASE within the exception handler. Use either a private copy, or this pointer. NOT FOLLOWING THIS RULE MIGHT BE FATAL. exd_MMUBase A pointer to the library base of the mmu.library, also in register a6. Additional notes about the exception handlers --------------------------------------------- In case you're going to write an automatic stack extension program, you should keep in mind that the EXDF_CALL - and therefore the message hook - mechanism requires about 300 bytes of user stack space. Similar, task switching takes some user stack, too. To be able to swap in stack in case of a stack overflow, a message hook alone WON'T DO GOOD for this reason. Use the following mechanism: - Add a high-level exception hook that extends the user stack in super- visor mode with a "Last Chance" backup page you must have preallocated. Note that calling AllocMem() in supervisor mode is not permitted! Return with d0 not equal zero to allow the library to call all other hooks. - Add a low-level message hook that captures the underflow and utilizes the stack you prepared in the high-level handler. For the same reason, you might have to add "Switch" and "Launch" handlers to allow execbase to store the user registers safely. Unfortunately, this is done with supervisor accessed going to the user stack, and not with a "moves". Additional wierdos to watch out for ----------------------------------- There are unfortunately a number of "features" of each CPU which should not go unmentioned: The 68020/68851 and 68030: Execution of instructions in a access-protected "zero page" is really, really slow. Please keep this in mind! "moves" to instruction space is unsupported (goes for all other CPUs as well). The FPU and exceptions: This is a dark chapter in exception handling. Of a faulty fmove, only the first two long words are checked by the 68030 and therefore at most two hits get reported, all others go unnoticed. Of a faulty fmovem, only the first two long words hits get reported, too. The CPU will ignore all other hits and continue execution. This means specifically that a Debugger tool might not be able to see all hits that happened on these instructions, and you will not be able to capture them for private use. VM systems shouldn't have much problems with that, though, since the invalid page should have been swapped in and each access can hit at most two pages at once if it is misaligned. However, you must be prepared that the mmu.library will not be able to set the "misaligned" flag correctly in this case. The 68040: Movem writeback data is not always available, and certain movems can't be continued safely. In case a movem read or write goes to an invalid page other than at level 2 or 3, the mmu library has to abort the movem completely. There's nothing I can do against it. To avoid this situation in case you really need the movem data, set the MAPP_REPAIRABLE property. Movems to a register included in the register list itself cannot be rerun safely in all circumstances. Avoid them. Readback data is only available for all destination registers at once. The FPU and exceptions: Even more problems than with the 030. If a fmove or fmovem moves more than two long words, for example a fmove.x or any fmovem, the processor will only notice the first access of a faulty page. If the instruction would catch more than one hit, the processor restarts the instruction completely from the beginning. This means for debugging tools that you will *not* be able to fix a faulty instruction of this kind at all, the processor will loop forever. Note that even disassembling the instruction at the PC will not help because the PC might point to an FPU instruction even if the hit was caused by the instruction before. Hence, the following two instructions move.l d0,(a3) fmovem.x fp0,(a3) with a3 pointing to an invalid page, will both invoke the exception handler with the same PC value, namely the address of the fmovem. This might be a problem for VMM systems as well: Because there's no single flag that tells whether the hit was caused by an FPU instruction or not, the processor might re-run a FPU instruction as mentioned above again, possibly accessing the two different pages. If the VMM system would have swapped in the second page by the first hit, and removed the first page accessed by this instruction from memory, re-running this instruction will cause another VMM hit, this time from the first page. It is therefore important that a VMM system *MUST NOT* swap out a page previously swapped in. At least two pages must be available at once. The 68060: The library handles branch cache flushes transparently, no need to worry about them. So much for the good point. Writeback data is usually not available unless the library emulates this with some heavy magic and is told to do so with the MAPP_REPAIRABLE flag. Things are getting even worse if no exception handler wants to make use of the writeback data and returns with the EXDF_WRITEBACK flag cleared. In this case, the mmu.library tries to rerun the faulty instruction again, even though it already executed. It tries to rebuild all the registers (except for the FPU registers and the supervisor model and the SSP) and then returns to the PC of the faulty instruction. Whether this mechanism is reliable or not has to be proven. Except that, all 040 restrictions regarding movems apply here again, see above. In case of a non-locked read-modify-write access, as for example a addq.l #1,(ax), the library will call the context hooks twice, once with the read data, and then again with the write data. However, things are getting rather nasty in case of a misaligned RMW access of which one part of the access is valid and the other part is not. The 060 manual states that these accesses can't be rerun safely because parts of the data might have been written back already, and rerunning the instruction might cause this wrong data to be re-used again. I can't comment on that feature because I haven't tried to reproduce it yet, but it may reduce reliability of the 060 for virtual memory appli- cations.