Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Exception Handling #9

Closed
charsleysa opened this issue Nov 28, 2013 · 28 comments
Closed

Implement Exception Handling #9

charsleysa opened this issue Nov 28, 2013 · 28 comments
Assignees
Labels
Milestone

Comments

@charsleysa
Copy link
Member

Missing Mosa.Internal.ExceptionEngine

Are there any ideas on how this should be implemented?
I would like to help any way I can.
I'm currently having to comment out the content of IIRVisitor.Throw to get things to compile since I've used throws in some code I've added to Korlib.

Cheers.

@tgiphil
Copy link
Member

tgiphil commented Nov 28, 2013

Hi -

The new register allocator does not fully support try/except/finally regions yet. It's very high on the priority list. At the moment, avoid the use of exceptions.

@ghost ghost assigned tgiphil Nov 28, 2013
@mosa mosa added this to the Future milestone May 26, 2014
@tgiphil tgiphil removed their assignment May 27, 2014
@charsleysa
Copy link
Member Author

So I've been thinking about this for a while and how to implement it and came up with the following solution:

Change the calling convention so that the address of the method Metadata gets pushed on to the stack.

Convert throw instructions to call instructions which call a processing function which will walk the stack and at every step it will fetch the return pointer that is pushed on to the stack by the call instruction.

Using this pointer address a look up will be performed on the Metadata table that contains all the try definitions in the method and will test to see if the pointer is within the range of the try definition.

if the pointer is outside the range then the process finds the next pointer and so on. If a range is never found then a kernel fault is raised.

If the pointer is inside the range and matches a catch clause then return the address of the start of the clause.

Unwind the stack and perform a jump (not a call) to the specified clause address which will deal with the exception.

@tgiphil
Copy link
Member

tgiphil commented Aug 10, 2014

Lookup the current method using the EIP and the Method Lookup Table, instead of modifying the calling convention. To determine the previous caller method, look at the stack at [EBP -4].

For exception/finally handlers calling, store the return address on the local stack as a temp variable. That way the stack layout is compile time constant and would help simplify stack unwinding in some cases. The end of the exception/finally handlers code can simply JMP to the caller rather than RET to return to the caller (whatever that might be).

Note: The exception type must be stored on the stack as a temp (and not a virtual register). Due to the non-normal flow control caused by exception processing. And the metadata "Exception Handler Table" needs to store this temp location (offset from EBP) since the stack unwind know where to store the exception type. Or, an alternative implementation is to always store the exception type in a specific virtual register, and add IR.Gen {register} instruction to the top all the exception/finally handlers.

@charsleysa
Copy link
Member Author

Can't use EIP as that would be pointing inside the exception engine.

Another way to determine the current method we're stack walking on would be to use the return pointer [EBP-4] and the Method Lookup Table as you suggested instead of modify the calling convention.

Exceptions are objects so the type can be deduced from the object.

Exception handler address, and exception object address will be stored in registers then a stack unwind (not a stack rewind) will be performed modifying the ESP and EBP registers until we reach the Method that contains the handler then we will perform a jump to the handler and continue normal execution.

@tgiphil
Copy link
Member

tgiphil commented Aug 10, 2014

Correction: exception type should have been exception object.

@charsleysa
Copy link
Member Author

Wouldn't it be best to have an object on the heap as it follows the correct code structure and would be fairly simple to pass along to the handler as the handlers can be forced to use a certain register for holding the exception object address?

@kiootic
Copy link
Contributor

kiootic commented Aug 10, 2014

IIRC, Exception handling is commonly implemented using interrupts. In interrupt handlers, the EIP is saved in stack and restored after the execution of handler. You can read the EIP and modify it to point to the exception handler in the interrupt handler.

@tgiphil
Copy link
Member

tgiphil commented Aug 10, 2014

What would be stored in this object?

