Tuesday, December 6, 2011

Debugging event leaks

Events can be lame. Wonderful, but lame. One thing that bugs me about the subscribe model is that in most of our code we really are just thinking in terms of one subscriber. But the event as a tool encourages thinking only of many subscribers. Into this hole we fall...

The bug is that you have one subscriber to an event, who goes away and comes back later, possibly as a different instance of the same object. Of course he's forgotten to unsubscribe from the event. Meanwhile, the event source is some kind of singleton in your system (in our case it's an expensive-to-create object, lazily initialized and then kept around).

Later, after crash reports come in, you find dozens of zombie consumer objects held in the crypt of the event handler. This wouldn't be so bad, but the zombies clutch in their hands very valuable resources that consume a lot of memory elsewhere.




(output from !gcroot)

...

01acbce4(HistoScanning.Visualization.VizController.VizController)->
14e13e0c(System.EventHandler`1[[HistoScanning.VizInterfaces.PushToolEventArgs, HistoScanning.VizInterfaces]])->
3b110b38(System.Object[])



The object array at 0x3b110b38 is the "crypt," let's see who is in there:


0:000> !dumparray 3b110b38
Name: System.Object[]
MethodTable: 793042f4
EEClass: 790eda64
Size: 144(0x90) bytes
Array: Rank 1, Number of elements 32, Type CLASS
Element Methodtable: 79330740
[0] 01acdb9c
[1] 0e153f30
[2] 02024884
[3] 0203ddc0
[4] 02056444
[5] 31b35dac
[6] 0e51b69c
[7] 323258cc
[8] 3233c5bc
[9] 32644078
[10] 32657a84
[11] 32794b64
[12] 31f88780
[13] 3aa3491c
[14] 3aa7c034
[15] 3adda58c
[16] 3b110b18
[17] 3b127c80
[18] 3b42a008
[19] 149f03fc
[20] 14b0f778
[21] 14e13dec
[22] null
[23] null
[24] null
[25] null
[26] null
[27] null
[28] null
[29] null
[30] null
[31] null


Taking one at random:


0:000> !do 0203ddc0
Name: System.EventHandler`1[[HistoScanning.VizInterfaces.PushToolEventArgs, HistoScanning.VizInterfaces]]
MethodTable: 08cadb2c
EEClass: 790c3518
Size: 32(0x20) bytes
 (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79330740  40000ff        4        System.Object  0 instance 0203dcdc _target
7932ff98  4000100        8 ...ection.MethodBase  0 instance 00000000 _methodBase
793333ec  4000101        c        System.IntPtr  1 instance  33801b8 _methodPtr
793333ec  4000102       10        System.IntPtr  1 instance        0 _methodPtrAux
79330740  400010c       14        System.Object  0 instance 00000000 _invocationList
793333ec  400010d       18        System.IntPtr  1 instance        0 _invocationCount
0:000> !do 0203dcdc
Name: HistoScanning.Prost8.ViewModels.PushToolsViewModel
MethodTable: 251f55b0
EEClass: 2306cf38
Size: 68(0x44) bytes
 (C:\Program Files\AMD\HistoScanning\Prost8.ViewModels.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
7932ca34  400007e        8          System.Guid  1 instance 0203dce4 uniqueId
793045f0  400007f        4       System.Boolean  1 instance        1 disposed__BackingField
...
Ah. A disposed PushToolsViewModel, the very definition of zombie, along with 20 friends. Back to the original event handler:
0:000> !do 14e13e0c
Name: System.EventHandler`1[[HistoScanning.VizInterfaces.PushToolEventArgs, HistoScanning.VizInterfaces]]
MethodTable: 08cadb2c
EEClass: 790c3518
Size: 32(0x20) bytes
 (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79330740  40000ff        4        System.Object  0 instance 14e13e0c _target
7932ff98  4000100        8 ...ection.MethodBase  0 instance 00000000 _methodBase
793333ec  4000101        c        System.IntPtr  1 instance   95b1e8 _methodPtr
793333ec  4000102       10        System.IntPtr  1 instance 790fe7b0 _methodPtrAux
79330740  400010c       14        System.Object  0 instance 3b110b38 _invocationList
793333ec  400010d       18        System.IntPtr  1 instance       16 _invocationCount
Examine the _invocationList fields of your events to find these characters. Either make sure consumers unsubscribe, or figure out a time when you can blank out the consumers en masse with a statement like
    ...
    ToolChanged = delegate { };
    ...
It's good to do that anyway on dispose of the source object, but in my case we can't dispose of it because of cost.

A few nice links on the topic are here, which is a nice rundown of strategies to deal with the drawbacks of events, and a class from Jeff Richter here.

Tuesday, October 18, 2011

Moving Target

Earlier this year my boss said I'd get to write the code that moves a motor in a breast ultrasound probe. "Oh boy!" I thought. Finally, after years of pushing pixels around on a screen, I'd reached the big time! A tiny servo motor would spin, entirely due to my will! Exaultant, I gathered the CD-ROMs, cables, demo boards and two dozen marketing PDFs from the motor company.

I'd entered a new world. Setting off purposefully, I immediately fell hard asleep on my desk over the "data sheet." Companies that make motors divide them into subcategories and keep their software very general. They had an application that lets you move the motor, tune it and read various properties from the controller board. Setting up the software requires you to enter a host of cryptic values that are extremely scary. And boring. And you are never sure if you got the right answer.

For example we had Maxxon Motor SRV-432343 with 2 channel control and demo board EPOS2. But the software didn't know about this motor. Someone named Chris might come along and reassure me that SRV-43234-R* was the right selection to make. And there wasn't a place to specify 2 channel control. Instead, the channel choices were "3" and "1+polarity". I entered "1+polarity." I was getting the hang of this: when it doubt improvise.

But it's scary too, because being unfamiliar with hardware control I'm imaging all sorts of storylines ending with a stripped motor, a big phone bill and copious sobbing. (to be continued...:D)

Thursday, July 8, 2010

When SOS commands don't work

Woe to the man or woman who wants to debug a managed code dump file, but who doesn't have the same version of the CLR on their machine as the machine that the dump file came from. Specifically, the version of mscorwks.dll needs to match. If you try to run an SOS command the debugger will complain like this:


0:000> .loadby sos mscorwks
0:000> !threads
Failed to load data access DLL, 0x80004005
Verify that 1) you have a recent build of the debugger (6.2.14 or newer)
2) the file mscordacwks.dll that matches your version of mscorwks.dll is
in the version directory
3) or, if you are debugging a dump file, verify that the file
mscordacwks___.dll is on your symbol path.
4) you are debugging on the same architecture as the dump file.
For example, an IA64 dump file must be debugged on an IA64
machine.
You can also run the debugger command .cordll to control the debugger's
load of mscordacwks.dll. .cordll -ve -u -l will do a verbose reload.
If that succeeds, the SOS command should work on retry.
If you are debugging a minidump, you need to make sure that your executable
path is pointing to mscorwks.dll as well.


How to deal with this? First, you need to get the mscordacwks.dll file from the machine that created the dump. For the HistoScanning.exe DumpService? component, we copy this file in a subdirectory next to the dump file every time we create a dump. If you can't get it from the machine, you have to download the exact version of the .NET Runtime from Microsoft that was running on the machine where the dump was created. One way to find the version is to look in the dump file like this:


0:000> lm v mmscorwks
start end module name
79e70000 7a400000 mscorwks (deferred)
Image path: C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll
Image name: mscorwks.dll
Timestamp: Fri Jul 25 15:58:48 2008 (4889DC18)
CheckSum: 00597CC4
ImageSize: 00590000
File version: 2.0.50727.3053
Product version: 2.0.50727.3053
File flags: 0 (Mask 3F)
File OS: 4 Unknown Win32
File type: 2.0 Dll
File date: 00000000.00000000
Translations: 0409.04b0
CompanyName: Microsoft Corporation
ProductName: Microsoft« .NET Framework
InternalName: mscorwks.dll
OriginalFilename: mscorwks.dll
ProductVersion: 2.0.50727.3053
FileVersion: 2.0.50727.3053 (netfxsp.050727-3000)
FileDescription: Microsoft .NET Runtime Common Language Runtime - WorkStation
LegalCopyright: ⌐ Microsoft Corporation. All rights reserved.
Comments: Flavor=Retail


In this case, the version was 2.0.50727.3053.

Once you have the right mscordacwks.dll, put it in a subdirectory, and add that directory to your system path. Restart the debugger and type:


0:000> .cordll -ve


This will cause the debugger to spit out an interesting message. Now type


0:000> .loadby sos mscorwks
0:000> !threads


The !threads command will not succeed, but you'll get a valuable piece of information in the output.


0:000> !threads
CLRDLL: C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscordacwks.dll:2.0.50727.4036 f:0
doesn't match desired version 2.0.50727.3053 f:0
CLRDLL: Unable to find mscordacwks_x86_x86_2.0.50727.3053.dll by mscorwks search
CLRDLL: Unable to find 'mscordacwks_x86_x86_2.0.50727.3053.dll' on the path
CLRDLL: Unable to find mscorwks.dll by search
CLRDLL: ERROR: Unable to load DLL mscordacwks_x86_x86_2.0.50727.3053.dll, Win32
error 0n2
Failed to load data access DLL, 0x80004005
Verify that 1) you have a recent build of the debugger (6.2.14 or newer)
2) the file mscordacwks.dll that matches your version of mscorwks.dll is
in the version directory
3) or, if you are debugging a dump file, verify that the file
mscordacwks___.dll is on your symbol path.
4) you are debugging on the same architecture as the dump file.
For example, an IA64 dump file must be debugged on an IA64
machine.

You can also run the debugger command .cordll to control the debugger's
load of mscordacwks.dll. .cordll -ve -u -l will do a verbose reload.
If that succeeds, the SOS command should work on retry.

If you are debugging a minidump, you need to make sure that your executable
path is pointing to mscorwks.dll as well.


Exit the debugger and rename your mscordacwks.dll to mscordacwks_x86_x86_2.0.50727.3053.dll. Now, restart and everything should work:


0:000> .symfix
0:000> !reload
................................................................
................................................................
................................................................
0:000> .loadby sos mscorwks
0:000> !threads
ThreadCount: 24
UnstartedThread: 2
BackgroundThread: 13
PendingThread: 0
DeadThread: 3
Hosted Runtime: no
...


So in summary:


  • 1. Get the right mscordacwks.dll file

  • 2. Put it in a directory on your system path

  • 3. Discover the proper name of the file

  • 4. Rename the file and restart...now you can debug.



These instructions should always work. You'll see other notes on the web that instruct you to use .cordll /lp instead of the option I have here (putting mscordacwks.dll on your system path), but it seems that some versions of the debugger ignore that directive. Basically, the .cordll command is pretty squirrely, both in documentation and function.

Monday, April 26, 2010

This blog has moved


This blog is now located at http://deathstarplayset.blogspot.com/.
You will be automatically redirected in 30 seconds, or you may click here.

For feed subscribers, please update your feed subscriptions to
http://deathstarplayset.blogspot.com/feeds/posts/default.

Thursday, November 12, 2009

Fixed used with anonymous delegates is tricky

I'm back (5 minutes later) to continue trying to understand a mystery with fixed. At work our problem didn't just involve fixed(), but also the usage of fixed pointers inside an anonymous delegate defined and called within the scope of the fixed block.

Andreas came up with a nice test that surprised us both when we saw the disassembly. Here is the code:


private delegate void Action();
private unsafe void FixedIsColdComfortToDelegates(int runNumber)
{
byte[] array = new byte[1];
fixed(byte *p = array)
{
Action action = () => { *p = 22; };
Thread.Sleep(10);
action();
}
if(22 != array[0])
{
Console.WriteLine("Experienced failure, run {0}!", runNumber);
}
}


The disassembly was interesting, basically the fixed() statement was ignored! The question is why. Ultimately I found out the bug:


You can cause a fixed statement to be ignored by the JIT compiler if you simply include an anonymous delegate in the fixed block which references the fixed pointer. There is no need to run the delegate. Any additional usages of the fixed pointer in the fixed block will not prevent the fixed block from being ignored.


Here is my test program:


using System;
using System.Threading;

namespace FixedTest
{
class FixedProblem
{
private unsafe void Changer(byte *p)
{
// Do some allocations
byte[] bigarray = new byte[1024*50];
bigarray[3] = 34;
GC.Collect();
*p = bigarray[3];
}

private unsafe void NormalFixedStatement(int runNumber)
{
byte[] array = new byte[100];
fixed(byte *p = array)
{
Changer(p + 1); // will change array[1] to 34
Thread.Sleep(10);
*p = 10;
}

if(array[1] != 34)
{
Console.WriteLine("Experienced failure, run {0}", runNumber);
}
}

private unsafe void NullifiedFixedStatement(int runNumber)
{
byte[] array = new byte[100];
fixed (byte* p = array)
{
Action action = () => { *p = 10; }; // This destroys the fixed statement
Changer(p + 1); // will change array[1] to 34
Thread.Sleep(10);
*p = 10; // but p isn't fixed!
}

if (array[1] != 34)
{
Console.WriteLine("Experienced failure, run {0}", runNumber);
}
}

static void Main(string[] args)
{
FixedProblem p = new FixedProblem();

int count = 500;

Console.WriteLine("First run a function with a fixed statement and no anonymous delegate that uses the fixed pointer");
for (int i = 0; i < count; i++)
{
p.NormalFixedStatement(i);
}

Console.WriteLine("Now run a function with a fixed statement and an anonymous delegate that uses the fixed pointer");
for (int i = 0; i < count; i++)
{
p.NullifiedFixedStatement(i);
}
}
}
}


Here is the output:

C:\FixedTest\bin\Release>fixedtest.exe
First run a function with a fixed statement and no anonymous delegate
that uses the fixed pointer.


Now run a function with a fixed statement and an anonymous delegate that
uses the fixed pointer.

Experienced failure, run 4
Experienced failure, run 8
Experienced failure, run 12
Experienced failure, run 16
Experienced failure, run 20
...


I wonder if the CLR team knows about this bug?

Update: My friend Josef points out that if you introduce a temporary variable and pass that in to the anonymous delegate then fixed protection is restored. Something like:


...
byte *pTemp = p;
Action action = () => { *pTemp = 10; }
...

Wednesday, November 11, 2009

Fixed statement not what you expect (or is it?)

Recently we ran into the problem at work that a fixed statement doesn't have the lifetime you expect. Consider:

...
fixed(byte *p = byteArray)
{
    UnmanagedCode(p);
    OtherStuff();
    Console.WriteLine("finished");
}


You might expect that p is held fixed until after the Console.WriteLine statement, but it's only held fixed to the last usage of p, so the fixed protection terminates at the start of the OtherStuff() line. This is something I used to know as a CLR dev, but since forgot. Do you want to see proof? Let's break out SOS.

Here is a little test function:

private unsafe void FixedToLastUsage() {
    DebugBreak();

    int size = 100;
    byte[] byteArray = new byte[size];
    byteArray[0] = 1;

    fixed(byte *p = byteArray) {
        Console.WriteLine("p = " + *p);
        byte* p2 = p + 1;

        // Now we won't reference p anymore but we'll do some other stuff.
        // A look at the gc encoding should prove that p isn't protected beyond this point.
        int sum = 0;
        for(int i=0;i<10;i++) {
            sum += i;
        }
        Console.WriteLine("sum = " + sum.ToString());
    }
}


The DebugBreak() is a pinvoke call to the Windows DebugBreak() function. This is just to make it easier for me to disassemble the function and use SOS in the Windows Debugger.

Here is the disassembly of the method:

0:000> .loadby mscorwks sos
0:000> !clrstack
OS Thread Id: 0xfe8 (0)
ESP       EIP     
0012f410 7c90120e [NDirectMethodFrameStandalone: 0012f410] FixedTest.Program.DebugBreak()
0012f420 00d3014d FixedTest.Program.FixedToLastUsage()
0012f474 00d300b4 FixedTest.Program.Main(System.String[])
0012f69c 79e71b4c [GCFrame: 0012f69c] 
0:000> !u d3014d
Normal JIT generated code
FixedTest.Program.FixedToLastUsage()
Begin 00d30108, size 159
00d30108 55              push    ebp
00d30109 8bec            mov     ebp,esp
00d3010b 83ec4c          sub     esp,4Ch
00d3010e 33c0            xor     eax,eax
00d30110 8945f4          mov     dword ptr [ebp-0Ch],eax
00d30113 8945ec          mov     dword ptr [ebp-14h],eax
00d30116 894dfc          mov     dword ptr [ebp-4],ecx
00d30119 833de430930000  cmp     dword ptr ds:[9330E4h],0
00d30120 7405            je      00d30127
00d30122 e89aa53979      call    mscorwks!JIT_DbgIsJustMyCode (7a0ca6c1)
00d30127 33d2            xor     edx,edx
00d30129 8955d4          mov     dword ptr [ebp-2Ch],edx
00d3012c 33d2            xor     edx,edx
00d3012e 8955e8          mov     dword ptr [ebp-18h],edx
00d30131 33d2            xor     edx,edx
00d30133 8955d8          mov     dword ptr [ebp-28h],edx
00d30136 33d2            xor     edx,edx
00d30138 8955f8          mov     dword ptr [ebp-8],edx
00d3013b c745e400000000  mov     dword ptr [ebp-1Ch],0
00d30142 33d2            xor     edx,edx
00d30144 8955f0          mov     dword ptr [ebp-10h],edx
00d30147 90              nop
00d30148 e8ffbec0ff      call    0093c04c (FixedTest.Program.DebugBreak(), mdToken: 06000001)
>>> 00d3014d 90              nop
00d3014e c745f864000000  mov     dword ptr [ebp-8],64h
00d30155 8b55f8          mov     edx,dword ptr [ebp-8]
00d30158 b902410c79      mov     ecx,offset mscorlib_ni+0x4102 (790c4102)
00d3015d e83620bfff      call    00922198 (JitHelp: CORINFO_HELP_NEWARR_1_VC)
00d30162 8945d0          mov     dword ptr [ebp-30h],eax
00d30165 8b45d0          mov     eax,dword ptr [ebp-30h]
00d30168 8945d8          mov     dword ptr [ebp-28h],eax
00d3016b 8b45d8          mov     eax,dword ptr [ebp-28h]
00d3016e 83780400        cmp     dword ptr [eax+4],0
00d30172 7705            ja      00d30179
00d30174 e883c13979      call    mscorwks!JIT_RngChkFail (7a0cc2fc)
00d30179 c6400801        mov     byte ptr [eax+8],1
00d3017d 8b45d8          mov     eax,dword ptr [ebp-28h]
00d30180 8945d4          mov     dword ptr [ebp-2Ch],eax
00d30183 837dd400        cmp     dword ptr [ebp-2Ch],0
00d30187 7409            je      00d30192
00d30189 8b45d4          mov     eax,dword ptr [ebp-2Ch]
00d3018c 83780400        cmp     dword ptr [eax+4],0
00d30190 7508            jne     00d3019a
00d30192 33d2            xor     edx,edx
00d30194 8955f4          mov     dword ptr [ebp-0Ch],edx
00d30197 90              nop
00d30198 eb14            jmp     00d301ae
00d3019a 8b45d4          mov     eax,dword ptr [ebp-2Ch]
00d3019d 83780400        cmp     dword ptr [eax+4],0
00d301a1 7705            ja      00d301a8
00d301a3 e854c13979      call    mscorwks!JIT_RngChkFail (7a0cc2fc)
00d301a8 8d4008          lea     eax,[eax+8]
00d301ab 8945f4          mov     dword ptr [ebp-0Ch],eax
00d301ae 90              nop
00d301af b920353379      mov     ecx,offset mscorlib_ni+0x273520 (79333520) (MT: System.Byte)
00d301b4 e8631ebfff      call    0092201c (JitHelp: CORINFO_HELP_NEWSFAST)
00d301b9 8945cc          mov     dword ptr [ebp-34h],eax
00d301bc 8b0530202a02    mov     eax,dword ptr ds:[22A2030h] ("p = ")
00d301c2 8945b8          mov     dword ptr [ebp-48h],eax
00d301c5 8b45cc          mov     eax,dword ptr [ebp-34h]
00d301c8 8b55f4          mov     edx,dword ptr [ebp-0Ch]
00d301cb 8955e0          mov     dword ptr [ebp-20h],edx
00d301ce 8b55e0          mov     edx,dword ptr [ebp-20h]
00d301d1 8a12            mov     dl,byte ptr [edx]
00d301d3 885004          mov     byte ptr [eax+4],dl
00d301d6 8b45cc          mov     eax,dword ptr [ebp-34h]
00d301d9 8945b4          mov     dword ptr [ebp-4Ch],eax
00d301dc 8b4db8          mov     ecx,dword ptr [ebp-48h]
00d301df 8b55b4          mov     edx,dword ptr [ebp-4Ch]
00d301e2 e869ca5978      call    mscorlib_ni+0x20cc50 (792ccc50) (System.String.Concat(System.Object, System.Object), mdToken: 060001c5)
00d301e7 8945c8          mov     dword ptr [ebp-38h],eax
00d301ea 8b4dc8          mov     ecx,dword ptr [ebp-38h]
00d301ed e82637a678      call    mscorlib_ni+0x6d3918 (79793918) (System.Console.WriteLine(System.String), mdToken: 060007c8)
00d301f2 90              nop
00d301f3 8b45f4          mov     eax,dword ptr [ebp-0Ch]
00d301f6 8945dc          mov     dword ptr [ebp-24h],eax
00d301f9 8b45dc          mov     eax,dword ptr [ebp-24h]
00d301fc 40              inc     eax
00d301fd 8945f0          mov     dword ptr [ebp-10h],eax
00d30200 33d2            xor     edx,edx
00d30202 8955ec          mov     dword ptr [ebp-14h],edx
00d30205 33d2            xor     edx,edx
00d30207 8955e8          mov     dword ptr [ebp-18h],edx
00d3020a 90              nop
00d3020b eb0b            jmp     00d30218
00d3020d 90              nop
00d3020e 8b45e8          mov     eax,dword ptr [ebp-18h]
00d30211 0145ec          add     dword ptr [ebp-14h],eax
00d30214 90              nop
00d30215 ff45e8          inc     dword ptr [ebp-18h]
00d30218 837de80a        cmp     dword ptr [ebp-18h],0Ah
00d3021c 0f9cc0          setl    al
00d3021f 0fb6c0          movzx   eax,al
00d30222 8945e4          mov     dword ptr [ebp-1Ch],eax
00d30225 837de400        cmp     dword ptr [ebp-1Ch],0
00d30229 75e2            jne     00d3020d
00d3022b 8b0534202a02    mov     eax,dword ptr ds:[22A2034h] ("sum = ")
00d30231 8945c4          mov     dword ptr [ebp-3Ch],eax
00d30234 8d4dec          lea     ecx,[ebp-14h]
00d30237 e8d40a5b78      call    mscorlib_ni+0x220d10 (792e0d10) (System.Int32.ToString(), mdToken: 06000b22)
00d3023c 8945c0          mov     dword ptr [ebp-40h],eax
00d3023f 8b55c0          mov     edx,dword ptr [ebp-40h]
00d30242 8b4dc4          mov     ecx,dword ptr [ebp-3Ch]
00d30245 e806ea5478      call    mscorlib_ni+0x1bec50 (7927ec50) (System.String.Concat(System.String, System.String), mdToken: 060001c9)
00d3024a 8945bc          mov     dword ptr [ebp-44h],eax
00d3024d 8b4dbc          mov     ecx,dword ptr [ebp-44h]
00d30250 e8c336a678      call    mscorlib_ni+0x6d3918 (79793918) (System.Console.WriteLine(System.String), mdToken: 060007c8)
00d30255 90              nop
00d30256 90              nop
00d30257 33d2            xor     edx,edx
00d30259 8955f4          mov     dword ptr [ebp-0Ch],edx
00d3025c 90              nop
00d3025d 8be5            mov     esp,ebp
00d3025f 5d              pop     ebp
00d30260 c3              ret


And here is the gc encoding for the method. What is gc encoding? I think the SOS help explains it best. (Depending on your feelings about SOS help you can thank or curse me...I wrote it over the Christmas holidays in 2002 or 2003, I forgot)

0:000> !help GCInfo
-------------------------------------------------------------------------------
!GCInfo (methoddesc address="" | code address="")

!GCInfo is especially useful for CLR Devs who are trying to determine if there 
is a bug in the JIT Compiler. It parses the GCEncoding for a method, which is a
compressed stream of data indicating when registers or stack locations contain 
managed objects. It is important to keep track of this information, because if 
a garbage collection occurs, the collector needs to know where roots are so it 
can update them with new object pointer values.


You should copy this gc info into a notepad on a second monitor and read it as you read the disassembly. It becomes clear pretty quick that EBP-0CH is our pointer p.

0:000> !GCInfo d3014d
entry point 00d30108
Normal JIT generated code
GC info 00931934
Method info block:
    method      size   = 0159
    prolog      size   = 17 
    epilog      size   =  4 
    epilog     count   =  1 
    epilog      end    = yes  
    callee-saved regs  = EBP 
    ebp frame          = yes  
    fully interruptible= yes  
    double align       = no  
    arguments size     =  0 DWORDs
    stack frame size   = 19 DWORDs
    untracked count    =  2 
    var ptr tab count  = 10 
    epilog        at   0155
    argTabOffset = 22  
82 59 D2 81 D3 | 
B9 93 F1 40 0A | 
22             | 

Pointer table:
04             |             [EBP-04H] an untracked  local
05             |             [EBP-0CH] an untracked pinned byref local
2C 24 82 35    | 0024..0159  [EBP-2CH] a  pointer
28 0A 82 2B    | 002E..0159  [EBP-28H] a  pointer
30 2F 0F       | 005D..006C  [EBP-30H] a  pointer
34 57 26       | 00B4..00DA  [EBP-34H] a  pointer
48 09 1D       | 00BD..00DA  [EBP-48H] a  pointer
4C 17 06       | 00D4..00DA  [EBP-4CH] a  pointer
38 0E 03       | 00E2..00E5  [EBP-38H] a  pointer
3C 4A 11       | 012C..013D  [EBP-3CH] a  pointer
40 0B 06       | 0137..013D  [EBP-40H] a  pointer
44 0E 03       | 0145..0148  [EBP-44H] a  pointer
B8 5A 40       | 005A        reg EAX becoming live
F1 07          | 0071        reg EAX becoming dead
40             | 0071        reg EAX becoming live
F2 01          | 008A        reg EAX becoming dead
F0 43          | 0095        reg EAX becoming live
F0 03          | 00A0        reg EAX becoming dead
40             | 00A0        reg EAX becoming live
03             | 00A3        reg EAX becoming dead
BF 40          | 00A3        reg EAX becoming live (iptr)
03             | 00A6        reg EAX becoming dead
F0 43          | 00B1        reg EAX becoming live
F1 BF 52       | 00C3        reg EDX becoming live (iptr)
16             | 00C9        reg EDX becoming dead
F0 4E          | 00D7        reg ECX becoming live
53             | 00DA        reg EDX becoming live
0D             | 00DF        reg ECX becoming dead
10             | 00DF        reg EDX becoming dead
4E             | 00E5        reg ECX becoming live
05             | 00EA        reg EAX becoming dead
08             | 00EA        reg ECX becoming dead
BF 44          | 00EE        reg EAX becoming live (iptr)
06             | 00F4        reg EAX becoming dead
F5 45          | 0129        reg EAX becoming live
BF 4E          | 012F        reg ECX becoming live (iptr)
0D             | 0134        reg ECX becoming dead
56             | 013A        reg EDX becoming live
4B             | 013D        reg ECX becoming live
0D             | 0142        reg ECX becoming dead
10             | 0142        reg EDX becoming dead
4E             | 0148        reg ECX becoming live
05             | 014D        reg EAX becoming dead
08             | 014D        reg ECX becoming dead
FF             | 

If you look for usages of EBP-0CH in the code, you'll find first the initialization to 0:

00d3010e 33c0            xor     eax,eax
00d30110 8945f4          mov     dword ptr [ebp-0Ch],eax


then it's population with the address of the first element in byteArray (itself stored as an object reference at location EBP-2CH).

00d3019a 8b45d4          mov     eax,dword ptr [ebp-2Ch]
00d3019d 83780400        cmp     dword ptr [eax+4],0
00d301a1 7705            ja      00d301a8
00d301a3 e854c13979      call    mscorwks!JIT_RngChkFail (7a0cc2fc)
00d301a8 8d4008          lea     eax,[eax+8] (here is the fixed(byte *p = byteArray) statement)
00d301ab 8945f4          mov     dword ptr [ebp-0Ch],eax


The last reference is interesting, and seems to disprove my assertion that the pointer is only protected to the last reference. At around the place where the closing curly brace for the fixed statement is, we have:

00d30257 33d2            xor     edx,edx
00d30259 8955f4          mov     dword ptr [ebp-0Ch],edx
00d3025c 90              nop


Wow. I am surprised and shocked! It looks like indeed the pointer is held live until the end of the scope. This raises more questions...

Managed C++ IS good :-)

After my ill-advised rant about managed C++ I learned something interesting. Managed C++ is a great bridge between a C# model and unmanaged C++ code, especially if your method signatures are kind of complex. Managed C++ does a better (quicker) job of marshaling between it's module and the unmanaged C++ module than PINVOKE does.

Also, you don't have to learn those fancy PInvoke attributes and duplicate structures in C# and C++.

Therefore, managed C++ is the most elegant solution to bridge between C# and unmanaged C++.