在本文的第二部分里,我将探讨一下局部变量的使用。为了展示这个过程,让我们来写一个将两数相加的简单程序。
在 MSIL 方法中,使用 .locals 指示符来声明局部变量
该语句为当前方法声明了三个局部变量。在本例中,恰巧都是 int32 类型—— System.Int32 类型的同义词。init 指定这些变量需要以其对应类型的默认值进行初始化。变量名称也可以被省略,那样的话你需要通过声明时的zero-based 索引来指明变量。当然,使用变量名会增加代码的可读性。
在我们继续之前,需要先明确一下 MSIL 当中显式使用栈的方式。当你需要向一个指令传值的时候,首先需要把那些值压栈。而指令需要进行弹栈操作以读出数值。类似地,在调用方法时也需要将对象引用 ( 如果有的话 ) 和需要传递的参数按顺序压栈。在开始调用方法时,所有的参数以及对象引用会被弹栈。使用 ldloc 指令来将变量的值压栈;使用 stloc 指令将栈顶的值弹出并保存到指定变量中。另外要时刻记得,值类型的右值会被直接存储到栈中,而对象 ( 引用类型的实例 ) 不会,因为 CLI 不允许在栈上为引用类型的对象分配内存,而只是将对象的引用存储到栈上。这类似于将一个原生C++对象分配在堆上并将一个指向它的指针存储在栈中。请在阅读本文的时候记住这个栈的存在,这将有助于理解为什么数值会不停地在栈上压入弹出。
下一步是要让用户输入相加的数值。
上一篇文章中提到, ldstr 指令将字符串压栈,而 call 指令调用 Write 方法,并将其参数弹栈。下一个 call 指令调用 ReadLine 方法,该方法从控制台读入并返回的字符串被压栈。因为返回值正好位于栈顶,所以我们直接调用 Int32::Parse 方法,将读入的字符串弹栈并将其等价的 int32 类型的数值压栈。注意为了清楚起见,我省略了所有的错误处理。接下来用 stloc 指令将数值弹栈并存储于局部变量 first 当中。第二个数值以同样的方式获得,并存储于变量 second 当中。
接下来,我们使用 add 指令完成加法并将结果保存到变量 result 。
最后显示结果。
我们使用WriteLine 的一个重载版本,它接收一个格式化字符串和三个 object 对象作为参数。调用之前,每一个参数都必须按顺序压栈。因为数值是以 int32 这个值类型来存储的,所以我们需要对其进行装箱操作,否则将无法匹配方法签名。 ldloc 指令将每一个参数压栈,随后 box 指令被应用于每个参数。装箱操作将数值弹栈,而后构造一个包含该数值拷贝的新对象,并将新对象的引用压栈。
完整地程序如下:
.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
}
最后值得注意的是,本例中的程序需要大小为4的栈空间,因为最后调用 WriteLine 方法时需要传入4个参数。