Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Potential .NET x86 JIT issue?

Tags:

c#

jit

c#-4.0

The following code behaves differently when built in Release mode (or Debug with optimizations on) and run without the Visual Studio debugger attached.

It also only seems to replicate if the x86 JITter is used. I have tested this on an x86 machine as well as running in WOW64 on a x64 machine (by setting the Platform target to x86).

I've only tried this with .NET 4.0.

When running outside the debugger in Release I see:

Value is 4 

When running inside the debugger the e.Value.Length portion of the WriteLine call throws NullReferenceException, which is what I expected to have happen.

The code:

namespace Test {     class UsingReleasable<T>     {         public UsingReleasable(T obj)         {             m_obj = obj;         }          public T Release()         {             T tmp = m_obj;             m_obj = default(T);             return tmp;         }          public T Value         {             get { return m_obj; }         }          T m_obj;     }      class Program     {         static void Main(string[] args)         {             var e = new UsingReleasable<string>("test");             e.Release();             System.Console.WriteLine("Value is {0}", e.Value.Length);         }     } } 

My peering at the JIT generated code makes me think it is a bug in that piece, but I wanted to double check here before forwarding this on to MS Connect.

like image 773
FatKenny Avatar asked Jul 11 '11 22:07

FatKenny


1 Answers

I can reproduce your behavior:

R:\>csc /platform:x86 releasable.cs Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1 Copyright (C) Microsoft Corporation. All rights reserved.   R:\>releasable  Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.    at Test.Program.Main(String[] args)  R:\>csc /o+ /platform:x86 releasable.cs Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1 Copyright (C) Microsoft Corporation. All rights reserved.   R:\>releasable Value is 4  R:\>csc /platform:anycpu releasable.cs Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1 Copyright (C) Microsoft Corporation. All rights reserved.   R:\>releasable  Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.    at Test.Program.Main(String[] args)  R:\>csc /o+ /platform:anycpu releasable.cs Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1 Copyright (C) Microsoft Corporation. All rights reserved.   R:\>releasable  Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.    at Test.Program.Main(String[] args) 

The /checked compiler option makes no difference. Neither does generation of debug data /debug+. And the problem persists when Console.ReadLine() is called (to give a chance to attach the debugger and see optimized code.


I made a slight modification to Main to permit debugging of the optimized code:

    static void Main(string[] args)     {         var e = new UsingReleasable<string>("test");         System.Console.WriteLine("attach now");         System.Console.ReadLine();         e.Release();         System.Console.WriteLine("Value is {0}", e.Value.Length);     } 

And the actual disassembly:

--- r:\releasable.cs -----------------------------------------------------------             var e = new UsingReleasable<string>("test"); 00000000  push        ebp  00000001  mov         ebp,esp  00000003  push        edi  00000004  push        esi  00000005  mov         ecx,1839B0h  0000000a  call        FFF81FB0  0000000f  mov         esi,eax  00000011  mov         eax,dword ptr ds:[03772030h]  00000017  lea         edx,[esi+4]  0000001a  call        60452F70              System.Console.WriteLine("attach now"); 0000001f  call        5E927060  00000024  mov         ecx,eax  00000026  mov         edx,dword ptr ds:[03772034h]  0000002c  mov         eax,dword ptr [ecx]  0000002e  mov         eax,dword ptr [eax+3Ch]  00000031  call        dword ptr [eax+10h]  00000034  call        5EEF9A40  00000039  mov         ecx,eax  0000003b  mov         eax,dword ptr [ecx]  0000003d  mov         eax,dword ptr [eax+2Ch]  00000040  call        dword ptr [eax+1Ch]              e.Release(); 00000043  mov         edi,dword ptr [esi+4]              ; edi = e.Value 00000046  lea         esi,[esi+4]                        ; esi = &e.Value 00000049  xor         edx,edx                            ; edx = null 0000004b  mov         dword ptr [esi],edx                ; *esi = edx (e.Value = null) 0000004d  mov         ecx,5EBE28F8h 00000052  call        FFF81FB0 00000057  mov         edx,eax  00000059  mov         eax,dword ptr [edi+4]              ; this sets EAX to 4 0000005c  mov         dword ptr [edx+4],eax  0000005f  mov         esi,edx  00000061  call        5E927060  00000066  push        esi  00000067  mov         ecx,eax  00000069  mov         edx,dword ptr ds:[03772038h]  0000006f  mov         eax,dword ptr [ecx]  00000071  mov         eax,dword ptr [eax+3Ch]  00000074  call        dword ptr [eax+18h]                ; this results in the output "Value is 4\n" 00000077  pop         esi          } 00000078  pop         edi  00000079  pop         ebp  0000007a  ret  

