IL代码底层运行机制之循环处理

IL 代码底层运行机制之

** 循环处理 **

刘强

[email protected]

2003 年 10 月 22 日

上一篇文章我们讨论了 IL 代码的基本运行机制。在这篇文章里,我们将讨论 IL 代码是怎样处理 C# 中的循环。例子还涉及到数组处理,以及一些新涉及到的指令。虽然已经有人进行过相关问题的研究,我也看过几篇有关文章,不过我认为他们描述得并不是很清楚,所以在这里我借机重新整理成文,希望对大家学习理解 .net 会有所帮助,同时也希望对研究虚拟机机制的有关设计人员有所帮助。

同样,这里也先给出 C# 代码,然后再让我们详细研究其编译后的 IL 代码。下面是 C# 代码,它含有三个循环,分别是 for 、 while 、 foreach 循环:

public int LoopTest()

{

int i=3;

int j=9;

int s=0;

int k; file:// 以上各条语句定义变量并进行初始化

for(k=0;k<=i;k++)

{

s+=k;

} file://for 循环块

k=0;

while(k

  1<j) '<privateimplementationdetails="" 'array',="" (0x65)="" ([0]="" )="" .locals="" .maxstack="" .method="" 101="" 3="" 8="" 9="" [1]="" [2]="" [3]="" [4]="" [5]="" [6]="" [7]="" [8]="" [mscorlib]system.int32="" a="" a,="" add="" array="" array)="" ble.s="" blt.s="" br.s="" c#="" cil="" cs$00000003$00000000,="" cs$00000007$00000001,="" cs$00000008$00000002="" dup="" field="" file:="" foreach="" foreach(int="" hidebysig="" i,="" il="" il_0000:="" il_0001:="" il_0002:="" il_0004:="" il_0005:="" il_0006:="" il_0007:="" il_0008:="" il_0009:="" il_000b="" il_000b:="" il_000c:="" il_000d:="" il_000e:="" il_000f:="" il_0010:="" il_0011:="" il_0012:="" il_0013="" il_0013:="" il_0014:="" il_0015:="" il_0017:="" il_0018:="" il_0019:="" il_001b="" il_001b:="" il_001c:="" il_001d:="" il_001e:="" il_001f:="" il_0020:="" il_0021:="" il_0022:="" il_0023="" il_0023:="" il_0024:="" il_0025:="" il_0027:="" il_0028:="" il_002d:="" il_002e:="" in="" init="" instance="" int32="" int32[]="" int[]="" j,="" k++;="" k,="" ldc.i4.0="" ldc.i4.1="" ldc.i4.3="" ldc.i4.8="" ldc.i4.s="" ldloc.0="" ldloc.1="" ldloc.2="" ldloc.3="" ldtoken="" looptest()="" managed="" newarr="" public="" return="" s+="a;" s,="" s;="" stloc.0="" stloc.1="" stloc.2="" stloc.3="" system.int32="" valuetype="" void="" while="" {="" }="" ’数组。="" 代码以实现循环处理的,或者说如何用="" 代码大小="" 代码:="" 值。如果函数为="" 创建长度为="" 在这里,我们要做的是搞清楚="" 型,则无此变量。="" 局部变量,存储数组引用,用于="" 局部变量,存储数组索引。专用于="" 循环。本例中对应‘="" 循环块="" 循环,由编译器维护。="" 数组。可以看出数组元素被映射到="" 的="" 类对象。="" 编译器是把源程序翻译成怎样的="" 语言中的循环。这对我们深入理解="" 语言实现="" 语言特性是很有帮助的。当然仅仅这一点还不够,以后我还会介绍更多的有关方面的问题。="" 跟函数返回值类型相同的局部变量,由编译器维护,专门用于存储返回="" 首先让我们看看这个函数被编译成什么样的="">'/'$$struct0x6000002-1' 
  2
  3'<privateimplementationdetails>'::'$$method0x6000002-1' 
  4
  5IL_0033: call void [mscorlib] System.Runtime.CompilerServices.RuntimeHelpers:: 
  6
  7InitializeArray(class[mscorlib]System.Array, valuetype [mscorlib]  System.RuntimeFieldHandle) 
  8
  9IL_0038: stloc.s 'array' 
 10
 11IL_003a: ldloc.s 'array' 
 12
 13IL_003c: stloc.s CS$00000007$00000001 
 14
 15IL_003e: ldc.i4.0 
 16
 17IL_003f: stloc.s CS$00000008$00000002 
 18
 19IL_0041: br.s IL_0055 
 20
 21IL_0043: ldloc.s CS$00000007$00000001 
 22
 23IL_0045: ldloc.s CS$00000008$00000002 
 24
 25IL_0047: ldelem.i4 
 26
 27IL_0048: stloc.s a 
 28
 29IL_004a: ldloc.2 
 30
 31IL_004b: ldloc.s a 
 32
 33IL_004d: add 
 34
 35IL_004e: stloc.2 
 36
 37IL_004f: ldloc.s CS$00000008$00000002 
 38
 39IL_0051: ldc.i4.1 
 40
 41IL_0052: add 
 42
 43IL_0053: stloc.s CS$00000008$00000002 
 44
 45IL_0055: ldloc.s CS$00000008$00000002 
 46
 47IL_0057: ldloc.s CS$00000007$00000001 
 48
 49IL_0059: ldlen 
 50
 51IL_005a: conv.i4 
 52
 53IL_005b: blt.s IL_0043 
 54
 55IL_005d: ldloc.2 
 56
 57IL_005e: stloc.s CS$00000003$00000000 
 58
 59IL_0060: br.s IL_0062 
 60
 61IL_0062: ldloc.s CS$00000003$00000000 
 62
 63IL_0064: ret 
 64
 65} // end of method Advanced::LoopTest 
 66
 67关于函数话题如  .locals init  语句等,请参见文章〈函数相关〉。这里我对其中的一些指令做出解释,主要是与本文相关的条件转移指令(  b*.s  )等。其他指令以后我会作适当的介绍。如下所示: 
 68
 69指令 
 70
 71| 
 72
 73意义 
 74
 75| 
 76
 77记忆方法(  *  )   
 78  
 79---|---|---  
 80  
 81br.s 
 82
 83| 
 84
 85绝对跳转,相当于  jmp 
 86
 87|   
 88  
 89blt.s 
 90
 91| 
 92
 93小于转 
 94
 95| 
 96
 97Lower Than   
 98  
 99ble.s 
