devxlogo

Down to the Metal: Managed Code Under the Hood (Part III)

Down to the Metal: Managed Code Under the Hood (Part III)

n the first part of this article series you saw the basics of managed code execution and created your first Intermediate Language Assembler application; the well known “Hello World” sample. In the second part you saw how to perform basic programming tasks such as loops and control flow. In this part you will see how to use object oriented programming with Intermediate Assembler to declare fields, instance methods, and properties, and how to create instances and call their members. You’ll also add some more metadata with attributes to your classes. This article won’t cover more sophisticated topics such as delegates, as those are beyond its scope; but you can find out how to do that from the references and links in the resources column and at the end of this article.

A Sphere Example
The sample code for this article is another arithmetical project?a sphere class that you construct by providing the radius, and which offers properties for radius and volume. To make the sample a little more complex and to show off attribute usage in Intermediate Language Assembler you’ll also see how to add metadata to classes and methods in ILAsm.

Extending the Manifest
Once again, you need to add some extra complexity to the manifest. In the first article you used only the assembly name, in the second you added a version number. But there’s still a possibility for confusion and misuse, so now you’ll add a public key token to tell the CLR that you want a signed assembly with a specific public key.

Here’s the code:

?
Figure 1. The Global Assembly Cache: The Assembly Cache Viewer is a Windows shell extension that lets you view and manipulate the contents of the global assembly cache using Windows Explorer.
   .assembly extern mscorlib   {     .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                             .ver 1:0:5000:0   }      .assembly extern System.Xml   {     .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                   .ver 1:0:5000:0   }

You need mscorlib as the base assembly, as usual, but for this example you also need System.Xml because the sample uses the XmlSerializer class from that namespace.

You may be wondering where to get the values for the publickeytoken directives. That’s simple; the key values shown are the same keys as the ones shown in the Global Assembly Cache in Windows Explorer. So, to get the key for an assembly that resides in the GAC, simply direct your Windows Explorer to <%WinDir%>assembly. For example, Figure 1 shows a portion of the GAC listing on my computer from C:Windowsassembly.

Specifying Assembly Details
Now that you have finished the necessary imports, to complete the manifest you need to specify the details for the assembly in which you want your class to reside.

   .assembly Sphere   {     .hash algorithm 0x00008004     .ver 1:0:0:0   }      .module Sphere.exe   .subsystem 0x00000003

This code looks pretty much like the sample code from the last article; only the .hash directive is new. The .hash directive specifies the algorithm that the framework uses to compute the cryptographic hash used to sign this assembly. The value shown (0x8004) is the default value and represents the SHA1 algorithm, which all CLI compliant implementations must use. Microsoft chose SHA1 as the best widely available technology at the time of standardization. A single algorithm is the most practical solution because all conforming implementations of the CLI are required to implement all algorithms to ensure the portability of executable images. Note that all other values are currently reserved for future use.

Adding a Namespace and the Class Declaration
Now that you have finished the manifest you can add a namespace and a forward declaration for the classes in that namespace. You could add also the members of the class here, although it’s more convenient to first declare all classes and then to add the implementation of their members afterward.

Editor’s Note. No Line-Continuation Character in IL: Throughout this article, you’ll see lines of code that (as in the following code fragment) should appear on a single line; however, due to the limited display space available in most browsers, we were forced to wrap them. Wherever that occurs, we indented the wrapped lines to help make it clear that?when you write or copy the IL code yourself?you should put the code on a single line.

   .namespace Sphere   {     .class public auto ansi serializable beforefieldinit          Sphere extends [mscorlib]System.Object     {     }   }

The preceding code looks pretty much like C#, although you will notice a few differences: As always with ILAsm, all directives start with a dot (a period). The namespace declaration is simply a container declaration as in most high-level languages and?like C-style languages?uses curly brackets to define the scope?and therefore the members?of that namespace.

The .class directive is a bit more complicated, here’s a description of the keywords specified here:

  • public?This keyword has the same meaning as in most high-level languages: The class shall be visible to everybody.
  • auto?The framework should generate the memory layout of field members automatically
  • ansi?Marshal all strings that need to be marshalled to the platform as ANSI type strings.
  • serializable?Specifies that this type can be serialized. You need the serializable attribute because of the XmlSerializer that you’ll add later). Note that creating a serializable class in most high level .NET languages requires the SerializableAttribute.
  • beforefieldinit?This keyword specifies that calling a static method will not initialize the type, it instructs the CLI that it need not call a constructor of the type before calling a static method.
  • Sphere?The name of the sample class. In this particular case, it’s the same as the namespace name, but that’s just a coincidence.
  • extends?This is the keyword for inheritance. You follow it by a list of classes and interfaces that this type extends.
  • [mscorlib]System.Object?This is the base type that the current type extends. You could extend some other type, but every CLI compliant type must extend this base type.

Defining the Class Members
After adding the forward declaration of the classes in your namespace you can start to implement the class members. You begin with the namespace and the class like just as before:

   // CLASS MEMBERS DECLARATION    .namespace Sphere   {     .class public auto ansi serializable beforefieldinit         Sphere  extends [mscorlib]System.Object     {

Note that the preceding code is exactly the same as in the forward declaration.

Implementing Fields
Fields are extremely easy to implement. You simply add a .field directive and then specify the access modifier, type, and name, just as you do in any other high-level language:

   .field private float64 radius

The preceding code adds a field called “radius” of type float64 with an access modifier of private.

There are several types of access modifiers in Intermediate Language Assembler; here’s a complete list:

  • assembly
  • famandassem (family and assembly)
  • family
  • famorassem (family or assembly)
  • private
  • public
Author’s Note: Most high-level languages use the keyword protected instead of family

Adding Constructors
Constructors are basically methods of your class like any other method; but they have a special meaning, and therefore not only have a special name, but also a few special directives.

   .method public hidebysig specialname rtspecialname       instance void  .ctor() cil managed       {         .maxstack  2         ldarg.0         ldc.r8     0.0         call       instance void             Sphere.Sphere::.ctor(float64)         ret       }          .method public hidebysig specialname rtspecialname           instance void .ctor(float64 radius) cil managed       {         .maxstack  2         ldarg.0         call       instance void             [mscorlib]System.Object::.ctor()         ldarg.0         ldarg.1         stfld      float64 Sphere.Sphere::radius         ret       }

The above code specifies two constructors. The first one (which doesn’t require a parameter) calls the second one which accepts one float64 argument for initializing the radius of your sphere. In a high-level language such as C# you’d do this with an initializerlist.

Before examining the internal code of the constructors take a look at the declaration directives:

  • .method?The following code block is a method.
  • public?The constructor has to be callable by everybody.
  • hidebysig?Short for “hide by signature”; this is ignored by the runtime.
  • specialname?The method name needs to be treated in a special way by some tool.
  • rtspecialname?The method name needs to be treated in a special way by the runtime.
  • instance?This method is an instance method and not a static one.
  • void?The return type of the constructor (Note: in most high-level languages constructors do not have any return type).
  • .ctor?The name of the method; .ctor is reserved for constructors.
  • (), (float64 radius)?The argument list of the constructor.
  • cil?This method contains common intermediate language code.
  • managed?This method contains code that can be managed by the runtime (no unsafe code).

Now, take a look at the implementation of the constructor:

   ldarg.0   ldc.r8     0.0   call       instance void Sphere.Sphere::.ctor(float64)

Because the instructor is an instance method, the runtime needs to know on which instance to call that method. The first argument always stores the reference to the instance; therefore the code contains a ldarg.0 call to load the reference. Then it loads a default value for the radius as a float64 with a value of 0.0, and finally calls the overloaded constructor using the radius value on the stack as the one required parameter. Note that you have to add the keyword instance before the method signature to tell the runtime that you are not calling a static method, but an instance method. Because this constructor calls another one it doesn’t need to call the base class constructor first?the second method calls the base class constructor.

   ldarg.0   call       instance void [mscorlib]System.Object::.ctor()   ldarg.0   ldarg.1   stfld      float64 Sphere.Sphere::radius

As you can see, the code calls [mscorlib] System.Object::.ctor(), which is the constructor of the base class System.Object, to create the instance. Next, the argument passed for the radius needs to be stored in the appropriate field. The constructor does that using the command stfld, which means “store field”.

Creating Methods
The sample Sphere class exposes one CalcVolume method for calculating the volume of the sphere:

   .method family hidebysig instance float64 CalcVolume()       cil managed       {         .maxstack  2         ldarg.0         ldfld      float64 Sphere.Sphere::radius         ldarg.0         ldfld      float64 Sphere.Sphere::radius         mul         ldarg.0         ldfld      float64 Sphere.Sphere::radius         mul         ldc.r8     3.1415926535897931         mul         ret       }

The CalcVolume method has the access modifier family, which is about the same as protected in C#. The directive instance tells the runtime that this method can be called only on an instance of the Sphere class?that this method is not static. Note that to load the field value you always need to have the reference to the object (the this pointer, or Me in VB.NET) on the evaluation stack; therefore, there is a ldarg.0 before each ldfld (load field) statement to place the Sphere reference on the stack.

