Introduction to MSIL – Part 2 – Using Local Variables
In part 2 of the Introduction to MSIL series, I will be exploring the use of local variables. Without variables, programs would not be very interesting. To illustrate the use of variables, let’s write a simple program to add numbers together.
In an MSIL method, variables are declared using the .locals directive.
.locals init (int32 first,
int32 second,
int32 result)
This statement declares three local variables for the current method. In this case they all happen to be of type int32, which is a synonym for the System.Int32 type. init specifies that the variables must be initialized to the default values for their respective types. It is also possible to omit the variable names. In that case you would refer to the variables by their zero-based index in the declaration. Of course, using variable names improves readability.
Before we continue, I want to make sure that it is clear how the stack is used explicitly in MSIL. When you want to pass values to an instruction, those values need to be pushed onto the stack. To read those values, the instruction must pop them off the stack. Similarly when calling a method, you need to push the object reference (if any) onto the stack followed by each argument that you wish to pass to the method. In the process of invoking the method, all the arguments as well as the object reference will be popped off the stack. To push a value onto the stack, use the ldloc instruction indicating the variable that holds the value. To pop a value off the stack, use the stloc instruction specifying the variable you wish to store the value in. Also keep in mind that values (based on value types) are stored directly on the stack, but objects (instances of reference types) are not since the CLI does not allow reference types to be allocated on the stack. Rather references to objects are stored on the stack. This is analogous to a native C++ object allocated on the heap with a pointer to it stored on the stack. Keep the stack in your mind as you read this series on MSIL. It should help you understand why values are continually pushed and popped on and off the stack.
The next step is to get the numbers from the user.
ldstr "First number: "
call void [mscorlib]System.Console::Write(string)
call string [mscorlib]System.Console::ReadLine()
call int32 [mscorlib]System.Int32::Parse(string)
stloc first
As I mentioned in Part 1, the ldstr instruction pushes the string onto the stack and the call instruction invokes the Write method, popping its argument off the stack. The next call instruction invokes the ReadLine method which returns a string. The returned string is pushed onto the stack and since it is already there, we simply call the Int32::Parse method which pops the string off the stack and pushes the int32 equivalent on. Note that I am omitting any error handling for the sake of clarity. The stloc instruction then pops the value off the stack and stores it in the local variable named 'first'. Getting the next number from the user works the same way except that the value is stored in the local variable named 'second'.
Now that we have read the two numbers from standard input, it is time to add them up. The add instruction can be used for this purpose.
ldloc first
ldloc second
add
stloc result
The add instruction pops two values off the stack and calculates the sum. To push the values of the local variables onto the stack, we use the ldloc instruction. When the add instruction completes, it pushes the result onto the stack and the program pops the value off the stack and stores it in a variable named 'result' using the stloc instruction.
The final step is to display the result to the user.
ldstr "{0} + {1} = {2}"
ldloc first
box int32
ldloc second
box int32
ldloc result
box int32
call void [mscorlib]System.Console::WriteLine(string, object, object, object)
We use the WriteLine overload that takes a format string followed by three object arguments. Each argument to the WriteLine method must be pushed onto the stack one by one. Since the numbers are stored as int32 value types, we need to box each value; otherwise the method signature won’t match.
The ldloc instruction pushes each argument onto the stack. The box instruction is then used for each int32 argument. Boxing involves popping the value off the stack, constructing a new object containing a copy of the value and then pushing a reference to the object onto the stack.
Here is the complete program.
.method static void main()
{
.entrypoint
.maxstack 4
.locals init (int32 first,
int32 second,
int32 result)
ldstr "First number: "
call void [mscorlib]System.Console::Write(string)
call string [mscorlib]System.Console::ReadLine()
call int32 [mscorlib]System.Int32::Parse(string)
stloc first
ldstr "Second number: "
call void [mscorlib]System.Console::Write(string)
call string [mscorlib]System.Console::ReadLine()
call int32 [mscorlib]System.Int32::Parse(string)
stloc second
ldloc first
ldloc second
add
stloc result
ldstr "{0} + {1} = {2}"
ldloc first
box int32
ldloc second
box int32
ldloc result
box int32
call void [mscorlib]System.Console::WriteLine(string, object, object, object)
ret
}
The last thing to notice about this example is that I indicated the method will use at most four stack slots. This is to accommodate the four arguments passed to the WriteLine method at the end of the main method.
Read part 3 now: Defining Types
© 2004 Kenny Kerr