blitzmax
blitzmax copied to clipboard
Try/Catch leaves the stack (locals) in an invalid state after a Throw under certain conditions.
Im not sure what exactly causes this, but it seems that some local variables that are optimized away does not end up in the exception frame. Unless those locals are used ABOVE the try, like getting its address, they will have an invalid state (the one prior to entering Try) when re-entering the scope after a Throw.
minified sample:
Framework BRL.StandardIO
SuperStrict
Function Bail()
Throw "BAIL"
EndFunction
Function TryFail()
Local x:Int = 10
Try
x :* 2
Bail()
Catch e:Object
Print e.ToString() + " x == " + x + " should be 20"
EndTry
EndFunction
Function TryOk()
Local x:Int = 10
Local p:Byte Ptr = Varptr x ' taking the address solidifies the variable
Try
x :* 2
Bail()
Catch e:Object
Print e.ToString() + " x == " + x + " should be 20"
EndTry
EndFunction
TryFail()
TryOk()
output (win32):
BAIL x == 10 should be 20
BAIL x == 20 should be 20
expected output:
BAIL x == 20 should be 20
BAIL x == 20 should be 20
I can confirm this problem on linux. It seems to be a platform-independent code generation issue. I looked into this a little today. I could not find a way to fix this, yet, but here are some observations:
- when compiling your example without modifications the multiply-assign statement
x :* 2
ofTryFail()
is not even in the generated assembly code (maybe illegitimate dead code elimination?) - when I add a
Print x
afterx :* 2
it shows up in the assembly code and the added Print prints 20 in both cases, so the new value is lost after throwing the exception. -
TryFail()
keeps the localx
variable in the esi register, whereasTryOk()
has it saved on the stack (because you took its address). -
bbExEnter
is called at the beginning of a Try block and saves ebx,esi,edi (which are callee-saved registers by convention) and bbExThrow restores them. So even if it's modified in the Try block, it's restored to its original value when the exception is caught I guess. - Simply changing the behaviour of bbExEnter and bbExThrow (which are defined in brl.blitz) to not back-up/restore these registers didn't work :(
I looked into uses of Try-Catch in the standard modules brl, pub and maxgui. They are very rare and are not affected. Either they don't use the local variables after catching or they contain a single assignment statement that calls a function (so the assignment won't take place when the function throws). Non-primitive types are also less likely to be affected unless they are assigned in the try block and an exception is thrown afterwards.
Did somebody else look into this?
Edit: bbExThrow restores the callee-saved registers, not bbExLeave
Yeah, this seems to require compiler intervention.
I see 3 ways to mitigate this..
- The exception frame needs more information about what registers NOT to restore.
- Code inside Try blocks can not use callee-saved registers.
- Stack allocating all variables used inside Try blocks.
nr.3 is the easiest fix.
Btw, i tested the same sample in C++ and it behaves as we expect, it does not touch any variables after a throw.