Learning CIL Series – Part 2
In part 1 of the series we went over the "Hello World" in CIL. This post will cover creating and calling methods as well as introducing a few more instructions. We will be getting input from the user, an integer, and then adding 10 to it, in a method we create, and then display it on the screen. Let's get started!
The Code:
.assembly extern mscorlib {}
.assembly FunctionCall {}
.namespace FunctionCall
{
.method public static void main() cil managed
{
.entrypoint
ldstr "Enter a number: "
call void [mscorlib]System.Console::Write(string)
call string [mscorlib]System.Console::ReadLine()
call int32 [mscorlib]System.Int32::Parse(string)
call int32 AddTen(int32)
call void [mscorlib]System.Console::Write(int32)
ret
}
.method public static int32 AddTen(int32) cil managed
{
ldarg.0
ldc.i4 10
add
ret
}
}
Overview
We start the program off the same way as we did in the Hello World program, by declaring mscorlib as an external assembly and declaring our assembly name (I chose the name FunctionCall).
.assembly extern mscorlib {}
.assembly FunctionCall {}
Next we encounter something new, the .namespace instruction. Like its counter-part in C#, the .namespace instruction creates a namespace with the methods/classes inside it. It can be used to group like minded classes/functions as well as to avoid name conflicts. We see the namespace and namespace name declaration here:
.namespace FunctionCall
That should be pretty basic so far.
Main Method
We now come to our main method. There are some things that are the same as the main from HelloWorld but then there are things that are different. So here is our main method, let's go over it, shall we?
.method public static void main() cil managed
{
.entrypoint
ldstr "Enter a number: "
call void [mscorlib]System.Console::Write(string)
call string [mscorlib]System.Console::ReadLine()
call int32 [mscorlib]System.Int32::Parse(string)
call int32 AddTen(int32)
call void [mscorlib]System.Console::Write(int32)
ret
}
We are greeted with the familiar .entrypoint instruction which, again, lets the framework know that this is the method that is call when the program is executed. We then call
ldstr "Enter a number: " call void [mscorlib]System.Console::Write(string)
which, as we saw last time, pushes the string "Enter a number: " onto the stack and then calls the Write method which takes a string. Note that calling Write is the same as WriteLine, the only difference is the output that the user sees.
Now comes the new part. We need a way to store what the user gives us so we can add 10 to it. We will read the input from the user as follows:
call string [mscorlib]System.Console::ReadLine()
If you have programmed for .NET using C# or VB.NET then you should be familiar with the ReadLine() function. It takes no arguments but returns a string. Why am I doing this instead of just calling Read()? There is a good reason for that which is explained on StackOverflow. Long story short, Read() returns unicode and not ascii, which means if the user enters a 5, the number 53 is stored and it throws everything off. Anyways, back to the code.
We have stored the user's input on the stack as a string. But wait...how are we going to add 10 to a string you ask? Simple, we convert it to an int before we add the 10. We are able to do that by calling the function Int32.Parse(), which takes a string as an argument.
call int32 [mscorlib]System.Int32::Parse(string)
Since all arguments come from the stack, we only need to declare the type of the argument. The return type of the Parse() method is an int, so we declare that right after the call instruction. The int32 is pushed onto the stack.
Next comes our call to our method that will be created shortly, AddTen which takes and int32 and returns an int32. Finally, comes the last two instructions which should be familiar:
call void [mscorlib]System.Console::Write(int32) ret
It takes int32 from the stack and then displays it on the screen and returns.
Our Own Function:
Since we have the rest of the program out of the way, lets look at the function we will create, AddTen:
.method public static int32 AddTen(int32) cil managed
{
ldarg.0
ldc.i4 10
add
ret
}
There are only 5 instructions here, including the declaration, yet only 2 of them we have seen before. We start out with:
ldarg.0
This instruction loads the first argument onto the stack. Since we only have one argument for this function, we only have to load the 0th position. After we have loaded on the argument on the stack we need to somehow come up with the integer 10, after all the function is called AddTen. Because the .NET virtual machine is stack based we need to get that 10 on the stack so we using the instruction:
ldc.i4 10
The ldc is the instruction to push a value to the stack while the .i4 indicates that we are pushing an int32. This pushes 10 onto the stack. Adding the 10 with our argument is simple, so simple it doesn't even need an explanation and one could most likely surmise what it would be. Just in case, here is how we do it:
add
After that we have to return to the calling method with the return instruction:
retConclusion
There you have it. We introduced a handful of new instruction as well as learned how to create our own functions. If you're interested in more code examples, feel free to check out my CIL Examples on Github.