前阵子俺一直沉迷在游戏和小说中,最近玩累了。
于是回过头来,突然发现有些东西感觉就像抓不住的水一样就要被淡忘了。
也许是需要复习一下了,可不能把以前学的全忘光了……
当然,以我的懒惰,所谓的复习也就 基本是随兴复习,没什么计划的乱来了。
-------------------
MSIL和堆栈形影不离,两者的关系就像手术室中的医生和护士。
医生负责动手术(代码部分),护士负责把各种工具给医生准备好(数据部分),以备医生调用。
从一开始我就觉得,堆栈这玩意就像是一个弹夹,讲究先进后出。其基本的操作也就两种“压子弹”(push)和“扣扳机发射子弹”(pop)。
话说当一个方法运行之前和运行完后都需要保证堆栈是空的(官方语言叫堆栈平衡——个人认为和做手术的时候医生绝对不能把手术刀遗漏到病人身体里是以一个意思)。
---------下面看段代码
一个全局变量和一个方法
static string s;
string test1()
{
string ss = "123";
int ix = 1;
i = ix+111;
ss = ss + "s";
s = ss;
return s;
}
-------------以下是这个方法的IL
.method private hidebysig instance string
test1() cil managed
{
// 代码大小 23 (0x17)
.maxstack 1
.locals init ([0] string ss,
[1] string CS$1$0000)
IL_0000: nop
IL_0001: ldstr "123"
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: stsfld string ILtest.ILtest1::s
IL_000d: ldsfld string ILtest.ILtest1::s
IL_0012: stloc.1
IL_0013: br.s IL_0015
IL_0015: ldloc.1
IL_0016: ret
} // end of method ILtest1::test1
我们现在来分析一个方法的IL,为什么突然要贴代码呢,因为我觉得这段简单的代码可以很容易的说明堆栈的一些情况。
--------------------
.method private hidebysig instance string
test1() cil managed
这段是方法声明,先看看这个声明
.method告诉大家了,我是一个方法。
private代表私有。
hidebysig 就相当于new,这个可以自己翻翻书。
instance string test1() cil managed也就不用说了。
---------------下面接下来讲方法内容,
.maxstack 1 这句话定义了弹夹(堆栈)的大小,只有可怜的1啊。。。为什么是1呢,这就要往下看了。
.locals init ([0] string ss,
[1] string CS$1$0000) 定义了两个本地变量 string 类型的 ss和 CS$1$0000,其中ss是程序员定义的变量,CS$1$0000是系统定义的变量
IL_0000: nop --空指令 (这时候堆栈没有值,弹夹是空的)
IL_0001: ldstr "123" --把"123"放到堆栈上(压了一颗子弹进去,现在弹夹里有了一颗子弹)
IL_0006: stloc.0 --把"123"再放到第一个本地变量里([0] string ss)相当于 ss = "123"(把子弹射出去了,现在弹夹空了)
IL_0007: ldloc.0 --把第一个本地变量再放回堆栈里(把[0] string ss 压进弹夹里了,现在弹夹又变成有一颗子弹)
IL_0008: stsfld string ILtest.ILtest1::s --把堆栈里的值送到全局变量 s 中(又把子弹射出去了,现在弹夹又空了)
IL_000d: ldsfld string ILtest.ILtest1::s --把全局变量 s 放回堆栈(把 s 压进弹夹里了,现在弹夹又变成有一颗子弹)
IL_0012: stloc.1 --把堆栈里的值再放到第二个本地变量里([1] string CS$1$0000)(把子弹射出去了,现在弹夹空了)
IL_0013: br.s IL_0015 --跳转到IL_0015指令去,其实就是吓一跳指令
IL_0015: ldloc.1 --把第二个本地变量再放回堆栈里([1] string CS$1$0000压进弹夹里了,现在弹夹又变成有一颗子弹)
IL_0016: ret --把堆栈里的值返回给被调用者 (现在方法完了,堆栈又没有值了,弹夹又空了)
看完这段IL,也许发现了,这个方法的堆栈上从头到尾都只有一个值。而这就是为什么方法一开始就把堆栈的大小设为1的原因。
那么这个“1”有多大呢?是一个字节吗,还是一个字?
其实堆栈里的每个单元称之为slot,每个slot里都可以放一个托管的对象。int,struct,type等等。所以这个“1”有多大呢,那就要看slot里放的是什么了。
到这里就出来个问题,那么这个堆栈(我们称之为方法堆栈吧)是从哪里来的呢?
那么我们就要去看另一条IL语句了,
在MANIFEST里有一条IL:
.stackreserve 0x00100000,
这句话定义了这个现在的线程堆栈大小为0X100000,
也就是1M大小。
而我们所说的方法堆栈其实就是从.stackreserve 0x00100000这个地方划出来的。
----------
大家应该很熟悉这个界面吧,这是调用堆栈界面。
它会告诉你现在运行的这个方法的来龙去脉,告诉你这个方法从哪里来,中间爬过了多少的山,涉过了多少的河。。。
那么我们的vs是怎么记下的这些记录呢?
原来当clr每调用一个方法时,会从那1M(.stackreserve
0x00100000)的线程堆栈中分配出一块内存来保存这些信息(我们称之为堆栈帧)。
下图是这些内存的布局
图片里的evaluation stack也是个堆栈的名字,其实这个堆栈就是刚才我们在方法里一直用来用去的那个
方法堆栈。
为什么要从IL扯到 堆栈来呢,因为我们有时候都会碰到一个问题
---调用递归方法的时候经常会产生堆栈溢出的错误,
如果大家看懂了这篇文章,也许就会对这个错误有更深的了解了。