The specific register for the exception object can be fixed at compiler time. That's fine when calling into a exception/finally clause because all virtual register are reloaded anyway. (Exceptions can be generated at almost any point and the exact state of the register would be unknown.)

@tgiphil
Copy link
Member

tgiphil commented Aug 10, 2014

To follow up on @kiootic comment, exceptions can also be generated by the CPU (divide by zero), or OS (out of memory, or thread-abort). And thread-abort being a special case as control never returns to the non-exception flow control and instead the application and/or thread is terminated.

@charsleysa
Copy link
Member Author

The problem I see is that the stack needs to be destroyed all the way down until it reaches the Method that the contains the try/catch, otherwise we'll have an improperly aligned stack of we'll have to modify the catch so that it destroys the stack.

The exception object can contain anything as it is an object, at the moment it contains error messages.

Please note I am talking about Runtime Exceptions.
CPU exceptions that will be allowed to be handled by the Runtime and not special code will need to be wrapped and thrown just like a normal Runtime Exception.

@kiootic
Copy link
Contributor

kiootic commented Aug 10, 2014

Destroying stack frames should not be a hard problem, since it involves only poping EBP out of the stack, and it can be done along with stack walking. The exception handler expects the current stack frame to be its own method's, so it should be done by the exception engine.

@charsleysa
Copy link
Member Author

@kiootic basically described the way I wanted to do it, with the exception of doing a second stack walk to destroy the stack so that we have the entire stack still available while we are processing the throwing of the exception should the need arise to use information from the stack, then destroy the stack just before jumping to the catch.

Also this would allow stopping the stack destruction when we reach a certain point in the stack, such as the OS Boot method, so that we can maybe issue some sort of kernel fault that the OS can process without need to wrap the entire OS Boot method in a try/catch.

@tgiphil
Copy link
Member

tgiphil commented Aug 10, 2014

Let's walk thru a typical exception flow for a throw and rough out a plan:

  1. IR throw {object} places the exception object on the stack and generates a CALL to the platform specific ExceptionHandler Throw({object}) method.
  2. Throw() method retrieves the calling method address by looking at [EBP-4] and the exception object. It then determines the exception type of the object.
  3. Next, the method is looked up by the method address using the "Method Lookup Table".
  4. Next, the exception clause is looked up using the "Exception Handler Table". The innermost exception handler is retrieved first.
  5. If the exception handler type matches the exception object's type, then goto step 9.
  6. If not, look for another exception handler (next outer most).
  7. If out of exception handlers, unwind the stack, restart at step 4.
  8. If none are found, terminate the thread.
  9. The exception object is placed in a register, for simplicity, let's use EAX. If there is no exception object required (i.e.., for finally clause), the register is unaffected. (Use IR.GEN EAX at the top of the exception handler to inform the register allocator that EAX is alive/used on entry).
  10. The return address (*) is placed in a register or stack, again for simplicity, let's use EDX.
  11. Jump to the exception handler.

TODO: (*) Determining the return address.

@charsleysa
Copy link
Member Author

I have modified the flow to how I think it should occur:

  1. IR throw {object} generates a CALL to the platforms specific ExceptionHandler Throw({object}) method. (the {object} parameter is an address to the already generated exception object that is on the heap, there may be some confusion as c# generates a newobj instruction then a throw instruction, check the generate CIL for clarification).
  2. Throw() method walks the stack and retrieves the calling method address by looking at [EBP-4].
  3. Next, the method is looked up by method address using the "Method Lookup Table".
  4. Next, the exception clause is looked up using the "Exception Handler Table". The innermost exception handler is retrieved first.
  5. If the exception handler type matches the exception object's type, then goto 9.
  6. If not, look for another exception handler (next catch, or next outer most).
  7. If out of exception handler, restart at (2).
  8. If none are found, terminate the thread. If the thread is a main thread throw an interrupt to give the OS a last chance to recover.
  9. The exception object address is placed in a register, for simplistic, let's use EDX. (Use IR.GEN EDX at the top of the exception handler to inform the register allocator that EDX is alive/used on entry).
  10. Jump to the exception handler.
  11. It is the job of exception handler to clear EDX.
  12. If we fall into a finally then it must end by checking if EDX is set, if it is set then it must restart at (1), otherwise continue normal execution. (finally handlers do not stop exceptions from bubbling if uncaught by a catch handler).

Please note that finally handlers are finicky in that they must always be executed before normal execution is resumed.

Take a look at this for some examples of when they execute http://csharp.2000things.com/tag/finally/

@grover
Copy link
Contributor

grover commented Aug 10, 2014

Hi,

I would propose a different alternative to this:

Every method that has exception handlers should push the address of its exception handler table an exception handler stack right after setting up the stack frame. This way we can optimize handler lookups by not having to traverse the stack and looking up the methods in the method lookup table. It is a small penalty at runtime in general, but should lead to much better exception processing times compared to the way Stefan described. This is an approach as used by the vectored exception handling in Win32 and I believe that the .NET exception handling would fit well with that. An additional advantage of this is that stack corruption would not interfere with exception handling, e.g. handlers are still looked up properly even though the callstack was corrupted for any reason.

The exception handler stack would have to be thread specific like the normal call stack.

Regards,
Michael

Am 10.08.2014 um 19:06 schrieb Stefan Andres Charsley notifications@github.com:

I have modified the flow to how I think it should occur:

IR throw {object} generates a CALL to the platforms specific ExceptionHandler Throw({object}) method. (the {object} parameter is an address to the already generated exception object that is on the heap, there may be some confusion as c# generates a newobj instruction then a throw instruction, check the generate CIL for clarification).

Throw() method walks the stack and retrieves the calling method address by looking at [EBP-4].

Next, the method is looked up by method address using the "Method Lookup Table".

Next, the exception clause is looked up using the "Exception Handler Table". The innermost exception handler is retrieved first.

If the exception handler type matches the exception object's type, then goto 9.

If not, look for another exception handler (next catch, or next outer most).

If out of exception handler, restart at (2).

If none are found, terminate the thread. If the thread is a main thread throw an interrupt to give the OS a last chance to recover.

The exception object address is placed in a register, for simplistic, let's use EDX. (Use IR.GEN EDX at the top of the exception handler to inform the register allocator that EDX is alive/used on entry).

Jump to the exception handler.

It is the job of exception handler to clear EDX.

If we fall into a finally then it must end by checking if EDX is set, if it is set then it must restart at (1), otherwise continue normal execution. (finally handlers do not stop exceptions from bubbling if uncaught by a catch handler).

Please note that finally handlers are finicky in that they must always be executed before normal execution is resumed.

Take a look at this for some examples of when they execute http://csharp.2000things.com/tag/finally/


Reply to this email directly or view it on GitHub.

@charsleysa
Copy link
Member Author

Hi @grover, that would require modifying the calling convention which is preferred to stay the same.

Are you proposing some sort of dual stack or have I misread your comment?

@tgiphil
Copy link
Member

tgiphil commented Aug 10, 2014

I believe @grover is proposing a separate stack for exception handlers. Unfortunately, that'll either use up a register to track the exception handler stack location, or a query the kernel to determine the location of the exception handler stack. The kernel can lookup the thread via the current stack pointer, and return the exception handler stack for that thread.

IHMO - I'd still lean towards lookup via the method table --- as doesn't add any cost to the normal flow control. And performance costs associated with exceptions exist only when exceptions are used.

@tgiphil
Copy link
Member

tgiphil commented Aug 10, 2014

Note: "Use IR.GEN EDX at the top of the exception handler to inform the register allocator ".

Immediately after the IR.GEN EDX, add a move instruction to copy EDX to a virtual register. Otherwise, EDX will not be available for use. The register allocator attempt to keep the virtual register in a physical register and favor keeping it in the source register as well (EDX, in this case).

@tgiphil
Copy link
Member

tgiphil commented Aug 10, 2014

The part that we are missing so far in our discussion is determining to set the return address of exception handlers. Ideas?

@charsleysa
Copy link
Member Author

Which return address? Why do they need one?

@tgiphil
Copy link
Member

tgiphil commented Aug 10, 2014

Here's are two scenarios: 1) at the end of a try block which has a finally handler, the finally handler is called. In this case, the return address for the finally hander is simply the next block after the whole try/finally block. 2) However, the finally block can also be called as part of a deeper exception that is processed up thru the call stack. In this case, the return address is a bit more difficult to determine. It's probably back to an ExceptionHandler method so it can continue to propagate the exception further up the stack. However, this is the part that I find difficult to resolve; specially, how to get a reference to the original exception object? Maybe it's passed to the finally block in another register, even if the finally block never uses it, and placed back in that register prior to JMP to the return address.

@charsleysa
Copy link
Member Author

I probably didn't make it very clear in my exception flow plan but I did have a solution for that in (12) which was to check EDX to see if it was null.

If null continue normal execution, if not null then return to (1) and restart the process.

The only time EDX would be null is if it had been caught by a catch handler and been set to null during the CIL.leave instruction.

So both finally handlers and catch handlers would both receive the excepti9n object address in EDX.

As you mentioned above, EDX can be substituted for a virtual register once the handler has been entered.

@tgiphil
Copy link
Member

tgiphil commented Aug 11, 2014

I see how that can work. However, I'd still pass in the return address to the finally handler to avoid the comparison with object address in EDX.

I'll update the IR/X86 generation to generate the appropriate enter/exit code for the finally handler, and the leave instruction. Can someone else work on the method table generation and runtime lookup code? Afterwards, we can then tackle ExceptionHandler.

Note: There already is a native GetEBP instruction.

@charsleysa
Copy link
Member Author

@tgiphil I can do it but it won't be possibly until December, depending on how I progress with my industry project.

A lot of the code exists but it's out of date so it needs to be updated and needs to be placed in IR stages wherever possible.

@tgiphil
Copy link
Member

tgiphil commented Aug 31, 2014

Quick update - the "ExceptionHanding" branch in my repo now supports nested try/finally situations. New IR instructions were added to model the try/finally/exception flow.

Next phase is to implement the exception processing.

This incomplete branch will merge into main repo shortly since it's fixes a few finally issues, doesn't break anything and introduces a few new compiler concepts. Also, this is some very preliminary code to support branch targets as operands.

@charsleysa
Copy link
Member Author

@tgiphil looking good!

@tgiphil
Copy link
Member

tgiphil commented Dec 12, 2014

I hit a setback as well. Exceptions within finally or exception blocks cause the stack to become misaligned. I'm working out a solution that does not use the stack to store the return address.

@tgiphil tgiphil self-assigned this Dec 26, 2014
@tgiphil tgiphil modified the milestones: 1.3 - Release, Future Dec 26, 2014
@tgiphil tgiphil changed the title Missing Mosa.Internal.ExceptionEngine Implement Exception Handling Dec 26, 2014
@tgiphil
Copy link
Member

tgiphil commented Dec 26, 2014

Just a quick note: Exception handling is steps discussed above are slightly obsolete. I'll re-write them once the implementation is completed.

@tgiphil tgiphil closed this as completed in f807f73 Jan 4, 2015
charsleysa pushed a commit to charsleysa/MOSA-Project that referenced this issue Jul 31, 2016
tgiphil added a commit that referenced this issue Jul 30, 2017
* VBE world

* HU scancode map

* HU scancode map

* VBE using proper IFramebuffers

* - IDE Updates (work in progress) (#9)
tgiphil pushed a commit that referenced this issue Sep 17, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants