blitzmax icon indicating copy to clipboard operation
blitzmax copied to clipboard

Try/Catch leaves the stack (locals) in an invalid state after a Throw under certain conditions.

Open grable0 opened this issue 7 years ago • 2 comments

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

grable0 avatar Aug 08 '17 15:08 grable0

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:

  1. when compiling your example without modifications the multiply-assign statement x :* 2 of TryFail() is not even in the generated assembly code (maybe illegitimate dead code elimination?)
  2. when I add a Print x after x :* 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.
  3. TryFail() keeps the local x variable in the esi register, whereas TryOk() has it saved on the stack (because you took its address).
  4. 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.
  5. 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

chtisgit avatar Mar 26 '18 13:03 chtisgit

Yeah, this seems to require compiler intervention.

I see 3 ways to mitigate this..

  1. The exception frame needs more information about what registers NOT to restore.
  2. Code inside Try blocks can not use callee-saved registers.
  3. 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.

grable0 avatar Mar 29 '18 17:03 grable0