Amiga.org

Operating System Specific Discussions => Amiga OS => Amiga OS -- Development => Topic started by: Karlos on October 22, 2006, 03:30:39 AM

Title: From low level exception handling to high level...
Post by: Karlos on October 22, 2006, 03:30:39 AM
Hi,

I am wondering:

Is it possible to write a routine for Task->tc_TrapCode that can cause a second, higher level routine to be called upon return from the exception (rte) when the system is back in User mode?

What I basically want to do is to use low level traps to invoke higher level error handling such that handling a 680x0 trap, such as divide by zero has the overall effect of calling my high level function immediately after returning to User mode, rather than trying to do something about the error in the 680x0 trap code itself. The reason for this strange request is so that I can do something like this:

Code: [Select]

// hidden in some implementation file
void throwZeroDiv()
{
  throw ZeroDivide();
}

// in 'normal' code
int exampleFunction(int a, int b)
{
  return a/b;
}

//....

try {
  exampleFunction(2,0);
} catch (ZeroDivide& zd) {
  //...
}



Now, it would be easy to call throwZeroDiv() from within the 680x0 trap as set by Task->tc_TrapCode but, of course, the stack would be the SSP rather than the USP. Consequently, the supervisor stack would be unwound and I expect it would all go mental very quickly.

So, what I need to do is write a nice low level 'intermediate' handler in assembler (most likely) which would sneakily modify the stack of the user code to fool it into calling throwZeroDiv() upon executing the rte instruction.

I can see that this would need some careful manipulation of the stack frame. Has anybody actually done anything like this?

I realise that checking to see if an argument is zero and then throwing an appropriate exception directly would seem to be the easiest solution but what I am looking for is a 'zero touch' solution that doesn't require this and therefore doesn't affect the performance of 'good' code in normal operation.
Title: Re: From low level exception handling to high level...
Post by: Piru on October 22, 2006, 03:42:33 AM
I'm sorry but this is not going to work well. You have absolutely no way of knowing the state in which the system is when the exception occurs. It could well be inside some critical system semaphore being locked, and calling your high level function would just fubar the system.

The only semi-bulletproof way of doing this would be to have some signal or message being sent, and then the processing of the event would be delayed. This wouldn't be runtime processing of the exception though. And it still could be inside some critical system function where even Signal would be fatal.

One way of doing this would be to have some handler process around. At exception time a simple "frame" would be created, and it would be fed to the handler, and the offending task would be put to sleep. Then the handler would get the event, process it, and then (possibly) make the offending task continue. This would still nuke majorly if the offender is holding some semaphore the handler also wants. Exception handlers and high level functions just don't mix. Anyway, this method is what apps such as TNT and SmartCrash use. Check SmartCrash archive (http://www.aminet.net/package.php?package=dev/debug/SmartCrash.lha) for source code.
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 22, 2006, 04:06:43 AM
@Piru

Cheers.

Do you ever sleep? :lol:

Quote
It could well be inside some critical system semaphore being locked, and calling your high level function would just fubar the system.


What if I could guarentee that the the tc_TrapCode would only be set to point to our "handler" above the implementation layer (by which I mean the place where all OS calls are made) ?
Title: Re: From low level exception handling to high level...
Post by: Piru on October 22, 2006, 03:57:40 PM
@Karlos

Quote
Do you ever sleep?

I do. Perhaps not at the same time as others. I also like blood, and... Ehh, wait, forget that.

Quote
What if I could guarentee that the the tc_TrapCode would only be set to point to our "handler" above the implementation layer (by which I mean the place where all OS calls are made) ?

If you mean that the tc_Trapcode would be set while executing your own code. Yes, that would work. But it could still get lockup condition, unless if you really isolate ALL OS call stuff from user code (for example no user code gets to run at all with any OS lock obtained). Also, you'd naturally still need to handle the case you get exception while doing something semi-critical in your own code... But at least that is easy to predict, unlike some OS related lock ups.
Title: Re: From low level exception handling to high level...
Post by: motorollin on October 22, 2006, 04:35:17 PM
How do you guys know all this stuff?  :-o

--
moto
Title: Re: From low level exception handling to high level...
Post by: Piru on October 22, 2006, 04:44:58 PM
Quote
How do you guys know all this stuff?

20 years of hacking helps.
Title: Re: From low level exception handling to high level...
Post by: motorollin on October 22, 2006, 04:52:55 PM
I suppose that would be an advantage (and would mean I started hacking at age 3 :lol: )

--
moto
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 22, 2006, 05:13:28 PM
@Piru

