Monday, January 2, 2017

Dissecting C# Executables: Part 1

.Net frameworks available in Visual Studio 2015

The What

Today you can build executables that are written in C# on many different platforms.  You can build them using many different Frameworks. 

The Why

Does it matter if the executables generated by different methods are identical?  Should we care if there are minor differences between libraries created in different frameworks if they do the same work?  Just within vanilla VS 2015, you have the option of 10 different frameworks:

What about applications built using Mono on a Mac or Linux?  What difference does it make if you build your application from the command line?  And that doesn’t get into the whole .NET Core discussion. 

The How

We are going to be creating assemblies and .exe artifacts using the command line C# compiler (CSC); Visual Studio 2015; Visual Studio 2017 RC; Microsoft Code on Windows, Mac, and Linux; Xamarin Studio on Windows and Mac; and MonoDevelop on Windows, Mac, and Linux.  We will then examine them using JustDecompile and a custom program to view the binary itself. 

The First Example

Let’s start out by looking at the Cannonical first program, HelloWorld.exe.  I made a slight addition to the standard source to provide us with the version of the framework that the program is running in.  I added a call to Environment.Version (https://msdn.microsoft.com/en-us/library/system.environment.version(v=vs.110).aspx) because that is how it is done, or at least that is what I though.  Reading the linked page I see that that it how it was done until .Net framework version 4.5 and higher.  That is a discussion for another article, for now, here it the source that we will use:

using System;
 
namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World");
            Console.WriteLine(Environment.Version);
        }
    }
}

I created a VS solution with 11 different projects, each targeting a different framework version.  I also created a file in notepad named Program.cs that contained the above code to compile from the command line using the different frameworks.    When I created the .NET Core application, I had to change the Environment.Version call to the following:

Console.WriteLine("{0}"PlatformServices.Default.Application.RuntimeFramework.FullName);

Because the Environment object no longer contains a Version property. 

I then ran the following batch file to compile and run the sample programs:

C:\Windows\Microsoft.NET\Framework\v2.0.50727\csc.exe /out:HelloWorld_CSC_2.0.exe Program.cs
C:\Windows\Microsoft.NET\Framework\v3.5\csc.exe /out:HelloWorld_CSC_3.5.exe Program.cs
C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /out:HelloWorld_CSC_4.0.exe Program.cs
msbuild ..\HelloWorld.sln /p:Configuration=Release /target:Rebuild
copy ..\HelloWorld_VS_2.0\bin\Release\HelloWorld_VS_2.0.exe .
copy ..\HelloWorld_3.0\bin\Release\HelloWorld_VS_3.0.exe .
copy ..\HelloWorld_3.5\bin\Release\HelloWorld_VS_3.5.exe .
copy ..\HelloWorld_4\bin\Release\HelloWorld_VS_4.exe .
copy ..\HelloWorld_4.5\bin\Release\HelloWorld_VS_4.5.exe .
copy ..\HelloWorld_4.5.1\bin\Release\HelloWorld_VS_4.5.1.exe .
copy ..\HelloWorld_4.5.2\bin\Release\HelloWorld_VS_4.5.2.exe .
copy ..\HelloWorld_4.6\bin\Release\HelloWorld_VS_4.6.exe .
copy ..\HelloWorld_4.6.1\bin\Release\HelloWorld_VS_4.6.1.exe .
copy ..\HelloWorld_VS_4.6.2\bin\Release\HelloWorld_VS_4.6.2.exe .
copy ..\HelloWorld_VS_Core_1.0\bin\Release\netcoreapp1.0\HelloWorld_VS_Core_1.0.dll .
 
 
HelloWorld_CSC_2.0.exe
HelloWorld_CSC_3.5.exe
HelloWorld_CSC_4.0.exe
HelloWorld_VS_2.0.exe
HelloWorld_VS_3.0.exe
HelloWorld_VS_3.5.exe
HelloWorld_VS_4.exe
HelloWorld_VS_4.5.exe
HelloWorld_VS_4.5.1.exe
HelloWorld_VS_4.5.2.exe
HelloWorld_VS_4.6.exe
HelloWorld_VS_4.6.1.exe
HelloWorld_VS_4.6.2.exe
cd ..\HelloWorld_VS_Core_1.0
dotnet run
cd ..\CommandLine


When all of the files are executed, I see another difference between the executables:
D:\Source\HelloWorld\CommandLine>HelloWorld_CSC_2.0.exe
Hello World
2.0.50727.8780

D:\Source\HelloWorld\CommandLine>HelloWorld_CSC_3.5.exe
Hello World
2.0.50727.8780

D:\Source\HelloWorld\CommandLine>HelloWorld_CSC_4.0.exe
Hello World
4.0.30319.42000

