//
you're reading...
intermediate language, Programming Issues/Tips

Explore Possibilities with MSIL.

1. Introduction.

1.1. It will be hard to find an intermediate and advanced .NET programmer who has not heard of MSIL.

1.2 I have occasionally worked with IL code in the past, specifically when there is a need to modify the declarations of parameters and attributes for the purpose of interop marshaling.

1.3 Then last year, I recalled a great article written by Matt Pietrek titled “Just-Enough-Assembly-Language-to-Get-By Guide” on MSJ which I read many years ago.

1.4 It brought back much nostalgia and inspired me to want to seriously study the .NET Intermediate Language for its own sake.

1.5 Note that it is not my intent to demonstrate how to program using IL. There are many reference materials on this already including some listed in the reference section below.

1.6 I urge the reader to study these references in order to get a basic understanding of MSIL.

1.7 I will proceed with the assumption that the reader has a fairly firm grasp of IL code.

1.8 In this blog and several more like it to come in the future, I intend to show readers some of the benefits of programming in MSIL.

1.9 These benefits include writing .NET code that cannot be accomplished in higher-level languages like C#.

1.10 This series of blogs primarily serve to provide programming tips to the reader.

2. What I Plan to Show.

2.1 In this current blog, I will show the reader how to customize the entry point of a C# program.

2.2 I will write a simple C# console program.

2.3 Then, via ILDASM.exe, we will disassemble the program and then modify the output IL code.

2.4 I will specifically change the entry point of the program from Main() to a custom method.

2.5 After that, I will use ILASM.exe to re-assemble the IL code and then run the resultant program.

3. Simple C# Console Program.

3.1 The following is a simple C# console program :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestConsoleApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Inside Main()");
        }
    }
}

Notice that I have declared class Program as a public class and Main() as a public method. This is important as will be explained later.

3.2 After compiling this code in Visual Studio, a console executable TestConsoleApp.exe will be created. We then open a command line prompt, go to the output folder of the project and run the following command :

ildasm /TEXT TestConsoleApp.exe /OUT=TestConsoleApp.il
  • ILDasm.exe is used to de-compile TestConsoleApp.exe.
  • A file TestConsoleApp.il (a text file) will be produced.
  • You can open this IL file using notepad.exe.

3.3 The following is a listing of TestConsoleApp.il (with some parts omitted for brevity) :

 
//  Microsoft (R) .NET Framework IL Disassembler.  Version 4.6.81.0
//  Copyright (c) Microsoft Corporation.  All rights reserved.



// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly TestConsoleApp
{
    // Code not shown for brevity.
    ...
    ...
    ...
}
.module TestConsoleApp.exe
// MVID: {3301E34A-3F33-4110-A7AC-00825B0E44B9}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000003    //  ILONLY 32BITREQUIRED
// Image base: 0x01890000


// =============== CLASS MEMBERS DECLARATION ===================

.class public auto ansi beforefieldinit TestConsoleApp.Program
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Inside Main()"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       8 (0x8)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Program::.ctor

} // end of class TestConsoleApp.Program


// =============================================================

// *********** DISASSEMBLY COMPLETE ***********************
// WARNING: Created Win32 resource file TestConsoleApp.res
  • The important part of the code above is highlighted in bold : .entrypoint.
  • The .entrypoint keyword must be embedded within a method.
  • It identifies the method as the entry point of the application.

3.4 Now, let’s do some modification to the IL code :

3.4.1 First comment out the .entrypoint keyword as shown below :

  .method public hidebysig static void  Main(string[] args) cil managed
  {
    // Comment out the .entrypoint keyword.
    //.entrypoint
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Inside Main()"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Program::Main

3.4.2 Next copy the following code :

.method public static void MyEntryPoint(string[] args)
{
    .entrypoint
    ldstr      "Entered MyEntryPoint()."
    call       void [mscorlib]System.Console::WriteLine(string)

    ldarg.0
    call       void TestConsoleApp.Program::Main(string[])

    ldstr      "Leaving MyEntryPoint()."
    call       void [mscorlib]System.Console::WriteLine(string)

    ret    
}

and paste it inside the IL code right at the end as follows :

...
...
...
} // end of class TestConsoleApp.Program


// =============================================================

// *********** DISASSEMBLY COMPLETE ***********************
// WARNING: Created Win32 resource file TestConsoleApp.res

.method public static void MyEntryPoint(string[] args)
{
    .entrypoint
    ldstr      "Entered MyEntryPoint()."
    call       void [mscorlib]System.Console::WriteLine(string)

    ldarg.0
    call       void TestConsoleApp.Program::Main(string[])

    ldstr      "Leaving MyEntryPoint()."
    call       void [mscorlib]System.Console::WriteLine(string)

    ret    
}

3.4.3 Next perform a re-compilation of the IL code using ILAsm.exe as follows :

ilasm /EXE TestConsoleApp.il

3.4.4 If you have followed the instructions above carefully, a new TestConsoleApp.exe will be created. Run this program and the following will be output on the console :

Entered MyEntryPoint().
Inside Main()
Leaving MyEntryPoint().

3.5 The following is what we have done :

  • We have created a static global method (global functions are recognized in MSIL) named MyEntryPoint().
  • This global method has replaced Program::Main() as the entry point to the program.
  • MyEntryPoint() served as a wrapper for Main() and outputs some messages on the console just before calling Main() and just after calling Main().
  • Notice that MyEntryPoint() also takes the program argument string array as parameter and will pass it to Main().

3.6 Note that MyEntryPoint() worked only because the Program class and Main() have both been declared as public. As a global method, MyEntryPoint() has no access to Program::Main() unless the Program class and Main() are both public.

3.7 But what if these are private ? There are 2 ways to overcome this issue :

3.7.1 By replacing the private keyword with public :

.class /*private*/ public auto ansi beforefieldinit TestConsoleApp.Program
       extends [mscorlib]System.Object
{
  .method /*private*/ public hidebysig static void  Main(string[] args) cil managed
  {
    //.entrypoint
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Inside Main()"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       8 (0x8)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Program::.ctor

} // end of class TestConsoleApp.Program

3.7.2 By inserting MyEntryPoint() as a method of the Program class :

.class private auto ansi beforefieldinit TestConsoleApp.Program
       extends [mscorlib]System.Object
{
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    //.entrypoint
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Inside Main()"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       8 (0x8)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Program::.ctor

.method private static void MyEntryPoint(string[] args)
{
    .entrypoint
    ldstr      "Entered MyEntryPoint()."
    call       void [mscorlib]System.Console::WriteLine(string)

    ldarg.0
    call       void TestConsoleApp.Program::Main(string[])

    ldstr      "Leaving MyEntryPoint()."
    call       void [mscorlib]System.Console::WriteLine(string)

    ret    
}

} // end of class TestConsoleApp.Program

4. An Important Side-Note.

4.1 Note that a managed EXE program must have one single entry point. ILAsm.exe, when run with the /EXE option, will not compile a target IL code that either does not have the .entrypoint keyword or has more than one.

5. Summary.

5.1 I hope that this small programming tip has been interesting to the reader.

5.2 I have successfully modified the entry point of console programs as well as Windows forms programs.

5.3 Go ahead and experiment with de-compiling managed code, inserting your own entry point and then re-compiling.

5.4 I will be writing more blogs on this in the future.

References

Introduction to IL Assembly Language

Understanding Common Intermediate Language (CIL)

MSIL Programming Part 1

MSIL Programming Part 2

 

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: