Why do I get FatalExecutionEngineError when breakpoint editing parameters to System.Numerics functions on x64 builds?

Viewed 296

On AnyCPU or x86, I can run this program, set a breakpoint on Foo, edit the first parameter from 0.5f to 0.6f, and resume just fine. On x64 though, it consistently crashes with FatalExecutionEngineError on resuming.

using System;
using System.Numerics;

public static class Interaction
{
    public static void Main(string[] args)
    {
        while (true)
        {
            Console.WriteLine(Foo.M11);
            System.Threading.Thread.Sleep(0);
        }
    }

    private static Matrix4x4 Foo
    {
        get
        {
            return Matrix4x4.CreateFromYawPitchRoll(0.5f /* breakpoint and set this to 0.6f*/, 0.5f, 0.5f);
        }
    }
}

fatalexecution

At the very least, it's happening on .NET Framework 4.7.2 and .NET Framework 4.6.2. Is there a way I can fix this?

Edit: Tested it on .NET Framework 4.8, after reading that they fixed some x64 stuff in that version, and it's still crashing!

3 Answers

It appears that this is a known issue in both .NET Core and .NET Framework when using edit-and-continue on a method that returns a struct. (see the associated issue on GitHub: EnC: ExecutionEngineException after edit in struct returning method).

The comments on the issue suggest it might get fixed in .NET 6.

Edit:

Having looked further at the issue, it seems to result from a signed/unsigned mismatch in the handling of "variable numbers" (the internal index assigned to each local variable) when applying the edit-and-continue changes to a currently running method.

When applying the edit-and-continue changes the runtime allocates arrays to hold information about the local variables before and after the change, however it has to take account of the fact that there are up to 4 "special" locals which have negative variable numbers (-1 to -4), so it allocates the arrays with 4 extra entries and then adds 4 to each local's variable number to determine the index it should be written to in the corresponding array.

Unfortunately the variable number is stored in an unsigned field, and the conversion to a signed value was missed when determining the array index, so for special locals, instead of being written to the beginning of the array, it calculates a large positive index which attempts to write off the end of the array and causes an access violation.

One of these "special" local variables is the memory used to return a struct from a method, which is why the method in the question (and indeed any method that returns a struct) triggers the issue.

I managed to repro the issue too, couple of interesting observations:

  • issue is stack frame specific - if you try to edit const when the debugger is in the same stack frame, the issue occurs, but you can edit const just fine if the debugger is anywhere else (for example at Console.WriteLine(Foo.M11);)
  • issue is struct specific - I managed to repro the issue with my own structs which are defined in the separate assembly with 'Optimize code' option turned on. No repro with classes.

I suspected the newer Jit and indeed I managed to have edited and continue working with legacy JIT. You can enable it by adding the below section to your app.config:

  <runtime>
    <useLegacyJit enabled="1" />
  </runtime>

Still checking assembly code why edit and continue fails with RuiJIT, however you may use workarounds from @Misha or, as I proposed, use legacy jit for debugging or change const when you are outside of the stack frame.

RuiJIT (repro)

        private static Matrix4x4 Foo => Matrix4x4.CreateFromYawPitchRoll(0.6f /* breakpoint and set this to 0.6f*/, 0.5f, 0.5f);
00007FFCECF609B2  push        rsi  
00007FFCECF609B3  sub         rsp,70h  
00007FFCECF609B7  vzeroupper  
00007FFCECF609BA  mov         rbp,rsp  
00007FFCECF609BD  xor         eax,eax  
00007FFCECF609BF  mov         qword ptr [rbp+68h],rax  
00007FFCECF609C3  mov         qword ptr [rbp+90h],rcx  
00007FFCECF609CA  cmp         dword ptr [7FFCECE545C8h],0  
00007FFCECF609D1  je          00007FFCECF609D8  
00007FFCECF609D3  call        00007FFD4C98CBA0  
00007FFCECF609D8  lea         rcx,[rbp+28h]  
00007FFCECF609DC  vmovss      xmm1,dword ptr [7FFCECF60A48h]  
00007FFCECF609E5  vmovss      xmm2,dword ptr [7FFCECF60A4Ch]  
00007FFCECF609EE  vmovss      xmm3,dword ptr [7FFCECF60A50h]  
00007FFCECF609F7  call        00007FFD53107740  
00007FFCECF609FC  mov         rax,qword ptr [rbp+90h]  
00007FFCECF60A03  vmovdqu     xmm0,xmmword ptr [rbp+28h]  
00007FFCECF60A09  vmovdqu     xmmword ptr [rax],xmm0  
00007FFCECF60A0E  vmovdqu     xmm0,xmmword ptr [rbp+38h]  
00007FFCECF60A14  vmovdqu     xmmword ptr [rax+10h],xmm0  
00007FFCECF60A1A  vmovdqu     xmm0,xmmword ptr [rbp+48h]  
00007FFCECF60A20  vmovdqu     xmmword ptr [rax+20h],xmm0  
00007FFCECF60A26  vmovdqu     xmm0,xmmword ptr [rbp+58h]  
00007FFCECF60A2C  vmovdqu     xmmword ptr [rax+30h],xmm0  
00007FFCECF60A32  mov         rax,qword ptr [rbp+90h]  
00007FFCECF60A39  lea         rsp,[rbp+70h]  
00007FFCECF60A3D  pop         rsi  
00007FFCECF60A3E  pop         rdi  
00007FFCECF60A3F  pop         rbp  
00007FFCECF60A40  ret  

legacy JIT (no repro)

        private static Matrix4x4 Foo => Matrix4x4.CreateFromYawPitchRoll(0.6f /* breakpoint and set this to 0.6f*/, 0.5f, 0.5f);
00007FFCECF609C2  and         al,8  
00007FFCECF609C5  push        rbp  
00007FFCECF609C6  sub         rsp,30h  
00007FFCECF609CA  mov         rbp,rsp  
00007FFCECF609CD  mov         rax,7FFCECE545C8h  
00007FFCECF609D7  mov         eax,dword ptr [rax]  
00007FFCECF609D9  test        eax,eax  
00007FFCECF609DB  je          00007FFCECF609E2  
00007FFCECF609DD  call        00007FFD4C98CBA0  
00007FFCECF609E2  movss       xmm3,dword ptr [7FFCECF60A18h]  
00007FFCECF609EA  movss       xmm2,dword ptr [7FFCECF60A18h]  
00007FFCECF609F2  movss       xmm1,dword ptr [7FFCECF60A20h]  
00007FFCECF609FA  mov         rcx,qword ptr [rbp+40h]  
00007FFCECF609FE  call        00007FFD53107740  
00007FFCECF60A03  mov         qword ptr [rbp+20h],rax  
00007FFCECF60A07  mov         rax,qword ptr [rbp+40h]  
00007FFCECF60A0B  jmp         00007FFCECF60A0D  
00007FFCECF60A0D  nop  
00007FFCECF60A0E  lea         rsp,[rbp+30h]  
00007FFCECF60A12  pop         rbp  
00007FFCECF60A13  ret  

I managed to reproduce the same issue on my computer running on VS 2019 and .NET 5.

It seems to be a Microsoft internal bug that is related to the direct memory access. In x64 it works differently than in x86.

You may tune it with Managed Debugging Assistants.

A possible workaround would be to add a variable and update its value from the watch while debugging:

private static Matrix4x4 Foo
{
    get
    {
        float yaw = 0.5f;
        return Matrix4x4.CreateFromYawPitchRoll(yaw, 0.5f, 0.5f);
    }
}
Related