D:\Source\HelloWorld\CommandLine>HelloWorld_VS_2.0.exe
Hello World
2.0.50727.8780

D:\Source\HelloWorld\CommandLine>HelloWorld_VS_3.0.exe
Hello World
2.0.50727.8780

D:\Source\HelloWorld\CommandLine>HelloWorld_VS_3.5.exe
Hello World
2.0.50727.8780

D:\Source\HelloWorld\CommandLine>HelloWorld_VS_4.exe
Hello World
4.0.30319.42000

D:\Source\HelloWorld\CommandLine>HelloWorld_VS_4.5.exe
Hello World
4.0.30319.42000

D:\Source\HelloWorld\CommandLine>HelloWorld_VS_4.5.1.exe
Hello World
4.0.30319.42000

D:\Source\HelloWorld\CommandLine>HelloWorld_VS_4.5.2.exe
Hello World
4.0.30319.42000

D:\Source\HelloWorld\CommandLine>HelloWorld_VS_4.6.exe
Hello World
4.0.30319.42000

D:\Source\HelloWorld\CommandLine>HelloWorld_VS_4.6.1.exe
Hello World
4.0.30319.42000

D:\Source\HelloWorld\CommandLine>HelloWorld_VS_4.6.2.exe
Hello World
4.0.30319.42000

D:\Source\HelloWorld\CommandLine>cd ..\HelloWorld_VS_Core_1.0

D:\Source\HelloWorld\HelloWorld_VS_Core_1.0>dotnet run
Project HelloWorld_VS_Core_1.0 (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation.
Hello World
.NETCoreApp,Version=v1.0


Note that the 2.0 and 3.x versions of the code run on the 2.0.50727.5485 version of the framework and the 4.x versions run on 4.0.30319.42000.

Now, let's load all of the files into JustDecompile and see what we can see.  The first thing I notice is that some of the files compiled down to x86 versions rather than Any CPU:

JustDecompile output for the different assemblies
Interesting!  Well, if I look at the properties on the 4.5 framework project I see that the Prefer 32-bit is checked by default:

.NET 4.5 default build properties


while the option is disabled available on the 2.0 project:

.NET 2.0 default build properties
What about the code itself, are there any differences in the code emitted?  Well, all of the programs decompiled into the same C# that went in (no surprise there) and the IL generated was the same for all of the VS versions:

IL produced from Visual Studio


but different for the command line compilation versions.

IL produced from command line compilation
Now, why are there nop commands littered through the code?  I compiled in debug mode and didn't optimize the code as VS does by default.  Oops!  Quick change to the batch file:

C:\Windows\Microsoft.NET\Framework\v2.0.50727\csc.exe /out:HelloWorld_CSC_2.0.exe Program.cs /debug- /optimize
C:\Windows\Microsoft.NET\Framework\v3.5\csc.exe /out:HelloWorld_CSC_3.5.exe Program.cs /debug- /optimize
C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /out:HelloWorld_CSC_4.0.exe Program.cs /debug- /optimize

and everything is matching nicely.  Ok, almost everything is matching.  Remember the .NET Core required a different call to get the version number?  Well of course that changed the C# which changed the IL.  But, the changes were much greater than just that.  Here is the IL:
IL produced from .NET Core HelloWorld
Well, that isn't helpful.  Even an extra large image isn't really readable.  That extra code to get the environment variable goes through a lot of abstraction layers.  Let's just look at a partial output, that which is in common with the other code:
Partial IL produced from .NET Core HelloWorld
The biggest differences are the ones that we would expect if we thought about what .NET Core is all about.  .NET Core doesn't rely on MSCorLib because that is the platform dependent part of MS.NET.  That is the part that the Mono guys and gals had to re-create/simulate to allow .NET code to run on the different environments they support.  .NET Core doesn't have that dependency, so it extends a different class.  That also means that it doesn't get things like System.Console for free anymore.  If you want to use it, you have to import it.

Ok, that's enough for now.  We haven't gotten into Mono or compiling on a different platform yet and we still have an interesting couple of good questions to discuss next time:

  1. Why are the files different sizes?
  2. Will the different compilers ever emit different IL?


To look at why the binary files differ in size and what is going on inside, we need to delve into PECOFF land (you can get a head start by looking at the whitepaper here) and break out something to look at the binary.

To see differences in the IL produced, we will have to leave behind our safe/simple HelloWorld program and get into some more complex code.

I hope you enjoyed the journey so far.  Please comment with suggestions or questions!

Part 1
Part 2
Part 3
Part 4
Part 5
Part 6
Part 7
Part 8
Part 9

No comments:

Post a Comment