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.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With