Quote
I do. Perhaps not at the same time as others. I also like blood, and... Ehh, wait, forget that.


A bit hypocritical of me, considering what time I posted. Surprised you never picked up on that :lol:

Quote

If you mean that the tc_Trapcode would be set while executing your own code. Yes, that would work. But it could still get lockup condition, unless if you really isolate ALL OS call stuff from user code (for example no user code gets to run at all with any OS lock obtained).


Well now, see that's the thing :-) The purpose of the implementation layer is to hide away the OS. It's even namespaced so that unless you are really trying to deliberately code at an OS level, you can't even reach it.

That said, OS Locks may be held within my code as a consequence of using synchronised calls (using the Lock class, for example). However, the destructor for this class guarentees that any lock it holds will be released.

Consider this:

Code: [Select]


class Lockable {
  // service class, wraps signalsemaphore
  // inherit this class to get threadsafe locking properties
  public:
    void waitLock(); // May throw ObjectDestroyed
    void freeLock();
    // ....other locking methods

  public:
    ~Lockable();

  private:
    OSNative::SignalSemaphore sem;
    bool destroyed;
  // guts...
};

void Lockable::waitLock()
{
  // if this throws ObjectDestroyed, it is considered
  // illegal for the caller to access the object again
  // in any way, shape or form.
  if (destroyed) {
    // too late
    throw ObjectDestroyed();
  }
  OSNative::ObtainSemaphore(&sem);
  if (destroyed) {
    // we got unlucky whilst waiting
    OSNative::releaseSemaphore(&sem);
    throw ObjectDestroyed();
  }
  // ok, we got the lock.
}

void Lockable::freeLock()
{
  if (!destroyed) {
    OSNative::ReleaseSemaphore(&sem);
  }
}

Lockable::~Lockable()
{
  OSNative::ObtainSemaphore(&sem);
  destroyed = true;
  OSNative::ReleaseSemaphore(&sem);

  // Any pending calls to waitLock() in concurrent threads
  // will now throw ObjectDestroyed() as soon as they obtain
  // the semaphore. Wait for the semaphore again before
  // exiting the destructor, to ensure this is the last
  // thread that will touch the object.

  OSNative::ObtainSemaphore(&sem);
  OSNative::ReleaseSemaphore(&sem);
}

// auxilliary class, used to make functions safely synchronizable:

class LockExclusive {
  public:
    LockExclusive(Lockable* obj) : item(obj) { item->waitLock(); }
    ~LockExclusive() { item->freeLock(); }

  private:
     Lockable* item;
};


The above is more or less the locking strategy my high level code uses.

To incorporate our handler, consider:

Code: [Select]

class MyClass : public Lockable {

  public:
   int riskyOperation(int a, int b);

  // ....

  private:
    int n;
}

int MyClass::riskyOperation(int a, int b)
{
  // Irrespective of how this function terminates, the
  // automatic instance of ExclusiveLock should be destroyed
  // correctly.

  LockEsclusive lock(this);
  n = a / b;
  return n;
}


This is the principle way I use locking in high level code. If something throws after 'lock' is instansiated, 'lock' itself will be destroyed and consequently will release the lock on the object it was called for (in this trivial case, our "MyClass" object).

So, if our a/b invokes our tc_TrapCode exception mechanism and 'appears' to magically throw ZeroDivide, our lock ought to be released cleanly.

Or at least, this is the idea.
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 22, 2006, 05:20:58 PM
Quote

motorollin wrote:
How do you guys know all this stuff?  :-o

--
moto


I don't, that's why I'm asking ;-)
Title: Re: From low level exception handling to high level...
Post by: motorollin on October 22, 2006, 05:24:09 PM
Quote
Karlos wrote:
Quote
motorollin wrote:
How do you guys know all this stuff?  :-o

I don't, that's why I'm asking ;-)

True. But you knew enough to ask the question in the first place  :idea:

--
moto
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 22, 2006, 05:40:57 PM
That would be a side effect of my near schiziophrenic passion for both assembly language (low level) and C++ (other end of the scale)...

Anyway, getting back to the point, what I need to be able to do in order to at least test this idea is implement some sort of low level exception handler (in the 680x0 sense) that modifies the stackframe passed to it such that on executing the rte (return from exception) instruction, the flow of execution jumps into a function of my choosing, making it look as if the actual code went there all by itself. That function then throws the equivalent C++ exception that matches the class of error trapped by our low level code.

In other words, it would make the code appear to be modified as follows:

void throwZeroDivide()
{
  throw ZeroDivide();
}
//....

int n = a/b;

would, as if by magic, appear to be self-modifying, thus:

int n = throwZeroDivide(), a/b;

for the case where b was zero.

The issue is that I'm not sure how to modify the 680x0 stack frame in the low level handler to get the flow of execution to return to the 'wrong' place (throwZeroDivide()) and making it appear that the original code jumped there, rather than being shoved there by the trap.

Suggestions?
Title: Re: From low level exception handling to high level...
Post by: Piru on October 22, 2006, 06:11:45 PM
Quote
The issue is that I'm not sure how to modify the 680x0 stack frame in the low level handler to get the flow of execution to return to the 'wrong' place (throwZeroDivide()) and making it appear that the original code jumped there, rather than being shoved there by the trap.

You can't do that (easily). The problem is that you'd need something similar to setjmp/longjmp to restore the register and stack to known good situation. But if you do that, you again lose ability to return to previous context.

Making the code to return to some other location is trivial. Anything else gets VERY tricky.

If you intend to get this working you need assembly at both exception and the userlevel side.

Did you check the SmartCrash src? It doesn't divert the execution at all, though.
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 22, 2006, 06:31:57 PM
That's what I mean, really. Making it jump to some other place is not complicated, but preserving any contextual data is the hard part. Furthermore, the jump has to be executed immediately after we leave the supervisor state, which is slightly trickier than doing it whilst in the supervisor state - I can only assume this involves poking the stackframe.

Not checked the smart crash sources yet. I'm just working on this as a thought experiment for now :-)

-edit-

*sigh* It's been so long since I did any 'real' programming...
Title: Re: From low level exception handling to high level...
Post by: Piru on October 22, 2006, 06:36:19 PM
This works 010+ and up at least:
Code: [Select]

addq.l #4,sp         ; pop out the exception type in stack
move.l #myfunc,2(sp) ; new return address
rte

But that's it. There is NO context saving or restoring of any kind here. The execution continues at myfunc with the stack and register at the time of the exception.
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 22, 2006, 06:59:47 PM
Hmm, I think I got a bit confused: I was thinking that a7 was the SSP during the trap handling but your code implies it's the USP ?

Title: Re: From low level exception handling to high level...
Post by: Piru on October 22, 2006, 09:09:55 PM
That is SSP.

The exec exception handler pushes 1 extra longword to stack, it's the vector number (say division by zero 0x14 >> 2 = 5 will be in stack).

Before exiting the exception handler the extra longword must be popped out of the stack. After that it's the regular stackframe in the SSP. With 68010++, all stackframes begin with:
Code: [Select]

UWORD sr;
APTR  returnaddr;

Thus poking 2(sp) with new address will make the exception return to poked address.
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 22, 2006, 09:14:01 PM
Right, now that makes sense :-D

Sorry, it's been a while ;-)

I don't seem to have my 680x0 UM's handy anyplace atm either...

Assuming that I write a handler that doesn't trash any registers right up until the 'throw ' is invoked, I'm wondering how much the context matters. The actual try { } block that would surround the code at the high level gives you your basic restore point anyway.

-edit-

It's quite amusing really, I only want to do all this just so I can get away with not having to test certain things before executing statements that might result in a problem :-)

I'm sure most people would be perfectly happy with 'if (b==0) { throw ZeroDivide(); }' before any such division...
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 22, 2006, 09:46:48 PM
Enough theorising for now, methinks it is time to test the idea out :-)
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 23, 2006, 12:24:58 AM
Quote

motorollin wrote:
I suppose that would be an advantage (and would mean I started hacking at age 3 :lol: )

--
moto


How old do you think he was when he started:

(http://www.amiga.org/uploads/cavt402e47c9927d1.jpg)
;-)
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 23, 2006, 01:58:47 AM
Quote

Karlos wrote:
Enough theorising for now, methinks it is time to test the idea out :-)


Well, I tried it with a quick and dirty test program and it appears to work fine:

For those interested:

Code: [Select]

mc68020

SECTION ":0", CODE

XREF _throwBoom
XDEF _trap_68K

_trap_68K
lea    4(a7), a7
move.l #_throwBoom, 2(a7)
rte


All this does trap handler does is cause the execution to jump to a function called "throwBoom()":

Code: [Select]

/*

Evil low level -> high level exception test code

*/

#include
#include
#include


struct Boom {}; // simple exception struct

extern "C" {

void trap_68K(); // defined in trap.asm
void throwBoom() // referenced by trap_68K
{
throw Boom();
}

}


int main(int argN, char** argV)
{
// install our trap
Task* me = FindTask(0);
void* oldTrap = me->tc_TrapCode;
me->tc_TrapCode = (void*)(&trap_68K);
std::puts("Installed custom Trap");

// try it out
try {
int a = 1, b=0;
std::puts("Time to try something naughty...");
int c = a/b;
std::printf("\n%d\n",c);
}
catch (Boom) {
std::puts("Caught a Boom");
}
catch (...) {
std::puts("Caught... something anyway");
}


// restore the original trap
me->tc_TrapCode = oldTrap;
std::puts("Restored Trap");

return 0;
}


Running this produces the anticipated output:

Quote

Installed custom Trap
Time to try something naughty...
Caught a Boom
Restored Trap


In all, I'm quite happy about that. Some more detailed analysis needs to be done, however, to see what it does in more complicated examples ;-)
Title: Re: From low level exception handling to high level...
Post by: Piru on October 23, 2006, 02:07:24 AM
I believe in this case you're lucky to have "close" enough environment in both the code nuking and the catch part. However, I'm pretty confident this isn't always the case.

So I believe to be fully functional in all cases this thing requires some sort of state saving and restoring. For simple cases this could well be enough, however.
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 23, 2006, 02:19:12 AM
Don't worry, I intend to investigate it in greater depth. The state information is largely taken care of by the C++ stack unwinding. I've managed to ascertain so far that in a slightly modified version of this example where a, b and c are declared outside the try {} block that each have the value they had right up until the exception is raised, which is what I was expecting.

If I can ensure it satisfies the same criteria as a normal C++ exception then I'll be happy enough.

Dealing with low level locks etc. is slightly beyond the scope of any language level exception processing mechanism in any event.
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 23, 2006, 02:33:27 AM
Ok, I've confirmed that objects created prior to the exception   that was raised in this "underhand" way still have their destructors called during the stack unwind. In other words, it seems to be indistinguishable so far from invoking a C++ exception manually :-)
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 23, 2006, 12:54:45 PM
Right...

Assuming all threads in my code (including the main one) have Task->tc_UserData pointing to a handle (containing a special identifier) that gives access to the actual Thread class instance, the next trick is:

Within the supervisor mode trap, can I call FindTask(0) safely  so that I can test to see if the executing task is one of my Thread instances?

The reason being that when I install my own trap handler, I'd like to be able to get the original tc_TrapCode if my trap decides it cannot handle the error (that is to say, find a reasonable C++ exception to throw for it).

Naturally I'd use my Thread class implementation to store the original tc_TrapCode, so that all the asm handler has to do is first ascertain if the thread is one of mine (which it should be to be even called in the first place) then if it falls through all the cmp #trapNumber, (a7) tests it can load the old tc_TrapCode from the Thread, pop it on the stack then rts to it...

I don't really want to handle things like Line A emulation errors and the like...
Title: Re: From low level exception handling to high level...
Post by: Piru on October 23, 2006, 01:14:17 PM
Quote
Within the supervisor mode trap, can I call FindTask(0) safely so that I can test to see if the executing task is one of my Thread instances?

Yes. FindTask(NULL) is "return SysBase->ThisTask;" basically. It's always safe.

Quote
Naturally I'd use my Thread class implementation to store the original tc_TrapCode, so that all the asm handler has to do is first ascertain if the thread is one of mine (which it should be to be even called in the first place) then if it falls through all the cmp #trapNumber, (a7) tests it can load the old tc_TrapCode from the Thread, pop it on the stack then rts to it...

I don't really want to handle things like Line A emulation errors and the like...

Sounds good.
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 23, 2006, 01:38:36 PM
This idea is starting to shape up quite nicely :-)

From the point of view of describing a trap, do the following exceptions make sense (remember that these exception classes are meant to be meaningful on other platforms than the 68K/AmigaOS) in your opinion?

TRAP #2 (access fault) -> class SegmentationFault
TRAP #3 (address error) -> class BadAlignment
TRAP #5 (our favourite) -> class ZeroDivide
TRAP #6 (chk/chk2) -> class RangeError

these might also make sense, if the FPU is triggered to trap

TRAP #50 (FP zero divide) -> class ZeroDivide
TRAP #51 (FP underflow) -> class Underflow
TRAP #53 (FP overlflow) -> class Overflow
TRAP #54 (FP NAN) -> class InvalidOperand

Naturally, these classes fall into a heirarchy and are ultimately all derived from RuntimeError
Title: Re: From low level exception handling to high level...
Post by: Speelgoedmannetje on October 23, 2006, 01:48:54 PM
I haven't had the time to read every post in the thread, and I have never programmed for the 68k, but usually, when I code in assembly I use a watchdog timer if things can become critical (only IF - when I program I usually try to minimize the 'working environment' as much as possible.)
Title: Re: From low level exception handling to high level...
Post by: Speelgoedmannetje on October 23, 2006, 01:51:52 PM
Quote

motorollin wrote:
How do you guys know all this stuff?  :-o

--
moto
With patience.
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 23, 2006, 02:06:51 PM
@Speel

The basic idea here is to utilize the CPU's own ability to detect certain errors in order to invoke the language level exception mechanism (in this case, C++).

So, for example, instead of always having to check if you are about to divide by zero in some function, you can just do the division. If it goes wrong, an exception is thrown at the point the divide operation takes place.

Other errors you might encounter come from bad data alignment, accessing memory that doesnt exist and that kind of thing.

C++ itself offers no mechanism to detect these, but the system does. All I am doing is marrying the system level ability to detect a problem with C++ ability to report it at user code level.

And so far, it's working ;-)
Title: Re: From low level exception handling to high level...
Post by: Karlos on October 24, 2006, 11:47:42 AM
Quote

Speelgoedmannetje wrote:

and I have never programmed for the 68k


You are missing out, my friend! M86K assembly is actually fun. I wish I could say the same about PPC or x86, both of which I can manage (PPC moreso than x86) but they always seem to be a bit of a grind.
Title: Re: From low level exception handling to high level...
Post by: Karlos on November 05, 2006, 05:34:05 PM
@Piru
Quote

Piru wrote:
@Karlos

Quote
Do you ever sleep?

I do. Perhaps not at the same time as others. I also like blood, and... Ehh, wait, forget that.


Your terrible secret is out:
(http://www.extropia.co.uk/img/piru_68k.gif)


(sorry, couldnae resist!)
Title: Re: From low level exception handling to high level...
Post by: Karlos on November 07, 2006, 01:00:14 PM
@Anybody interested

Quote

Piru wrote:
I believe in this case you're lucky to have "close" enough environment in both the code nuking and the catch part. However, I'm pretty confident this isn't always the case.

So I believe to be fully functional in all cases this thing requires some sort of state saving and restoring. For simple cases this could well be enough, however.


I made some extensive tests of the idea. The behaviour is exactly as you would expect from manually checking for the problem and throwing a appropriate exception. The only difference is that you don't actually have to check for condition, thereby removing conditional tests for every case.



There is an impact, as Piru predicted, on things like holding locks etc. However, the impact is exactly the same as if you throw an exception manually from within a piece of code. As with all error handling strategies, anything that can be handled locally obviously should be. By using automatically created and destroyed auxilliaries (see earlier post with the Lockable class example) that handle locking (lock on create, unlock on destruct) within a function, this problem is totally mitigated from C++ side.

An exception can occur deep within the OS, which is a different prospect, but simply restoring the trap within deep implementation of an OS realisation of a class is more than sufficient to handle this case.

So the end result is, for high level code the technique works, and works well. When dealing with much lower level code, which naturally should be restricted only to implementation details, the handling can be safely deactivated to prevent any ill effects from diverting the flow of execution at the point of failure.
Title: Re: From low level exception handling to high level...
Post by: Karlos on November 14, 2006, 12:30:41 PM
One for Piru:

Any way of getting the CPU to trap access to address zero? I am basically assuming this isn't feasable without buggering around with the MMU?

If it is feasable, that's the hardware issued NullPointerException sorted :-)
Title: Re: From low level exception handling to high level...
Post by: Piru on November 14, 2006, 01:11:49 PM
Quote
Any way of getting the CPU to trap access to address zero? I am basically assuming this isn't feasable without buggering around with the MMU?

Correct, this is not possible without making the first MMU page invalid. Even then, access to bytes 4..7 must remain valid (SysBase load).

Quote
If it is feasable, that's the hardware issued NullPointerException sorted

Actually not quite, it would not catch read accesses that end up between 4..7.

Since this would require per task MMU tables, the only semi-feasible way to do it would be mmu.library (http://www.aminet.net/package/util/libs/MMULib).
Title: Re: From low level exception handling to high level...
Post by: Karlos on November 14, 2006, 01:28:34 PM
@Piru

Thanks, that's pretty much what I thought anyway. Shame, mmu.library seems to utterly hate my system :-(