monoilmono.ceciltamperingilspy

When tampering an assembly, why can't I remove original instructions?


In order to be able to test legacy code which relies on SharePoint, I need to mock some of the objects of SharePoint. I do this by tampering with SharePoint assemblies, replacing their methods by mine on the fly.

This works for some cases, but not for others. A strange situation I encountered is this one.

I want to replace the getter of SPContext.Current by my own implementation; for the sake of simplicity, my implementation just throws an exception:

.property class Microsoft.SharePoint.SPContext Current()
{
  .get class Microsoft.SharePoint.SPContext Proxy.SPContextProxy::get_Current()
}

.method public hidebysig specialname static 
  class Microsoft.SharePoint.SPContext get_Current () cil managed 
{
  // Method begins at RVA 0x877e68
  // Code size 12 (0xc)
  .maxstack 8

  IL_0000: nop
  IL_0001: ldstr "Proxy don't have an effective implementation of this property."
  IL_0006: newobj instance void [mscorlib]System.NotImplementedException::.ctor(string)
  IL_000b: throw
} // end of method SPContextProxy::get_Current

When tampering the original assembly, if I replace the IL code corresponding to SPContext.Current getter, the property cannot be used any longer. I can't even visualize its contents in ILSpy, because this is what is shown instead:

System.NullReferenceException: Object reference not set to an instance of an object.
   at Mono.Cecil.Cil.CodeReader.ReadExceptionHandlers(Int32 count, Func`1 read_entry, Func`1 read_length)
   at Mono.Cecil.Cil.CodeReader.ReadSection()
   at Mono.Cecil.Cil.CodeReader.ReadFatMethod()
   at Mono.Cecil.Cil.CodeReader.ReadMethodBody()
   at Mono.Cecil.Cil.CodeReader.ReadMethodBody(MethodDefinition method)
   at Mono.Cecil.MethodDefinition.<get_Body>b__2(MethodDefinition method, MetadataReader reader)
   at Mono.Cecil.ModuleDefinition.Read[TItem,TRet](TRet& variable, TItem item, Func`3 read)
   at Mono.Cecil.MethodDefinition.get_Body()
   at ICSharpCode.Decompiler.Disassembler.ReflectionDisassembler.DisassembleMethodInternal(MethodDefinition method)
   at ICSharpCode.ILSpy.ILLanguage.DecompileProperty(PropertyDefinition property, ITextOutput output, DecompilationOptions options)
   at ICSharpCode.ILSpy.TextView.DecompilerTextView.DecompileNodes(DecompilationContext context, ITextOutput textOutput)
   at ICSharpCode.ILSpy.TextView.DecompilerTextView.<>c__DisplayClass16.<DecompileAsync>b__15()

On the other hand, when I insert my instructions before the original instructions, I can call the getter successfully, as well as see its contents in ILSpy:

.property class Microsoft.SharePoint.SPContext Current()
{
  .custom instance void [Microsoft.SharePoint.Client.ServerRuntime]Microsoft.SharePoint.Client.ClientCallableAttribute::.ctor() = (
    01 00 00 00
  )
  .get class Microsoft.SharePoint.SPContext Microsoft.SharePoint.SPContext::get_Current()
}

.method public hidebysig specialname static 
  class Microsoft.SharePoint.SPContext get_Current () cil managed 
{
  // Method begins at RVA 0x33e2d8
  // Code size 61 (0x3d)
  .maxstack 1
  .locals init (
    [0] class Microsoft.SharePoint.SPContext,
    [1] class [System.Web]System.Web.HttpContext,
    [2] class Microsoft.SharePoint.SPContext
  )

... follows by the instructions that I inserted:

  IL_0000: nop
  IL_0001: ldstr "Proxy doesn't implement this property yet."
  IL_0006: newobj instance void [mscorlib]System.NotImplementedException::.ctor(string)
  IL_000b: throw

... follows by the original instructions:

  IL_000c: ldnull
  IL_000d: stloc.0
  IL_000e: call class [System.Web]System.Web.HttpContext [System.Web]System.Web.HttpContext::get_Current()
  IL_0013: stloc.1
  IL_0014: ldloc.1
  IL_0015: brfalse.s IL_0039
  .try
  {
    IL_0017: ldloc.1
    IL_0018: call class Microsoft.SharePoint.SPWeb Microsoft.SharePoint.WebControls.SPControl::GetContextWeb(class [System.Web]System.Web.HttpContext)
    IL_001d: brtrue.s IL_0023

    IL_001f: ldnull
    IL_0020: stloc.2
    IL_0021: leave.s IL_003b

    IL_0023: leave.s IL_002a
  } // end .try
  catch [mscorlib]System.InvalidOperationException
  {
    IL_0025: pop
    IL_0026: ldnull
    IL_0027: stloc.2
    IL_0028: leave.s IL_003b
  } // end handler
  .try
  {
    IL_002a: ldloc.1
    IL_002b: call class Microsoft.SharePoint.SPContext Microsoft.SharePoint.SPContext::GetContext(class [System.Web]System.Web.HttpContext)
    IL_0030: stloc.0
    IL_0031: leave.s IL_0039
  } // end .try
  catch [mscorlib]System.IO.FileNotFoundException
  {
    IL_0033: pop
    IL_0034: leave.s IL_0039
  } // end handler
  catch [mscorlib]System.InvalidOperationException
  {
    IL_0036: pop
    IL_0037: leave.s IL_0039
  } // end handler

  IL_0039: ldloc.0
  IL_003a: ret

  IL_003b: ldloc.2
  IL_003c: ret
} // end of method SPContext::get_Current

What prevents the code from being loaded by ILSpy when original instructions are removed before new ones are inserted?

Notes:


Solution

  • When IL is shown in textual form, exception handling blocks (.try, catch, etc.) appear as actual blocks of IL instructions, just like they do in C#.

    But in the binary form, exception handling blocks are stored separately (see §II.25.4.6 Exception handling clauses of ECMA-335) and reference the IL instructions using offsets. In Cecil, exception handlers are represented using the MethodBody.ExceptionHandlers property.

    So, if you replaced the old MethodBody.Instructions with your own instructions, it's very likely that the offsets of the old exception handlers are now invalid, which is causing the issues. (The fact that Cecil throws NullReferenceException sounds like a bug to me, consider reporting it.)

    The other example that you linked to which doesn't exhibit this problem is different because there the original method doesn't contain exception handlers, it throws an exception. And throw is just a normal IL instruction, it doesn't have a special representation like e.g. .try/catch does.