Browse DevX
Sign up for e-mail newsletters from DevX


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

In this part of the series, you'll dig deeper into MSIL and learn to write conditional code, loops, and how to handle errors.




Building the Right Environment to Support AI, Machine Learning and Deep Learning

n the first part of this article series you explored the basics of managed code execution and created your first Intermediate Language Assembler application; the well-known "Hello World" sample. In this second part you'll see how to use flow control statements, declare variables, create your own methods, and call them. And you'll add some more metadata to your assemblies.

This article won't cover object-oriented programming topics such as creating your own classes, calling instance methods, creating properties and fields, etc., but you'll find those topics in the third and final part of this series.

A Simple Arithmetic Sample
The Hello World sample application didn't do much, so here's a small application that performs some work. This is a simple arithmetic application that calculates the volume of a sphere. It asks users to input a floating point value for the radius of the sphere, then calls a method that calculates and returns the volume. Finally, it prints the value to the screen.

First you'll have to write the manifest for your assembly to the beginning of your source file. This time there will be a few more features than in the "Hello World" application from Part I.

.assembly Sphere { .ver 1:0:0:0 } .assembly extern mscorlib { .ver 1:0:500:0 } .module Sphere.exe .subsystem 0x00000003

You need to specify the name of the assembly you are about to write. But this time the code also includes a version declaration. The .ver directive tells the compiler to write the version number to the metadata of your assembly. As you can see, there's also a second .assembly directive, but with the additional keyword external, which states that this is a reference to an external assembly. That's similar to the /r parameter of the csc.exe C# Compiler which references other assemblies that needed to compile an assembly. In this case, the .ver directive after the second .assembly references version 1.1 of the .NET framework. If you were to write the assembly code without using that version directive it would default to framework version

Next, the.module directive specifies the name of the module (you can think of this as the filename) where the compiler will write the compiled assembly.

The last directive in the manifest is .subsystem with a value of 3, meaning that this executable will be a console application. You could omit that, because a console application is the default target anyway.

Now that your manifest is finished you can write the rest of the code. It's not too long, so I'll show it all at once:

.method static void main() cil managed { .entrypoint .maxstack 2 ldstr "Radius: " call void [mscorlib]System.Console::Write(string) call string [mscorlib]System.Console::ReadLine() call float64 [mscorlib]System.Double::Parse(string) call float64 CalcSphereVolume(float64) ldstr "Volume: " call void [mscorlib]System.Console::Write(string) call void [mscorlib]System.Console::WriteLine(float64) ret }

The .main method declaration is the same as for the Hello World sample, but the .maxstack directive here has a value of 2 because you need two items on the stack at the same time to create the memory space you need to store and retrieve one of the values to or from a local variable.

When you look back at the first part of this article series you'll find that the first two "real" code lines are essentially the same as the "Hello World" Sample. Those are followed by a call to System.Console::ReadLine, which puts a string on the evaluation stack when it returns. The code is basically equivalent to calling System.Console.ReadLine() in C#.

Because all input from the console is a string containing the characters the user typed in, you must convert the string value to a 64-bit floating point value. Perhaps you'd usually use the Convert class, but this sample uses the System.Double.Parse method to illustrate the difference in type names between the CLI CTS and high-level languages such as C#.

Note that so far there's no error checking or handling around the conversion code. You'll add that to the sample later on.

At this point, you will have one float64 element on the evaluation stack that represents the radius of the sphere. Here's how to do the calculation. You could simply inline the code, but that would be too easy; instead, you'll implement a method that returns the volume of the sphere as the topmost item on the evaluation stack. I'll show you that method later. For now, it's important to notice that the code calls the CalcSphereVolume method without giving it a fully qualified name; the call specifies only the return type, method name, and the parameter list. That's because CalcSphereVolume is a static global function defined in the same assembly. In fact, you aren't even allowed to use a fully qualified name (you use square brackets ([]) to designate cross-assembly references) for calls within the same assembly.

The CalcSphereVolume method pushes one float64 item from the stack and puts one float64 item back on the stack when it returns. Conveniently, at this point you have one float64 on the stack from the System.Double.Parse conversion, exactly what you need for the CalcSphereVolume method. Here's the code for this method:

.method static float64 CalcSphereVolume(float64) cil managed { .maxstack 3 ldarg.0 dup dup mul mul ldc.r8 3.141592654 mul ret }

Uhhh, … looks pretty scary, doesn't it? You'll find though, that when you examine the code line by line it's not that hard to understand.

ldarg.0 is a short version of ldarg, which is the token for the "load argument" opcode (operation code). Normally ldarg requires an argument specifying the index of the value it should load from the argument list. That version would use two bytes in the binary. This shorthand version (ldarg.0) requires only one byte. There are short versions for the first four arguments; if your method uses more than four arguments you have to call them with an operand to load the corresponding item on the execution stack, for example ldarg 5.

You need to write code to implement the following equation to solve the mathematical problem:

Volume = Radius³ * Pi

CalcSphereVolume is a new method, so you have a fresh and clean method state and therefore an untouched, virginal evaluation stack. ldarg.0 places the first argument from the caller on the evaluation stack. The dup statement duplicates the value at the top of the stack. Therefore, after the second dup statement the stack looks like this:

Radius, Radius, Radius

You need the three duplicates to cube the radius value. Next you'll see two mul operations. A mul takes two items from the stack, multiplies them together, and puts the result back on the top of the stack. You get the following stack transition diagram for the two multiplications:

Radius, Radius, Radius --> Radius, Radius² Radius, Radius^2 --> Radius^3

Perfect! Now you have the cubed Radius value on the stack and only need to multiply that by Pi. But Pi is nowhere on the stack, so you need to load the Pi constant onto the stack before you can multiply. For this you will use the ldc statement, which is short for "load constant". The Pi constant is a float64 (eight bytes) so you use the ldc.r8 directive. Be careful, the type is called float64, but all operations for that type use the abbreviation r8, which stands for "real 8 byte."

After the load operation your evaluation stack looks like this:

Radius^3, Pi

Now you can call mul to multiply those two values, which will put the result back on the stack. Your method should return a float64, therefore you can simply call ret to return the last item on the stack to the caller.

Note: Remember that the execution flow rules state that the evaluation stack must always be empty when a methods ends. In this case ret pops the last item from the stack and passes it back to the caller, leaving the stack empty at the end of the method scope.

Flow Control
In addition to getting input and output and making calculations, you need to be able to implement basic flow control in ILAsm. In this section you'll see how to branch on conditional statements and how to repeat code in a loop.

I'll show only the important parts of the code here, for the complete source to this section take a look at the FlowControl.il file in the downloadable sample code.

Comment and Contribute






(Maximum characters: 1200). You have 1200 characters left.



Thanks for your registration, follow us on our social networks to keep up-to-date