Defining Properties
In Intermediate Language assembler, you always implement properties as two separate methods: a get_ method and a set_ method:

   .property instance float64 Radius()   {     .custom instance void         [System.Xml]System.Xml.Serialization.        XmlElementAttribute::.ctor(string) =         ( 01 00 01 52 00 00 ) //"R"     .get instance float64 Sphere.Sphere::get_Radius()     .set instance void Sphere.Sphere::set_Radius(float64)   }

The preceding code defines a property named Radius. You can see that it contains the two directives for the property accessors which map the get and set actions to two different methods. The code also adds an attribute to the property. I’ll explain that later, in the “Adding Attributes” section of this article. First, have a look at the two accessor methods of the property:

   .method public hidebysig specialname instance float64       get_Radius() cil managed   {     .maxstack  1        ldarg.0     ldfld      float64 Sphere.Sphere::radius     ret    }      .method public hidebysig specialname instance void        set_Radius(float64) cil managed   {     .maxstack  2     ldarg.0     ldarg.1     stfld      float64 Sphere.Sphere::radius     ret   }

The get_Radius and set_Radius methods are no different from any other normal IL method. The only things that connect them to the property are the .get and .set directives in the property declaration.

Adding Attributes
You add attributes to elements in Intermediate Language Assembler by using a .custom directive right at the beginning of a code block. This is similar to the process for adding other directives such as .entrypoint, .locals init, or .maxstack.

   .custom instance void       [System.Xml]System.Xml.Serialization.      XmlElementAttribute::.ctor(string) =       ( 01 00 01 52 00 00 ) 

In the preceding code the .custom directive adds custom metadata to the element. In this case it’s adding an instance of a XmlElementAttribute with a constructor argument of type string that has the value “R.” The argument for the constructor has to have a special encoding with specific initializing and terminating bytes. You can find more info on this in the “Partition II Metadata” reference document described at the end of this article.

Instantiating Your Types
Now that you have defined your type members it’s time to instantiate your type. First, you need to define local variables to store the references of the types you are going to instantiate:

   .locals init (class Sphere.Sphere,     class [System.Xml]     System.Xml.Serialization.XmlSerializer xs,     class [mscorlib]System.IO.FileStream fs)

The preceding code creates three local variables. The first one will store a reference to the sphere object, the second one will hold a reference to an XmlSerializer and the third one will be used for a FileStream instance which has to be passed to the XmlSerializer. After creating the locals you need to instantiate the object, as follows:

   ldc.r8     1.   newobj     instance void Sphere.Sphere::.ctor(float64)   stloc.0

So, first you load load the arguments for the constructor call on the evaluation stack, and then you can use the statement newobj to create a new instance of a class by specifying the constructor method which has to be called as the argument of the newobj call. The newobj call returns a reference to the created instance on the evaluation stack. Therefore the only thing left to do is to store the reference to a local variable using the stloc statement.

Here’s the source for the other two objects needed:

   ldtoken    Sphere.Sphere   call       class [mscorlib]System.Type       [mscorlib]System.Type::GetTypeFromHandle(valuetype       [mscorlib]System.RuntimeTypeHandle)   ldstr      "DevXSample"   newobj     instance void       [System.Xml]System.Xml.Serialization.      XmlSerializer::.ctor(class [mscorlib]System.Type,       string)   stloc.1      ldstr      "sphere.xml"   ldc.i4.4   newobj     instance void       [mscorlib]System.IO.FileStream::.ctor(string,       valuetype [mscorlib]System.IO.FileMode)   stloc.2

The first newobj code block instantiates the XmlSerializer and the second one the FileStream for the target XML file.

Now that you have created all necessary objects you can call the serialize function to persist the sphere information to an XML file:

   ldloc.1   ldloc.2   ldloc.0   callvirt   instance void       [System.Xml]System.Xml.Serialization.      XmlSerializer::Serialize(class       [mscorlib]System.IO.Stream, object)

This code loads the references to the instances in the appropriate order on the evaluation stack and then uses callvirt to call the instance method Serialize exposed by the XmlSerializer object. The code uses callvirt used instead of call because the Serialize method is overridden, and the runtime has to figure out which override to use in this call.

It is legal to call a virtual method using call, which indicates that the method address is to be resolved using the class specified by method instead of dynamically resolving the address from the object being invoked. You might use this, for example, to compile calls to methods on the statically known parent class.

In this article series, you’ve seen the basics of object-oriented programming with Intermediate Language Assembler. Although the samples described in this series are basic, once you get started it should be quite easy to find your way through the documentation. Take a look at the accompanying source download and the books and Web sites in the “additional resources” section to learn more about ILAsm.

In addition to the online resources listed in the left column of this article, you may also find these print resources useful.

Books

  • MSPress, Serge Lidin, Inside Microsoft .NET IL Assembler, ISBN 0-7356-1547-0. You’ll find online corrections to this book here.
  • Wrox, Simon Robinson, Advanced .NET Programming, ISBN 1-86100-629-2.

Electronic Documents

  • Tool Developers Guide Subdirectory of your SDK Installation. See the two files Partition I Architecture.doc, and Partition III CIL.doc.

As always, you can download the source code for DevX articles from the download link in the left-hand column.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist