Jason He's WebLog

  • Coding Patterns to Avoid In AddIn Pipeline Development

    Why we need a new lifetime management model

     

    Managed code relies on Garbage Collection (GC) to manage the lifetime of object. For Add-In, object reference can be cross-appdomain or cross-process. We cannot rely on GC solely to give us automatic lifetime management. We need a new model that works cross any isolation boundary and can keep Add-In alive when it is in use and shut it down when it ends its lifetime. We also want to make sure that the container for the Add-In (either appdomain or process) can be recycled when there is no active Add-In.

     

    We created a lifetime model based on remoting infrastructure. In this model, every object cross the isolation boundary will be the ISponsor of itself. Remoting infrastructure will ask the sponsor whether it should renew the lease and keep it alive. CLR Add-In model has implemented ISponsor logic in a class called ContractBase, which is supposed to be used by all contract based objects that go across the isolation boundary. Add-In and pipeline developers can easily use our ContractBase as the base class for lifetime management.  The purpose of this blog is to help you understand our lifetime management model and avoid a few bad coding patterns in your Add-In development.

     

     

    Lifetime of Add-In and Reference Counting

     

    If you read our blogs or wrote a simple Add-In pipeline before, you would know that an Add-In is kept alive by Add-In Adapter which inherits from ContractBase.  The Add-In lifetime management is essentially ContractBase lifetime management.

     

    Below is how the ContractBase life cycle looks like. Assuming the left side is the LA appdomain and the right side is the RA appdomain (don’t assume LA is the Host and RA is the Add-In. It could be the other way around for data adapters)

     

    2 contract   ---------------------------------------------------------- 1 new ContractBase( );  TotalToken=0

    3 contract.AcquireLifetimeToken ------------------------------- 4 ContractBase; TotalToken =1

    5 Use contract functions  ----------------------------------------- 5 ContractBase renew lease since TotalToken!=0

    6 contract.RevokeLifetimeToken  ------------------------------- 7 ContractBase; TotalToken=0

    9 contract cannot be used anymore -------------------------------8 If TotalToken==0 disconnect or AD.Unload         

     

    First a new ContractBase object will be created in RA and passed to LA as a MarhalByRefObject. Inside LA this object will be treated as a contract (typically it implements both IContract and other functions that are supposed to go across the isolation boundary).

     

    The contract can call AcquireLifetimeToken to notify RA to increase the token count. Once the token has been acquired, the contract interface can be considered as a live object.  

     

    I used 5 twice. It means that operations at both sides of the isolation boundary can execute simultaneously.

     

    When contract user finished all the work it intended to do, it could call contract.RevokeLifetimeToken to notify RA side again. This API will decrease the ref count.

     

    Once the ref count reaches 0 it won’t renew the lease; therefore it is disconnected. If the ContractBase is the only adapter of the appdomain and the appdomain is created by our activation API, we may even unload the appdomain.

     

    If we try to use the contract in LA after it is disconnected, we will see exceptions in LA (either RemotingException or AppdomainUnloadedException).

     

     

    Understand ContractHandle, Remoting and Garbage Collection

     

    From above explanation, we may notice that the lifetime management work is really done by two APIs in IContract, AcquireLifetimeToken and RevokeLifetimeToken. If you compare IContract and IUnknown, you will agree that both interfaces carry some similarities. One advantage of IContract is that it uses a list of tokens to keep track of all the references. Each reference will get a unique token. Therefore it is not likely to release someone else’s token, which is common in COM and it isvery hard to debug.

     

    To improve the user experience dealing with the IContract, we created the ContractHandle class. ContractHandle calls AcquireLifetimeToken in the constructor and RevokeLifetimeToken in the finalizer or its dispose method. This class actually brings us close to the GC world. Since GC can manage the lifetime of ContractHandle, it will call RevokeLifetimeToken if it sees no root for ContractHandle. Acquire-Revoke lifetime token will automatically pair up, and we won’t confuse ourselves with complex if-else blocks.

     

    ContractHandle brings us closer to a GC based world. There are still some significant difference between GC and our lifetime management model. We do have some guidelines in terms of using the ContractBase.

     

    1.        Always hold a contract handle before using the contract functionality.

    2.        Avoid decreasing token count down to zero if contract is still in use.

    3.        Avoid circular reference of IContract.

    4.        Be aware of JIT optimization while using ContractHandle.

     

    Violating above rules will cause serious bugs in your product. Even worse, the bugs are totally not deterministic. If GC does not happen at the right moment, you may not even be able to find it in your dev test environment. But sooner or later, customers will see them.

     

    Fortunately, we have run into problems violating above rules. We learned how to debug them, fix them. We want to blog about it so that you don’t need to make the same mistakes.

     

     

    Demo Scenario

     

    To illustrate the issues that user may see in lifetime management, I created below sample. This sample contains several bugs that I will discuss later. In our pipeline model, Add-In and Add-In base are not really interesting for lifetime management. To simplify the scenario, Add-In and Addin base are intentionally omitted. We will focus on contract, host side adapter and Add-In adapter.

     

    Here is the contract.  IDemoContract contains two methods, Reverse and StoreContract. IDemoDataContract contains getter setter to an integer value.

     

        [AddInContract]

        public interface IDemoContract : IContract

        {

            IDemoDataContract Reverse(IDemoDataContract data);

            void StoreContract(IDemoDataContract data);

        }

        public interface IDemoDataContract : IContract

        {

            int GetValue();       

            void SetValue( int x);       

        }

        public delegate void Func();

     

    Here is the AddInAdapter. It implments the IDemoContract. It also contains DataAdapter which implements IDemoDataContract.   To make our debugging experience better, we changed the LeaseTime in the Add-In appdomain to 5 seconds. The default lease time is 5 minutes that is tool long for debugging.

     

        [AddInAdapter]

        public class TestAddInAdapter : ContractBase, IDemoContract

        {

            ITestAddInBase TestAddInBase;

            ContractHandle storedContractHandle;

     

            internal TestAddInAdapter(ITestAddInBase inTestAddInBase)

            {

                TestAddInBase = inTestAddInBase;

                System.Runtime.Remoting.Lifetime.LifetimeServices.LeaseTime = new TimeSpan(0, 0, 5);

                System.Runtime.Remoting.Lifetime.LifetimeServices.RenewOnCallTime = new TimeSpan(0, 0, 5);

            }

            public IDemoDataContract Reverse(IDemoDataContract data)

            {

                DataAdapter retData = new DataAdapter();

                retData.SetValue(0 - data.GetValue());

                return retData;

            }

            public void StoreContract(IDemoDataContract data)

            {

                storedContractHandle = new ContractHandle(data);

            }       

        }  

     

        public class DataAdapter : ContractBase, IDemoDataContract

        {

            int m_Value = 0;

            public int GetValue() { return m_Value; }

            public void SetValue(int num) { m_Value = num; }      

        }

     

     

    Here is the HostAdapter. It implements the host side view. DataAdapter2 also implements IDemoDataContract. It supports the SetTrigger function. It will write to console if x is set to zero. RunTest() is a simple method defined in HSVBlogSample

     

        [HostAdapter]

        public class TestHostAdapter : HSVBlogSample

        {

            IDemoContract contract;       

            ContractHandle handle;

     

            internal TestHostAdapter(IDemoContract inContract)

            {

                contract = inContract;

                handle = new ContractHandle(inContract);            

            }

     

            public override void RunTest()

            {

                DataAdapter2 data = new DataAdapter2();

                for (int i = 1; i <= 10; i++)

                {

                    data.SetValue(i);

                    data.SetTrigger(delegate { Trigger(); });

                    IDemoDataContract retData = contract.Reverse(data);

                    Console.WriteLine("The value is {0}", retData.GetValue());

                }

                contract.StoreContract(data);

            }

            public void Trigger() { Console.WriteLine("Value Set to Zero.");}

        }   

     

        public class DataAdapter2 : ContractBase, IDemoDataContract

        {

            int m_Value = 0;

            Func trigger;

           

            public int GetValue() { return m_Value; }

            public void SetValue(int num) { m_Value = num; if (m_Value == 0) trigger.Invoke();}

            public void SetTrigger(Func func) { trigger = func; }

        }       

     

     

    Incorrect Coding Pattern -1  (unprotected IContract issue)

     

    The first issue is in the implementation of Reverse method.

     

            public IDemoDataContract Reverse(IDemoDataContract data)

            {

                DataAdapter retData = new DataAdapter();

                retData.SetValue( 0 - data.GetValue() );

                return retData;

            }

     

    The first question is where is data from. It is called from HostAdapter into AddInAdapter, therefore crossing the isolation boundary. It seems that nobody is managing the lifetime of data. It could be disconnected at the other side of the isolation boundary.

     

    Since data is a contract, it is not safe to use it without taking a ContractHandle. There is a easy way to illustrate the issue here if you add Thread.Sleep() for 10 seconds before data.GetValue(). It is very likely to see a RemotingException.

     

    One way to fix the issue is writing code like below

     

                using (ContractHandle handle = new ContractHandle(data))

                {

                    DataAdapter retData = new DataAdapter();

                    retData.SetValue(0 - data.GetValue());

                    return retData;

                }

     

    A little exercise: there us a similar issue in HostAdapter. Can you find it?

     

     

    Incorrect Coding Pattern -2 (multiple reference issue)

     

    Assuming you fixed above issue, the next type of issue will surface. It is a little more complex than the first one. You do need to know some background to understand this. Let’s look at the HostAdapter

         

                DataAdapter2 data = new DataAdapter2();

                for (int i = 1; i <= 10; i++)

                {

                    data.SetValue(i);                                                             

                    data.SetTrigger(delegate { Trigger(); });                       

                    IDemoDataContract retData = contract.Reverse(data);  /// May throw Here

                    Console.WriteLine("The value is {0}", retData.GetValue());

                }

                contract.StoreContract(data);                                              /// May throw Here

     

     

    The problem here is that we are calling contract.Reverse(data) multiple times on the same data. Reverse(data) is executed at the other side of the isolation boundary. During the first cycle (i=1), the other side of the isolation boundary will have one ContractHandle attached with data.  After Reverse(data), that ContractHandle will go out of scope and could be disposed by GC. Therefore the TotalToken becomes zero for data. During the second cycle (i=2), the other side of isolation boundary will call ContractHandle constructor again on the same data, this will try to increase the token count from zero to one. This violates our rule No. 2 above.

     

    Typically you won’t see any exception if GC does not happen. As your code logic becomes complex, this is very likely to give you surprise. If you put GC.Collect() before contract.Reverse, you will see an exception with this message “Once all outstanding references to a Contract have been revoked, new ones may not be acquired.  during the second cycle.

     

    People may ask why don’t you disable this rule and allow token to go down to zero and go back up again. We cannot. We need a way to know when we could do clean up to remove dead appdomains and dispose unmanaged objectes etc. Removing this rule will potentially cause huge memory leak.

     

    There are two solutions for this kind of problem. We can either create unique DataAdpater for each call. Or we can use a ContractHandle to increase the token count before it drops to zero like below.

     

                using (ContractHandle handle = new ContractHandle(data))

                {

                     for (int i = 1; i <= 10; i++)

                    {

                        data.SetValue(i);

                        data.SetTrigger(delegate { Trigger(); });

                        IDemoDataContract retData = contract.Reverse(data);

                       Console.WriteLine("The value is {0}", retData.GetValue());

                   }

                   contract.StoreContract(data);

                }

     

     

    Incorrect Coding Pattern -3 (circular reference issue)

     

    I wrote a host with code like below. It simply activates a pipeline and call RunTest(). Token is a valid AddInToken we get from our discovery API. Remember that our HostAdapter inherits from HSVBlogSample.

     

                while (true)

                {

                    HSVBlogSample hsv = token.Activate<HSVBlogSample>(AddInSecurityLevel.FullTrust);

                    hsv.RunTest();

                    hsv = null;

                    Thread.Sleep(10000); GC.Collect(); GC.WaitForPendingFinalizers();

                    Thread.Sleep(10000); GC.Collect(); GC.WaitForPendingFinalizers();

                }

     

    Most people would not expect a memory leak in managed application like above. The host view is set to null. GC should take care of the rest.

     

    We indeed has a memory leak here. Before we show the details, I want to explain a little bit about why we have multiple sleep and multiple GC.Collect here. Remember we set the lease time to 5 secondes. Sleep for 10 seconds help remoting to disconnect objects. In our lifetime management model, objects are not cleaned in one shot by GC. Depending on how you connect up your objects, we might need multiple GC and remoting-disconnection to clean up the memory.

     

    In above example, no matter how many times we call GC or let the thread to sleep. The memory leak won’t go away. We can let the program run a few cycles and put a breakpoint at the beginning of the loop, then verify with SOS.

     

    First, you can load SOS and dump appdomains.

     

    .load sos

    extension D:\WINDOWS\Microsoft.NET\Framework\v2.0.orcasx86ret\sos.dll loaded

     

    !dumpdomain

     

    --------------------------------------

    Domain 9: 0028eaa0

    LowFrequencyHeap: 0028eac4

    HighFrequencyHeap: 0028eb1c

    StubHeap: 0028eb74

    Stage: OPEN

    SecurityDescriptor: 002f6008

    Name: Test

     

    With above code, we can easily find OPEN appdomains like above besides the default appdomain for the host. Now we are sure that appdomains are not unloaded. Therefore someone must be keeping it alive from the host side. Someone must be holding a ContractHandle to keep the addin alive. The dumpheap command can verify that.

     

     

    !dumpheap -type System.AddIn.Pipeline

     Address       MT     Size

    0280a86c 05491914       20    

    0280cec0 05259f6c       20    

    0281906c 05491914       20    

    0281c3dc 04f59f6c       20    

    total 4 objects

    Statistics:

          MT    Count    TotalSize Class Name

    05259f6c        1           20 System.AddIn.Pipeline.ContractHandle

    04f59f6c        1           20 System.AddIn.Pipeline.ContractHandle

    05491914        2           40 System.AddIn.Pipeline.ContractHandle

    Total 4 objects

     

    From dumpheap output, we do see live ContracHandle objects in the memory. That is really strange. Let’s find out who is keeping them alive.

     

    !gcroot 0280a86c

    Scan Thread 2692 OSTHread a84

    Scan Thread 1904 OSTHread 770

    Scan Thread 2472 OSTHread 9a8

    Scan Thread 2428 OSTHread 97c

    Scan Thread 3524 OSTHread dc4

    Scan Thread 3520 OSTHread dc0

    DOMAIN(0021D738):HANDLE(WeakLn):1da10fc:Root:027bea50(System.Runtime.Remoting.Contexts.Context)->

    027911b4(System.AppDomain)->

    027be914(System.Runtime.Remoting.DomainSpecificRemotingData)->

    0280ace4(System.Runtime.Remoting.Lifetime.LeaseManager)->

    0280ad0c(System.Collections.Hashtable)->

    0280ad44(System.Collections.Hashtable+bucket[])->

    0280b6cc(System.Runtime.Remoting.Lifetime.Lease)->

    0280ab2c(DataAdapter2)->

    0280b0c0(Func)->

    0280a85c(TestHostAdapter)->

    0280a86c(System.AddIn.Pipeline.ContractHandle)

    DOMAIN(0021D738):HANDLE(WeakSh):1da1298:Root:0280a85c(TestHostAdapter)->

    02806818()

    DOMAIN(0021D738):HANDLE(WeakSh):1da129c:Root:0280a85c(TestHostAdapter)->

    02806818()

     

    Examine the gcroot output closely will reveal why all those objects are alive.

    ContractHandle is kept alive by TestHostAdapter

    TestHostAdapter is kept alive by DataAdapter2’s trigger method.

    DataAdapter2 is kept alive because remoting did not disconnect it.

    It seems remoting LeaseManager still consider DataAdapter2 a live objects.

     

    Why remoting did not disconnect DataAdapter2? Let’s dump the object and find it out.

     

    !do 0280ab2c

    Name: DataAdapter2

    MethodTable: 05491c5c

    EEClass: 051744a0

    Size: 44(0x2c) bytes

     (D:\vbl\orcas\clrtest\testbin\AddIn\AddInImpl\RunBlogSampleCode\HostSideAdapters\Test1HostSideAdapters.dll)

    Fields:

          MT    Field   Offset                 Type VT     Attr    Value Name

    790f6f50  400018a        4        System.Object  0 instance 0280b200 __identity

    00000000  4000143        8                       0 instance 0280ab58 m_lifetimeTokens

    00000000  4000144        c                       0 instance 00000000 m_contractIdentifiers

    790f6f50  4000145       10        System.Object  0 instance 0280ab70 m_contractIdentifiersLock

    7910016c  4000146       14        System.Random  0 instance 0280ab7c m_random

    79104974  4000147       1c       System.Boolean  1 instance        0 m_zeroReferencesLeft

    790fb9c0  4000148       18         System.Int32  1 instance        0 m_tokenOfAppdomainOwner

    790fb9c0  4000003       24         System.Int32  1 instance       10 m_Value

    05491d64  4000004       20                 Func  0 instance 0280b0c0 trigger

     

    Let’s examing m_lifetimeTokens which is a List<int> defined in ContractBase. The list is to hold all the acquired tokens.

     

    !do 0280ab58

    Name: System.Collections.Generic.List`1[[System.Int32, mscorlib]]

    MethodTable: 7917bf7c

    EEClass: 791a6c60

    Size: 24(0x18) bytes

     (D:\WINDOWS\Microsoft.NET\Framework\v2.0.orcasx86ret\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)

    Fields:

          MT    Field   Offset                 Type VT     Attr    Value Name

    7910f3b0  40009c7        4       System.Int32[]  0 instance 0280ac88 _items

    790fb9c0  40009c8        c         System.Int32  1 instance        1 _size

    790fb9c0  40009c9       10         System.Int32  1 instance       23 _version

    790f6f50  40009ca        8        System.Object  0 instance 00000000 _syncRoot

    7910f3b0  40009cb        0       System.Int32[]  0   shared   static _emptyArray

     

    It turns out the size of the list is 1, which means that someone is still referencing DataAdapter2. That is why remoting did not disconnect it.

     

    Examing AddInAdapter code closely, we will see that TestHostAdapter keeps AddInAdapter alive.

    AddInAdapter keeps the storedContractHandle alive, which holds the contract of DataAdapter2.

     

     

    We got a big circular reference here. In the usual managed application, circular reference will be deemed as dead object. Unfortunaly, with the help of remoting infrastructure, our circular reference becomes real reference and kept alive by remoting LeaseManager.

     

    To fix the above sample, we need to break the circular reference. One simple way is to define the Trigger() function in the HostAdapter to be static. Another way is to avoid storing ContractHandle in the AddInAdapter.

     

    As you can imagine, even more complex circular reference pattern can be created. Stress testing is definitely helpful to discover such kind of issues.

     

     

    Incorrect Coding Pattern -4 (JIT optimization issue)

     

    When we discuss the first incorrect coding pattern, I left a question. There is one IContract with no protection in the HostAdapter. I don’t know whether you find out the answer or not. Here is the problematic code.

     

    IDemoDataContract retData = contract.Reverse(data);

    Console.WriteLine("The value is {0}", retData.GetValue());

     

    Above code may throw exception while doing Console.WriteLine if retData gets disconnected. Since retData is from the other side of the isolation boundary, we need a ContractHandle to protect it.

     

    Here is the fix.

     

    IDemoDataContract retData = contract.Reverse(data);

    ContractHandle handle2 = new ContractHandle(data);

    Console.WriteLine("The value is {0}", retData.GetValue());

     

    Now handle2 will call AcquireLifetimeToken and we have some protection here to make sure retData won’t be disconnected.

     

    This is the same issue we discussed in coding pattern -1. So what is the incorrect coding pattern -4?

     

    Take a second look at above fix. This actually is not the right way to fix the problem. The fix itself is the incorrect coding pattern that we want to discuss.

     

    Assuming one GC happens right before Console.WriteLine, will the handle2 be collected? I have seen failures with this coding pattern. It seems that JIT optimization is smart enough to know that ContractHandle will not be referenced later, so it could be assumed dead. Then GC calls its finalizer. Therefore Console.WriteLine is not protected and may still throw object disconnected exception.

     

    The recommended coding pattern looks like below.

    using (ContractHandle handle2 = new ContractHandle(retData))

    {

    IDemoDataContract retData = contract.Reverse(data);

    Console.WriteLine("The value is {0}", retData.GetValue());

    }

     

    You can also use a field in the adapter class to hold the ContractHandle if you are sure that you are not going to have circular reference issue.

     

     

    Summary

     

    In this blog, we looked at our Add-In lifetime management model. Our model sits between pure reference counting model and pure GC based model. It does have pros and cons. We also listed a few coding patterns that user may encounter in their development. Keeping the issues in mind will help you write good quality pipeline code.

    All the issues we listed above may not cause problems immediately on a dev machine due to the nature of how GC works.  Stress testing and running tests on machines with different configuration are very helpful to discover issues like these before you deploy your app to customers’ machines.

     

     

     

     

     

  • From C# to CLR Jitted Code - Call Delegate

    How expensive is a delegate call vs a direct function call? The delegate call is actually pretty fast. The delegate typically stores the function and the calling target. When we invoke the delegate, the JIT will retrieve the function pointer and the target and complete the call. You can see from below example.

    Here is the C# code

        public class CallDelegate

        {

            [MethodImpl(MethodImplOptions.NoInlining)]

            public static void Run()

            {

                Foo();          

                RetVoid f1 = new RetVoid(Foo);

                f1();           /// Delegate invoke

            }

     

            [MethodImpl(MethodImplOptions.NoInlining)]

            public static void Foo()

            {

                Console.WriteLine("foo");

            }

    }

     

       L_0000: nop

        L_0001: call void JitBlog.CallDelegate::Foo()

        L_0006: nop

        L_0007: ldnull

        L_0008: ldftn void JitBlog.CallDelegate::Foo()

        L_000e: newobj instance void JitBlog.RetVoid::.ctor(object, native int)       // Build the delegate objiect

        L_0013: stloc.0

        L_0014: ldloc.0

        L_0015: callvirt instance void JitBlog.RetVoid::Invoke()   // Delegate invoke

        L_001a: nop

    L_001b: ret

     

     Here is the disassmbly

     

                call    [CallDelegate.Foo()]

                nop

                mov     ECX, 0x1f336c8                 //Type of delegate

                call    CORINFO_HELP_NEWSFAST_CHKRESTORE

                mov     ESI, EAX

                mov     EAX, 0x1f3c090

                push    EAX

                push    0x4220f8       // Delegate stub

                mov     ECX, ESI

                xor     EDX, EDX

                call    MulticastDelegate.CtorOpened(ref,int,int)

                nop

                mov     EDI, ESI

                mov     ECX, EDI

                mov     EAX, dword ptr [ECX+12]      // Retrieve the function pointer from the Delegate

                mov     ECX, gword ptr [ECX+4]       // Retrieve the target object address from the target

                call    EAX                                          // complete the call

     

  • From C# to CLR Jitted Code - ByVal and ByRef

    I am trying to understand the difference between ByVal and ByRef objects. In below C# code we pass different parameter types to the Test method. Let’s see how the runtime treat them differently at the IL and Assembly level.

     

    using System;

    using System.Runtime.CompilerServices;

    namespace CodeGen

    {

        public class MyClass

        {

            public static void Main()

            {

                MyType o1 = new MyType();

                MyType o2 = new MyType();

                MyType o3;          

                Test(o1, ref o2, out o3);

            }

            [MethodImpl(MethodImplOptions.NoInlining)] // Without this attribute, it will be inlined

            public static void Test(MyType o1, ref MyType o2, out MyType o3)

            {

                o1.X = 1;

                o2.X = 2;

                o3 = new MyType();

                o3.X = 3;

            }

        }

     

        public class MyType

        {

            public int X;

        }

    }

    I highlighted operation that is specific for ByRef object. ByVal object usually can be pushed or popped from stack by one IL opcode. ByRef object is usually accessed indirectly from a managed pointer. That is why we need those stind ldind opcode.

     

    .method public hidebysig static void  Main() cil managed

    {

      .entrypoint

      // Code size       25 (0x19)

      .maxstack  3

      .locals init (class CodeGen.MyType V_0,

               class CodeGen.MyType V_1,

               class CodeGen.MyType V_2)

      IL_0000:  nop

      IL_0001:  newobj     instance void CodeGen.MyType::.ctor()

      IL_0006:  stloc.0

      IL_0007:  newobj     instance void CodeGen.MyType::.ctor()

      IL_000c:  stloc.1

      IL_000d:  ldloc.0

      IL_000e:  ldloca.s   V_1

      IL_0010:  ldloca.s   V_2

      IL_0012:  call       void CodeGen.MyClass::Test(class CodeGen.MyType,

                                                      class CodeGen.MyType&,

                                                      class CodeGen.MyType&)

      IL_0017:  nop

      IL_0018:  ret

    } // end of method MyClass::Main

     

     

    .method public hidebysig static void  Test(class CodeGen.MyType o1,

                                               class CodeGen.MyType& o2,

                                               [out] class CodeGen.MyType& o3) cil managed noinlining

    {

      // Code size       32 (0x20)

      .maxstack  8

      IL_0000:  nop

      IL_0001:  ldarg.0

      IL_0002:  ldc.i4.1

      IL_0003:  stfld      int32 CodeGen.MyType::X

      IL_0008:  ldarg.1

      IL_0009:  ldind.ref

      IL_000a:  ldc.i4.2

      IL_000b:  stfld      int32 CodeGen.MyType::X

      IL_0010:  ldarg.2                                          //It seems that below code can be optimized by reorder the stind.ref to the end of the method.

      IL_0011:  newobj     instance void CodeGen.MyType::.ctor()

      IL_0016:  stind.ref

      IL_0017:  ldarg.2

      IL_0018:  ldind.ref

      IL_0019:  ldc.i4.3

      IL_001a:  stfld      int32 CodeGen.MyType::X

      IL_001f:  ret

    } // end of method MyClass::Test

     

     

    CodeGen.MyClass.Main()

    Begin 02570070, size 4e

    02570070 57              push    edi    // Preserve edi esi, in case it is used in the caller code, since we are going to change both in below code

    02570071 56              push    esi   

    02570072 83ec08          sub     esp,8    // Leave some stack space and use them as local variables

    02570075 33c0            xor     eax,eax

    02570077 890424          mov     dword ptr [esp],eax     // set ref o3 to null

    0257007a 89442404        mov     dword ptr [esp+4],eax   // set ref o2 to null

    0257007e b97c34f301      mov     ecx,1F3347Ch            // !dumptype 1Fee47ch reveals that it is the MethodTable for CodeGen.MyType

    02570083 e89c1f9bff      call    01f22024                // Allocate some space for above type

    02570088 8bf0            mov     esi,eax                 // o1 -> esi       !do eax   can prove that

    0257008a 8bce            mov     ecx,esi                 // ecx represents this pointer

    0257008c e807440f59      call    mscorlib_ni!System.Object..ctor() (5b664498)     // Complete object construction

    02570091 b97c34f301      mov     ecx,1F3347Ch   

    02570096 e8891f9bff      call    01f22024                 

    0257009b 8bf8            mov     edi,eax                 // o2 ->edi

    0257009d 8bcf            mov     ecx,edi

    0257009f e8f4430f59      call    mscorlib_ni!System.Object..ctor() (5b664498)

    025700a4 893c24          mov     dword ptr [esp],edi           

    025700a7 8d442404        lea     eax,[esp+4]             // ref o3 is still null

    025700ab 50              push    eax                     // push ref o3 so that Callee can pick it up

    025700ac 8d542404        lea     edx,[esp+4]             // edx = [edi]     ref o2

    025700b0 8bce            mov     ecx,esi                 // o1 -> ecx

    025700b2 ff15fc32f301    call    dword ptr ds:[1F332FCh] // Stub for Test, since Test is not Jitted yet

    025700b8 83c408          add     esp,8                   // clean up stack and restore esi edi

    025700bb 5e              pop     esi

    025700bc 5f              pop     edi

    025700bd c3              ret

     

     

    CodeGen.MyClass.Test(CodeGen.MyType, CodeGen.MyType ByRef, CodeGen.MyType ByRef)

    025700d2 57              push    edi

    025700d3 56              push    esi

    025700d4 8b7c240c        mov     edi,dword ptr [esp+0Ch] // ref o3 -> edi

    025700d8 c7410401000000  mov     dword ptr [ecx+4],1   // o1 ->ecx     +4 is the offset for the field.  The first 4 bytes represents the method table.

    025700df 8b02            mov     eax,dword ptr [edx]   // o2 ->[edx]   o2->eax  ByRef has to be accessed indirectly here 

    025700e1 c7400402000000  mov     dword ptr [eax+4],2   // set field again 

    025700e8 b97c34f301      mov     ecx,1F3347Ch //CodeGen.MyType)

    025700ed e8321f9bff      call    01f22024     // Allocate memory

    025700f2 8bf0            mov     esi,eax      // o3 -> esi

    025700f4 8bce            mov     ecx,esi

    025700f6 e89d430f59      call    mscorlib_ni!System.Object..ctor() (5b664498)

    025700fb 8d17            lea     edx,[edi]    // This along with the WriteBarrier makes sure that GC can find the root of o3

    025700fd e8eeaa5b5b      call    5db2abf0     // JitHelper for WriteBarrier

    02570102 8b07            mov     eax,dword ptr [edi]

    02570104 c7400403000000  mov     dword ptr [eax+4],3  // set field for o3

    0257010b 5e              pop     esi

    0257010c 5f              pop     edi

    0257010d c20400          ret     4

  • MSIL Verification Notes - 11

    Tired of creating one sample for one unverifiable scenario. Here is a laundry list for the other scenarios I know of. Build the sample in your mind and be happy.

    calli by design unverifiable

    ldftn/ldvirtftn not allowedn on .ctor

     "This" parameter does not match the calling method

    arglist is used in non-vararg method

    Tailcall return type does not match

    Throw "this" in the constructor

    Bad Instance call with mismatch type

    Use of this ptr when it is not fully initialized

    Newobj on statci or abstract method 

    Overlapping try blocks 

    Incorrect Filter offset size

    Use static opcode with instance field

    Bad use of initonly field

    Accessing fields that should be invisible to other types

    Use of unknown type tokens

    Tail call not followed by return

    Treat pointer as byref

    Modify an imaged based <RVA> static belong to the module

    Init Locals are not set

    Bad endfilter argument

    Array operation on non-array object

    Jump to the middle of one Opcode

    Branch out of Try block

    CallVirt on ValueClass

    Conditional Branch type mismatch

    cpobj type of source address incompatible with type operand

    wrong number args to delegate ctor

    endfilter outside of exception filter

    ldvrirftn on static method

    Illegal use of leave to enter exception handler

     

  • MSIL Verification Notes - 10

    Another one that taking ArgIterator is deemed unverifiable. This time is for making a Type Reference.

    .method public hidebysig static vararg void VarArg() cil managed
    {
        .maxstack 8
        .locals init (
            [0] valuetype [mscorlib]System.ArgIterator args,
            [1] valuetype [mscorlib]System.TypedReference tref)
        L_0000: ldloca args
        L_0004: arglist 
        L_0006: call instance void [mscorlib]System.ArgIterator::.ctor(valuetype [mscorlib]System.RuntimeArgumentHandle)
        L_000b: ldloca args
        L_000f: mkrefany [mscorlib]System.ArgIterator
        L_0014: stloc tref
        L_0018: ldloca args
        L_001c: call instance valuetype [mscorlib]System.TypedReference [mscorlib]System.ArgIterator::GetNextArg()
        L_0021: refanyval int64
        L_0026: ldind.i8 
        L_0027: call void [mscorlib]System.Console::WriteLine(int64)
        L_002c: ret 
    }
    
    The PEverify states below
    [IL]: Error: [xxx.exe : TestClass::VarArg][mdToken=0x6000001][offset 0x0000000F] Mkrefany on TypedReference, ArgHandle, or ArgIterator.(Error: 0x801318B7)

  • MSIL Verification Notes - 9

     

    Tail call verification rules require that no managed pointers to be passed from caller to callee if it points to a stack frame that to be removed. ArgIterator does not satisfy the condition. Therefore below code will not be verifiable. 
    .method public hidebysig static void Main(string[] A_0) cil managed
    {
        .entrypoint
        .maxstack 8
        .locals init (
            [0] valuetype [mscorlib]System.ArgIterator args)
        L_0000: ldloc args
        L_0004: tail 
        L_0006: call void TestClass::t(valuetype [mscorlib]System.ArgIterator)
        L_000b: ret 
    }
    
    The PEverify result is
    [IL]: Error: [xxx.exe : TestClass::Main][mdToken=0x6000002][offset 0x00000006][found value 'System.ArgIterator'] Cannot pass ByRef to a tail call.(Error: 0x80131899)
     

     

  • MSIL Verification Notes - 8

     One more verification rule for ArgIterator. We cannot put it in an array. To avoid stack corruption, the JIT will consider below code unverifiable.

    IL_0000:    ldc.i4.1
    IL_0001:    newarr ArgIterator
    IL_0006:    stloc.0
    IL_0007:    ret

     Here is the PEVerify result.

    [IL]: Error: [xxx.exe : TestClass::Main][mdToken=0x6000001][offset 0x00000001] Array of ELEMENT_TYPE_BYREF or ELEMENT_TYPE_TYPEDBYREF.(Error: 0x80131890)

  • MSIL Verification Notes - 7

    Box operation can operate on ValueType but not all of them.

    Below example is trying to box System.ArgIterator. ArgIterator can have pointer points to stack, therefor BOX it won't be verifiable.

    .method public hidebysig static vararg void VarArg() cil managed
    {
        .maxstack 8
        .locals init (
            [0] valuetype [mscorlib]System.ArgIterator args)
        L_0000: ldloca args
        L_0004: arglist 
        L_0006: call instance void [mscorlib]System.ArgIterator::.ctor(valuetype [mscorlib]System.RuntimeArgumentHandle)
        L_000b: ldloc args
        L_000f: box [mscorlib]System.ArgIterator
        L_0014: unbox [mscorlib]System.ArgIterator
        L_0019: call instance valuetype [mscorlib]System.TypedReference [mscorlib]System.ArgIterator::GetNextArg()
        L_001e: refanyval int64
        L_0023: ldind.i8 
        L_0024: call void [mscorlib]System.Console::WriteLine(int64)
        L_0029: ret 
    }
    
    Here is the PEVerify result.
    [IL]: Error: [xxx.exe : TestClass::VarArg][mdToken=0x6000001][offset 0x0000000F][found value 'System.ArgIterator'] Value type, ObjRef type or variable type expected.(Error: 0x801318C0)

     

  • MSIL Verification Notes - 6

    Recently I am running some DynamicMethod scenario.

    It seems very easy to Emit incorrect IL using ILGenerator. If the IL is not correct, JIT will throw unverifiable code exception or invalid program exception.

    I tend to debug with WinDBG+SOS for IL related issues in the past. However, for some reason, !dumpIL does not work as expected for dynamic method.

    The best way to debug seems to be read source code and build the stack in mind. So here are a few mistakes I made. Hope you can avoid them.

    1. Type mismatch on the stack

    2. Stack underflow (caller expects one object on the stack, but callee returns with nothing left on the stack)

    3. Stack overflow (caller expects stack to be empty, but callee leaves one object on the stack before it returns)

    4. Signature not match (DynamicMethod returns a ref object, but delegate expects a valuetype)

     

  • MSIL Verification Notes - 5

    This is a short one.

    The arglist operation can only work when the method's signature indicates that it accepts a variable number of arguments. The verification rule also requires a type check for System.RuntimeArgumentHandle.

    Below scenario is clear a violation of both conditions.

    .method public hidebysig static void Test() cil managed
    {
        .maxstack 8
        L_0000: arglist 
        L_0002: call void [mscorlib]System.Console::WriteLine(int32)
        L_0007: ret 
    }
    
    The peverify result looks like below.
    [IL]: Error: [xxx.exe : arglist_negt::Test][mdToken=0x6000001][offset 0x00000000] Allowed only in vararg methods.(Error: 0x8013187A)

  • MSIL Verification Notes - 4

    I thought I knew what is box and unbox. After reading the ECMA spec, I know that what I believed was actually wrong. Maybe you had the same incorrect impression in your mind.

    Box a integer will create an object on the heap and copy the date from valuetype into the newly allocated object. It then put the object on the stack.

    What will unbox do? I thought it will do the reverse of "boxing"; copying the valuetype back to the stack from the boxed object. According to the ECMA spec, it will actually compues the address of the valuetype and make it a managed Pointer.

    Since the net result of unbox returns a managed pointer not the valuetype ifself. Sometime it is unverifable if you try to return the managed pointer to the caller.

     

     

  • MSIL Verification Notes - 3

    Below code tries to return a ByRef object which points on a stack location.

    CLR rule considers this kind of ByRef object is not safe to return to the caller.

    .method public instance int32& modopt([Microsoft.VisualC]Microsoft.VisualC.IsCXXPointerModifier)
              retLocalByref() cil managed
      {
        // Code size       30 (0x1e)
        .maxstack  2
        .locals init (int32 V_0)
        IL_0000:  ldstr      "Returning local byref"
        IL_0005:  ldloc.0
        IL_0006:  box        [mscorlib]System.Int32
        IL_000b:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                      object)
                                                                     
        ldloca 0
                                                                         
        IL_001d:  ret
      }

    This actually makes sense. When a method call returns the stack will be reused by other methods. If a dangling pointer randomly points to a stack location, it could do possibly anything to break type safety.

    Here is the Peverify result.

     [IL]: Error: [xxx.dll : C::retLocalByref][mdToken=0x6000003][offset 0x00000014] Return type is
    ByRef, TypedReference, ArgHandle, or ArgIterator.(Error: 0x80131870) 

    The equivalent C# code looks like below.

    public unsafe ref int modopt(IsCXXPointerModifier) retLocalByref()
    {
        int V_0;
        Console.WriteLine("Returning local byref", V_0);
        return (ref int modopt(IsCXXPointerModifier)) &V_0;
    }
    

  • MSIL Verification Notes - 2

    MSIL (or CIL) uses a stack based model.  To make sure that Type-safe code to be secure, it can only access the meory locations it is authorized to acess.
    We verify the type on the stack. If the data type on the stack does not match the expection of an opcode, we will treat it as unverifiable code. Take below code as an example


    .method public static void foo() cil managed
    {
        .maxstack 8
        L_0000: ldc.r4 0
        L_0005: ldc.i4 0
        L_000a: add
        L_000b: pop
        L_000c: ret
    }

    The opcode "add" expects the two numbers on the stack of the same type. Since they are not (r4 and i4 are pushed on the stack), at runtime JIT will throw exception, unless SkipVerification is specified.
     

    The PEVerify output looks like this
    [IL]: Error: [xxx.dll : Test::foo][mdToken=0x6000001][offset 0x0000000A][found Double][expected Int32] Unexpected type on the stack.(Error: 0x80131854)

  • MSIL Verification Notes - 1


    I am recently working on some IL verification related scenario. IL verification is critical for CLR security. As CLR support for Silverlight approaching, it is a critical to make sure that our JIT can verify IL code correctly. I figured it would be interesting to share some verification rules we have and how we verify them.

    Here is the IL code

    .method public hidebysig static vararg void VarArg(value class [mscorlib]System.RuntimeArgumentHandle&) il managed
    {
     .maxstack 8
     ldarg 0
     arglist
     stobj value class [mscorlib]System.RuntimeArgumentHandle
     ret
    }

    The problem here is RuntimeArgumengHandle is a value type. The arglist will put an argument list handle (an unmanaged pointer) on the stack. The stobj will store a value type from the stack into memory. When it sees the unmanaged pointer, it raises the red flag.

    [IL]: Error: xxx.dll : <Module>::VarArg][mdToken=0x6000001][offset 0x00000000][found value
    'System.RuntimeArgumentHandle'] ByRef of TypedReference, ArgHandle, or ArgIterat
    or.(Error: 0x801318B2)
    Here is what the code looks like in C#.

    public static void VarArg(ref RuntimeArgumentHandle A_0, __arglist)
    {
        A_0 = __arglist;
    }
    
     

  • CLR AddIn Adapters

     

    I posted a blog about AddIn Adapters here.

    There is a question. Can we have AddInAdapters inside HostAdapter?

    This is a tricky one. It depends on what the definition of AddInAdapter is.

     

    The answer is Yes. If AddInAdapter refers to any type inherited from ContractBase.

    The answer is No. If AddInAdapter refers to any type with AddInAdapterAttribute so that the Host can discover and activate.

     

     

More Posts Next page »

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker