MarKol4
MarKol4

Reputation: 341

Visual Studio debugger does not step into a "call" instruction

I have been researching some details of the assembly code which is executed for some typical C# constructs. When debugging simple C# code I follow its execution in the Visual Studio Disassembly window (it is a release build of the app with full debug info).

We have a following fragment of code:

return Interlocked.Increment(ref _boxedInt);

00007FFD6CFB5522  sub         esp,30h  
00007FFD6CFB5525  lea         rbp,[rsp+30h]  
00007FFD6CFB552A  mov         qword ptr [rbp+10h],rcx  
00007FFD6CFB552E  cmp         dword ptr [7FFD6C166370h],0  
00007FFD6CFB5535  je          00007FFD6CFB553C  
00007FFD6CFB5537  call        00007FFDCBCFFCC0  
00007FFD6CFB553C  mov         rcx,qword ptr [rbp+10h]  
00007FFD6CFB5540  cmp         dword ptr [rcx],ecx  
00007FFD6CFB5542  mov         rcx,qword ptr [rbp+10h]  
00007FFD6CFB5546  add         rcx,8  
00007FFD6CFB554A  call        00007FFDCA6624B0  
00007FFD6CFB554F  mov         dword ptr [rbp-4],eax  
00007FFD6CFB5552  mov         eax,dword ptr [rbp-4]  
00007FFD6CFB5555  lea         rsp,[rbp]  
00007FFD6CFB5559  pop         rbp  
00007FFD6CFB555A  ret  

There is a problem with a call instruction at 00007FFD6CFB554A address (which is in fact a call to Interlocked.Increment) because Visual Studio Debugger simply steps over the call and does not follow the execution into the subroutine.

It was my intention to have a look what code is executed when Interlocked.Increment is executed.

  1. Why debugger does not follow the execution into a called subroutine?

  2. How to force it to step into that call (there is already mixed debugging enabled for the C# projects)?

Upvotes: 2

Views: 1181

Answers (1)

MarKol4
MarKol4

Reputation: 341

Thanks Hans it worked... somehow ;)

Without JIT optimization it looks like:

00007FFDCA6624B0  nop         dword ptr [rax+rax]  
00007FFDCA6624B5  mov         eax,1  
00007FFDCA6624BA  lock xadd   dword ptr [rcx],eax  
00007FFDCA6624BE  inc         eax  
00007FFDCA6624C0  ret  

With JIT optimization everything is much more complicated. It is just a piece of code which structure is hard to embrace but it is there:

00007FFD6CD7219F  lea         rax,[rsi+8]  
00007FFD6CD721A3  mov         edx,1  
00007FFD6CD721A8  lock xadd   dword ptr [rax],edx  
00007FFD6CD721AC  lea         eax,[rdx+1]  

It looks that it returns incremented value in eax.

Although I've managed to achieve my goal I have had some difficulties.

  1. When turned off "Suppress JIT optimization on module load" and placed a breakpoint in the code I could not track execution in Assembly window. Process was terminated (access violation) when stepped into first call instruction. I had to take a different approach and switch to Debugger.Break() call just before Interlocked.Increment in C# code and attach debugger forcing it to handle my .NET process as a native process:

    • start my app without debug
    • attach debugger as if my app was native
    • trigger Interlocked.Increment execution (with debugger break just before it).

And I was able to track what I was looking for. But why did it all crash if my app was started directly with debugging in VS? I suppose that debugger was not attached to the app as if it was native. But why would it matter if all we care about is a stream of instructions in Assembly window?

  1. Considering we keep "Suppress JIT optimization on module load" enabled why does debugger not step into the call and reveal the code inside Interlocked.Increment routine? Once again - these are just CPU instructions. There are no managed and native instructions, right?

  2. It was mentioned in the comment that Interlocked.Increment is unmanaged code. In which way it is unmanaged since all boils down to a handful CPU instructions? What makes it unmanaged and why? It is not a system call or anything that depends on unmanaged resources. Everything it refers to and uses is in fact managed. Then why?

Upvotes: 1

Related Questions