When the program starts under the debugger, this code is generated instead (and does produce a NullReferenceException:

--- r:\releasable.cs -----------------------------------------------------------             var e = new UsingReleasable<string>("test"); 00000000  push        ebp  00000001  mov         ebp,esp  00000003  sub         esp,24h  00000006  mov         dword ptr [ebp-4],ecx  00000009  cmp         dword ptr ds:[001E313Ch],0  00000010  je          00000017  00000012  call        606B6807  00000017  xor         edx,edx  00000019  mov         dword ptr [ebp-0Ch],edx  0000001c  mov         ecx,1E39B0h  00000021  call        FFF91FB0  00000026  mov         dword ptr [ebp-10h],eax  00000029  mov         edx,dword ptr ds:[032E2030h]  0000002f  mov         ecx,dword ptr [ebp-10h]  00000032  call        dword ptr ds:[001E3990h]  00000038  mov         eax,dword ptr [ebp-10h]  0000003b  mov         dword ptr [ebp-0Ch],eax              System.Console.WriteLine("attach now"); 0000003e  mov         ecx,dword ptr ds:[032E2034h]  00000044  call        5E8D703C              System.Console.ReadLine(); 00000049  call        5EEAA728  0000004e  nop              e.Release(); 0000004f  mov         ecx,dword ptr [ebp-0Ch]  00000052  cmp         dword ptr [ecx],ecx  00000054  call        dword ptr ds:[001E3994h]  0000005a  nop              System.Console.WriteLine("Value is {0}", e.Value.Length); 0000005b  mov         eax,dword ptr ds:[032E2038h]  00000061  mov         dword ptr [ebp-14h],eax  00000064  mov         ecx,dword ptr [ebp-0Ch]  00000067  cmp         dword ptr [ecx],ecx  00000069  call        dword ptr ds:[001E3998h]  0000006f  mov         dword ptr [ebp-18h],eax  00000072  mov         ecx,dword ptr [ebp-18h]  00000075  cmp         dword ptr [ecx],ecx                 ; access violation here 00000077  call        608CBA5B  0000007c  mov         dword ptr [ebp-8],eax  0000007f  mov         ecx,5EBE28F8h  00000084  call        FFF91FB0 00000089  mov         dword ptr [ebp-1Ch],eax  0000008c  mov         eax,dword ptr [ebp-14h]  0000008f  mov         dword ptr [ebp-20h],eax  00000092  mov         eax,dword ptr [ebp-1Ch]  00000095  mov         edx,dword ptr [ebp-8]  00000098  mov         dword ptr [eax+4],edx  0000009b  mov         eax,dword ptr [ebp-1Ch]  0000009e  mov         dword ptr [ebp-24h],eax  000000a1  mov         ecx,dword ptr [ebp-20h]  000000a4  mov         edx,dword ptr [ebp-24h]  000000a7  call        5E8CD460          } 000000ac  nop  000000ad  mov         esp,ebp  000000af  pop         ebp  000000b0  ret  

I think I've commented all the relevant lines of code in the incorrect version. Clearly register edi is used to hold a pointer to e.Value, which consists most likely of a pointer to content (at offset 0) and a length (at offset 4) of the v-table (at offset 0), length (at offset 4), followed immediately by the content. Unfortunately e.Value (the string "test") is copied into edi before e.Value is cleared, so the length is fetched using the wrong string pointer. Ouch!


Bug filed on Connect (please upvote!): x86 JIT improperly reorders load of field of generic type with assignment to default(T)

like image 99
Ben Voigt Avatar answered Oct 03 '22 21:10

Ben Voigt