Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.NET decompiler distinction between "using" and "try...finally"

Given the following C# code in which the Dispose method is called in two different ways:

class Disposable : IDisposable
{
    public void Dispose()
    {
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var disposable1 = new Disposable())
        {
            Console.WriteLine("using");
        }

        var disposable2 = new Disposable();
        try
        {
            Console.WriteLine("try");
        }
        finally
        {
            if (disposable2 != null)
                ((IDisposable)disposable2).Dispose();
        }
    }
}

Once compiled using release configuration then disassembled with ildasm, the MSIL looks like this:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       57 (0x39)
  .maxstack  1
  .locals init ([0] class ConsoleApplication9.Disposable disposable2,
           [1] class ConsoleApplication9.Disposable disposable1)
  IL_0000:  newobj     instance void ConsoleApplication9.Disposable::.ctor()
  IL_0005:  stloc.1
  .try
  {
    IL_0006:  ldstr      "using"
    IL_000b:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_0010:  leave.s    IL_001c
  }  // end .try
  finally
  {
    IL_0012:  ldloc.1
    IL_0013:  brfalse.s  IL_001b
    IL_0015:  ldloc.1
    IL_0016:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_001b:  endfinally
  }  // end handler
  IL_001c:  newobj     instance void ConsoleApplication9.Disposable::.ctor()
  IL_0021:  stloc.0
  .try
  {
    IL_0022:  ldstr      "try"
    IL_0027:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_002c:  leave.s    IL_0038
  }  // end .try
  finally
  {
    IL_002e:  ldloc.0
    IL_002f:  brfalse.s  IL_0037
    IL_0031:  ldloc.0
    IL_0032:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0037:  endfinally
  }  // end handler
  IL_0038:  ret
} // end of method Program::Main

How does a .NET decompiler such as DotPeek or JustDecompile make the difference between using and try...finally?

like image 516
Laurent De Cant Avatar asked Apr 13 '18 11:04

Laurent De Cant


2 Answers

It doesn't make a difference actually. As Mark says in comments - if you write the same code as compiler would generate for using - decompiler won't be able to make a difference.

However, many decompilers, including DotPeek, can actually use debugging symbols (.pdb) files to locate the actual source code, and then use actual source code, so that no decompilation happens at all. Also, compiling in Debug mode might affect the pattern too (that is - your attempt to mimic using statement might have different resulting IL in debug vs release compilations).

To prevent DotPeek from using your real source code files, go to Tools > Options > Decompiler and uncheck "Use sources from symbol files when available". Then compile your code in Release and observe that DotPeek will decompile both statements as using.

like image 71
Evk Avatar answered Nov 15 '22 02:11

Evk


How does a .NET decompiler such as DotPeek or JustDecompile make the difference between using and try...finally?

Decompilers largely work on pattern matching. Generally, the IL is translated into the simplest equivalent representation possible in the target language (in this case, C#). That code model is then passed through a series of transformations that attempt to match up code sequences against well-known patterns. With a debug build of ILSpy, you can actually view the output at different stages of this pipeline.

A decompiler's pipeline may include transforms like a loop rewriter. A loop rewriter might reconstitute for loops by looking for while loops that are preceded by a variable initializers and which also contain common iteration statements prior to each back edge. When such a loop is detected, it gets rewritten as a more concise for loop. It doesn't know that the original code actually contained a for loop; it's simply trying to find the most concise way to represent the code while maintaining correctness.

In a similar way, a using rewriter would look for try/finally blocks where the finally contains a simple null check and Dispose() call, then rewrite those as using blocks, which are more concise while still being correct according to the language spec. The decompiler doesn't know that the code contained a using block, but since almost no one uses the explicit try/finally form, the results tend to be consistent with the original source.

like image 4
Mike Strobel Avatar answered Nov 15 '22 01:11

Mike Strobel