100
101| 
102
103小于等于转 
104
105| 
106
107Lower or Equals   
108  
109ldlen 
110
111| 
112
113取得数组长度 
114
115|   
116  
117ldelem.i4 
118
119| 
120
121根据索引取得数组项 
122
123|   
124  
125这里我们可以看到  .locals init  伪指令给出了同源程序相同变量名称。这是因为在反汇编时,相同目录下有调试信息文件(  *.pdb  ),否则的我们看到的结果变量以  V_x  形式(如  V_1  、  V_2  等)表示。有关函数局部变量的话题,请参见《函数相关》一文。 
126
127如果你有  WIN32  汇编程序设计经验,可能都熟悉怎样实现循环控制。如,要实现从  10  加至  100  的功能,我们可能会这样做: 
128
129mov ecx, 100 file://ecx  寄存器存放循环计数 
130
131xor eax, eax file://  给  eax  和标志寄存器清零 
132
133loop: add eax, ecx file://  实现相加并将结果存  eax 
134
135dec ecx file://  计数减一 
136
137cpr: cmp ecx, 9 file://  判断  ecx&gt;=10  或  ecx&gt;9 
138
139jg loop file://  如果判断结果为真(大于)的话,则转  loop 
140
141这跟高级语言(  C/C++/JAVA/C#  )不一样,  for  循环中的循环条件在程序首部给出,而顺序执行的低级语言如  MASM  都习惯是在循环末尾测试循环条件的。那么  C#  编译器又是怎样处理  C#  循环条件位置与一般汇编中循环条件测试语句位置的不一致,用  IL  来实现循环条件检测并正确实现循环的呢?首先,在这里我要说明,在顺序执行的汇编语言中,测试循环条件是完全可以放在循环首部的。如上例的  IL  版为: 
142
143.locals init([0] int32 eax, [1] int32 ecx  ,  [2] int32 RET_VAL) 
144
145ldc.i4 100 
146
147stloc.1 file://mov ecx, 100 
148
149ldc.i4.0 
150
151stloc.0 file://xor eax, eax  或  move eax, 0 
152
153L_0000: ldloc.1 
154
155ldc.i4 10 file:// 
156
157blt.s L_0003 // ecx &lt; 10 ? Yes-&gt; jmp L_0001  :  No -&gt; go on 
158
159L_0001: ldloc.0 
160
161ldloc.1 
162
163add 
164
165stloc.0 file://  这几句实现  eax=eax+ecx 
166
167ldloc.1 
168
169ldc.i4.1 
170
171sub 
172
173stloc.1 file://  这几句实现  ecx=ecx-1 
174
175L_0002: br.s L_0000 
176
177L_0003: … 
178
179其次,我要说明不这样做的理由。理由有二,其一是破坏了正常逻辑,这一点是从编译器层面上来说的。比如,对于语句  if(k<j) k="" k<j="" )的比较,为真向前跳转转,为假继续执行即可。而放在首部的话,则要进行的是(="" ,如果将比较放在循环尾部,进行的是(="">=j  )的比较,如真则向下跳出循环区域,如假则继续执行;在循环的末尾还要设置绝对跳转语句,以跳转到首部的比较指令处。由  (k<j) (k="" 向="">=j)  转变,对于我们人来说是很简单、直观的事情,可对编译器来说还要做更多的工作才能实现。更何况还有更复杂的布尔表达式呢,如(  k&gt;j  )  &amp;&amp; (k&gt;34) || (j&lt;=56)  。这就增加了编译器实现的负担——虽然不是很大的负担。而且,因为还增加了跳转语句,给编译器对跳转位置的定位增加了难度。大家知道,汇编器在处理、计算汇编语言中的标号与跳转指令的偏移量时要进行至少两次的扫描,高级语言就更复杂了。因此,采用前一种方法既容易理解,又容易实现。 
180
181下面我们来看看例子中三种循环的具体实现。有关  IL  代码的基本运行机制,请参看《  IL  代码底层运行机制》一文。  IL_0027  到  IL_003f  是进行数组初始化的,比较难懂一点。我们暂且放下,以后我还会介绍。 
182
1831.  for  语句 
184
185可以看出,程序段中  IL_0000  到  IL_0008  是执行变量初始化工作的。从  IL_0009  开始,就是循环体了。  IL_00009  是一条直接(绝对)跳转语句,跳转到  IL00_13  。我们看看这里的内容: 
186
187IL_0013: ldloc.3 
188
189IL_0014: ldloc.0 
190
191IL_0015: ble.s IL_000b 
192
193加载局部变量  3  (也即  k  ),再加载局部变量  0  (即  j  )。后面是一条比较转移指令  ble.s  。不难看出,这三条语句用于比较  k  与  j  的大小。如果比较结果为真(小于等于),则转入循环体内(  IL_000b  处),为假则继续执行直接出循环体。过程如行云流水,简洁直观,不多作解释。从这里我们也可以看出,  for  语句是先进行条件测试,后执行循环体的。 
194
195...  ldloc.3  ldloc.0  ble.s   
196  
197---  
198  
199top   
200  
201202  
203---  
204  
205top   
206  
207k   
208  
209210  
211---  
212  
213top   
214  
215j   
216  
217k   
218  
219220  
221---  
222  
223top   
224  
225226  
227load  指令将变量逐个加至程序栈。  ble.s  指令进行比较。值得我们注意的是,  ble.s  还要进行清栈操作。不仅是  ble.s  ,其他条件转移指令也都是如此。 
228
2292.  foreach  语句 
230
231foreach  语句和  for  语句处理过程大致相当。我们感兴趣的是  foreach  怎样处理边界条件。从  IL0041  开始,就进入了  foreach  循环体。同样一条直接跳转指令把我们带到了  IL_0055  处,让我们看看这里是什么。 
232
233IL_0055: ldloc.s CS$00000008$00000002 
234
235IL_0057: ldloc.s CS$00000007$00000001 
236
237IL_0059: ldlen 
238
239IL_005a: conv.i4 
240
241IL_005b: blt.s IL_0043 
242
243前面我介绍过  CS$00000008$00000002  是存储数组索引的,  CS$00000007$00000001  是数组‘  array  ’的引用。  IL0055  到  IL005b  的过程操作是这样的:首先向程序栈加载当前索引,再加载数组引用(  32  位的  HashCode  )。  ldlen  指令根据数组引用取得数组长度(  64  位长整型)并将之转换成  32  为整型,将索引与此长度进行比较。如果小于,则转入循环体继续执行;否则出循环。从这里我们也可以看出,  IL  对数组操作给予了很强的支持,直接为它提供了相应的指令。 
244
2453.  while  语句和  do-while  语句 
246
247从例子中可以看出,  while  和  for  循环处理方式是一样的。这里没有给出  do-while  例子,但是可以想见它跟  for  语句处理是一样的。但是,  do-while  循环要注意,在其循环首部没有像  for  和  foreach  循环那样的直接跳转指令跳转到条件测试代码处。因此,不管什么情况,  do-while  循环都是至少执行一次的。 
248
249在这篇文章中,我介绍了几条有关条件跳转指令,以及  C#  编译器是怎样处理  C#  语言中的循环的。其实,本文不能完全算是  IL  底层机制相关文章,但是要深入了解  IL  ,这点基础还是必要的。</j)></j)></privateimplementationdetails></j)>
Published At
Categories with Web编程
Tagged with
comments powered by Disqus