-
Notifications
You must be signed in to change notification settings - Fork 80
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
Comments
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. |
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. |
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. |
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. |
Correction: exception type should have been exception object. |
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? |
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. |
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.) |
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. |
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. |
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. |
@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. |
Let's walk thru a typical exception flow for a throw and rough out a plan:
TODO: (*) Determining the return address. |
I have modified the flow to how I think it should occur:
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/ |
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, Am 10.08.2014 um 19:06 schrieb Stefan Andres Charsley notifications@github.com:
|
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? |
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. |
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). |
The part that we are missing so far in our discussion is determining to set the return address of exception handlers. Ideas? |
Which return address? Why do they need one? |
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. |
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. |
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. |
@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. |
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. |
@tgiphil looking good! |
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. |
Just a quick note: Exception handling is steps discussed above are slightly obsolete. I'll re-write them once the implementation is completed. |
Added Disassembly Stage
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.
The text was updated successfully, but these errors were encountered: