|
Page 2 of 2
The stack
Of all of the features of IL, the one that high level language programmers tend to find strange is the central role the stack plays.
If you need a refresher on what a stack is, how it works and what it is used for then see - Stacks and Stack and Heap Memory Allocation..
In this case the stack is a little more sophisticated than a simple block of memory with a pointer. You need to think of it in terms of a strongly typed stack made up of “slots” that hold a complete data type.
When you push data onto and pop data off the stack it always works in terms of a complete data type. Nearly all IL instructions work by popping input data from the stack and pushing their results on the stack.
As an example, let’s add two numbers together.
The first instruction is ldc or LoaD Constant
ldc.i4 0x01
This pushes the 4-byte integer constant, i.e. an Int32, onto the stack. The ldc part of the instruction tells you what is to happen and the code after the dot tells you the data type - i4 is a four byte integer in this case. There are ldc instructions for all the standard data types - for example ldc.r8 pushes a float32 on the stack.
To perform an add we need two values on the stack so we need to push another int32 and then give the add command:
ldc.i4 0x02 add
The add command takes the top two items on the stack adds them together and pushes the result back on the topi of the stack.
Now we have the result of adding 1 and 2, i.e. 3, on the top of the stack and we can use WriteLine again to display the result and issue a return to complete the job:
call void [mscorlib]System.Console:: WriteLine(int32) ret
Notice that everything is still strongly typed and the add instruction can discover the type of the two items on the top of the stack and push an appropriate type back on the stack – you can try the same with floating point numbers:
ldc.r4 0.1 ldc.r4 0.2 add call void [mscorlib]System.Console:: WriteLine(float32) ret
The range of primitive data types available to you is similar to those in C# or VB with some changes to the names used.
Local variables
As well as the stack there are local variables, data structures and fields. But notice that in principle you can write any program using just the stack. For example to declare local variable called Total you would add:
.locals init(float32 Total)
The “init” is a modifier that indicates that the variables have to be initialised before use.
To load the result of the addition you have to use:
stloc Total ldloc Total
before the call to WriteLine.
The instruction stloc, i.e. Store to Local, pops the top of the stack into Total. You need the ldloc instruction, i.e. LoaD from Local, to push the value back on the stack so that the WriteLine can use it.
It is more common to work with local variables just in terms of the index number. For example:
.locals init([0] float32 Total)
Defines Total to be local varible zero and you can load it onto the stack using any of:
ldloc.0 ldloc 0 ldloc Total
Object oriented IL
Using a static object isn’t really the same thing as taking a full object-oriented approach – it’s just a way of writing a main program.
This next example is intended to give you an idea of the full extent of IL’s object facilities. Start a new program called Arith.il. First we have the usual declarations followed by a public class definition:
.assembly extern mscorlib {} .assembly Arith{} .module Arith.exe .class public Arith { .method public specialname void .ctor() { ret } .method public float32 Add(float32,float32) { ldarg.1 ldarg.2 add ret } }
The class has two methods .ctor which is its constructor – which does nothing in this case - and Add.
The Add method pushes its two parameters on the stack, using ldarg.n, adds them and leaves the result on the stack.
To try this class and its Add method out we use the static Main method again:
.class Test.Program extends [mscorlib]System.Object { .method static void Main(string[] args) cil managed { .entrypoint newobj instance void Arith::.ctor() ldc.r4 0.1 ldc.r4 0.2 call instance float32 Arith::Add(float32,float32) call void [mscorlib]System.Console:: WriteLine(float32) ret } }
The newobj instruction creates an instance of the class and calls its creator, .ctor(). The result of newobj is a pointer to the instance stored on the top of the stack.
Now we can load the stack with two parameter values and call the instance of Add.
Notice that the instance of the class that is called is determined by the first argument, i.e. arg0, passed to the method. You can think of this as a “this” reference and note that instance methods have to explicitly use it to work with instance fields. If you assemble this program you will discover that it adds two numbers together as before.
IL supports instance and static methods and fields. It supporst virtual and non-virtual methods and inheritance but this is beyond the scope of this introduction.
Where next?
Once you have the idea of the way that the object-oriented, strongly typed aspects of IL interact with the fact that it is a stack-oriented assembler you should find it easier to understand the documentation.
You can find some very dry technical definitions of how it all works at:
http://msdn.microsoft.com/en-us/netframework/aa569283.aspx
Another good way of learning IL is to use the ILdasm tool, which you will find in the same directory as ILasm. This can be used to disassemble .NET programs and it provides lots of clues as to how the compilers use IL.
If you would like more articles on how to use IL then email me at harry.fairhead@i-programmer.info.
If you would like to be informed about new articles on I Programmer you can either follow us on Twitter, on Facebook or you can subscribe to our weekly newsletter.
Each provides a full list of what's new each week - usually five hot book reviews, five thought-provoking articles and five not-to-be missed news items - in a compact click-to-read form.
<ASIN:1590596463>
<ASIN:0321694694>
<ASIN:0735627045>
<ASIN:0130622966>
<ASIN:0321578899>
<ASIN:1430225491>
<ASIN:0735619883>
|