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

IL代码底层运行机制之循环处理 (转)[@more@]microsoft FrontPage 4.0">

  IL代码底层运行机制之

        循环处理

                         刘强

                     cambest@sohu.com

                      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

  {

  s+=k;

  k++;

  }  file://while循环块

 

  int[] array={2,3,4,5,6,7,8,9};

  foreach(int a in array)

  {

  s+=a;

  }  file://foreach循环块

  return s;

}

在这里,我们要做的是搞清楚C#编译器是把源程序翻译成怎样的IL代码以实现循环处理的,或者说如何用IL语言实现C#语言中的循环。这对我们深入理解C#语言特性是很有帮助的。当然仅仅这一点还不够,以后我还会介绍更多的有关方面的问题。

  首先让我们看看这个函数被编译成什么样的IL代码:

.method public hidebysig instance int32  LoopTest() cil managed

{

  // 代码大小  101 (0x65)

  .maxstack  3

  .locals init ([0] int32 i,

  [1] int32 j,

  [2] int32 s,

  [3] int32 k,

  [4] int32[] 'array',

  [5] int32 a,

  [6] int32 CS$00000003$00000000,

  file://跟函数返回值类型相同的局部变量,由编译器维护,专门用于存储返回

//值。如果函数为void型,则无此变量。

 [7] int32[] CS$00000007$00000001,

    file://局部变量,存储数组引用,用于foreach循环。本例中对应‘array’数组。

  [8] int32 CS$00000008$00000002

    file://局部变量,存储数组索引。专用于foreach循环,由编译器维护。

  )

  IL_0000:  ldc.i4.3

  IL_0001:  stloc.0

  IL_0002:  ldc.i4.s  9

  IL_0004:  stloc.1

  IL_0005:  ldc.i4.0

  IL_0006:  stloc.2

  IL_0007:  ldc.i4.0

  IL_0008:  stloc.3

  IL_0009:  br.s  IL_0013

  IL_000b:  ldloc.2

  IL_000c:  ldloc.3

  IL_000d:  add

  IL_000e:  stloc.2

  IL_000f:  ldloc.3

  IL_0010:  ldc.i4.1

  IL_0011:  add

  IL_0012:  stloc.3

  IL_0013:  ldloc.3

  IL_0014:  ldloc.0

  IL_0015:  ble.s  IL_000b

  IL_0017:  ldc.i4.0

  IL_0018:  stloc.3

  IL_0019:  br.s  IL_0023

  IL_001b:  ldloc.2

  IL_001c:  ldloc.3

  IL_001d:  add

  IL_001e:  stloc.2

  IL_001f:  ldloc.3

  IL_0020:  ldc.i4.1

  IL_0021:  add

  IL_0022:  stloc.3

  IL_0023:  ldloc.3

  IL_0024:  ldloc.1

  IL_0025:  blt.s  IL_001b

  IL_0027:  ldc.i4.8

  IL_0028:  newarr  [mSCOrlib]System.Int32

  file://创建长度为8的System.Int32数组。可以看出数组元素被映射到Int32类对象

 IL_002d:  dup

  IL_002e: ldtoken field valuetype ''/'$$struct0x6000002-1'

  ''::'$$method0x6000002-1'

  IL_0033: call void [mscorlib] System.Runtime.CompilerServices.RuntimeHelpers::

   InitializeArray(class[mscorlib]System.Array, valuetype  [mscorlib] System.RuntimeFieldHandle)

  IL_0038:  stloc.s  'array'

  IL_003a:  ldloc.s  'array'

  IL_003c:  stloc.s  CS$00000007$00000001

  IL_003e:  ldc.i4.0

  IL_003f:  stloc.s  CS$00000008$00000002

  IL_0041:  br.s  IL_0055

  IL_0043:  ldloc.s  CS$00000007$00000001

  IL_0045:  ldloc.s  CS$00000008$00000002

  IL_0047:  ldelem.i4

  IL_0048:  stloc.s  a

  IL_004a:  ldloc.2

  IL_004b:  ldloc.s  a

  IL_004d:  add

  IL_004e:  stloc.2

  IL_004f:  ldloc.s  CS$00000008$00000002

  IL_0051:  ldc.i4.1

  IL_0052:  add

  IL_0053:  stloc.s  CS$00000008$00000002

  IL_0055:  ldloc.s  CS$00000008$00000002

  IL_0057:  ldloc.s  CS$00000007$00000001

  IL_0059:  ldlen

  IL_005a:  conv.i4

  IL_005b:  blt.s  IL_0043

  IL_005d:  ldloc.2

  IL_005e:  stloc.s  CS$00000003$00000000

  IL_0060:  br.s  IL_0062

  IL_0062:  ldloc.s  CS$00000003$00000000

  IL_0064:  ret

} // end of method Advanced::LoopTest

关于函数话题如.locals init语句等,请参见文章〈函数相关〉。这里我对其中的一些指令做出解释,主要是与本文相关的条件转移指令(b*.s)等。其他指令以后我会作适当的介绍。如下所示:

  指令

  意义

  记忆方法(*)

  br.s

绝对跳转,相当于jmp

 

  blt.s

小于转

  Lower Than

  ble.s

小于等于转

  Lower or Equals

  ldlen

取得数组长度

 

  ldelem.i4

根据索引取得数组项

 

 

这里我们可以看到 .locals init伪指令给出了同源程序相同变量名称。这是因为在反汇编时,相同目录下有调试信息文件(*.pdb),否则的我们看到的结果变量以V_x形式(如V_1、V_2等)表示。有关函数局部变量的话题,请参见《函数相关》一文。

  如果你有win32汇编程序设计经验,可能都熟悉怎样实现循环控制。如,要实现从10加至100的功能,我们可能会这样做:

  mov  ecx,  100  file://ecx寄存器存放循环计数

  xor  eax,  eax  file://给eax和标志寄存器清零

loop:  add  eax,  ecx  file://实现相加并将结果存eax

  dec  ecx  file://计数减一

cpr:  cmp  ecx,  9  file://判断 ecx>=10 或 ecx>9

  jg  loop  file://如果判断结果为真(大于)的话,则转loop

这跟高级语言(C/C++/Java/C#)不一样,for循环中的循环条件在程序首部给出,而顺序执行的低级语言如MASM都习惯是在循环末尾测试循环条件的。那么C#编译器又是怎样处理C#循环条件位置与一般汇编中循环条件测试语句位置的不一致,用IL来实现循环条件检测并正确实现循环的呢?首先,在这里我要说明,在顺序执行的汇编语言中,测试循环条件是完全可以放在循环首部的。如上例的IL版为:

  .locals init([0] int32 eax, [1] int32 ecx,[2] int32 RET_VAL)

  ldc.i4 100

  stloc.1  file://mov ecx, 100

  ldc.i4.0

  stloc.0  file://xor eax, eax 或 move eax, 0

L_0000:  ldloc.1

  ldc.i4 10    file://

  blt.s  L_0003  // ecx < 10 ? Yes-> jmp L_0001 :No -> go on

L_0001:  ldloc.0

  ldloc.1

  add

  stloc.0  file://这几句实现 eax=eax+ecx

  ldloc.1

  ldc.i4.1

  sub 

  stloc.1  file://这几句实现 ecx=ecx-1

L_0002:  br.s  L_0000

L_0003:  …

其次,我要说明不这样做的理由。理由有二,其一是破坏了正常逻辑,这一点是从编译器层面上来说的。比如,对于语句if(k=j)的比较,如真则向下跳出循环区域,如假则继续执行;在循环的末尾还要设置绝对跳转语句,以跳转到首部的比较指令处。由(k=j)转变,对于我们人来说是很简单、直观的事情,可对编译器来说还要做更多的工作才能实现。更何况还有更复杂的布尔表达式呢,如(k>j)&& (k>34)  || (j<=56)。这就增加了编译器实现的负担——虽然不是很大的负担。而且,因为还增加了跳转语句,给编译器对跳转位置的定位增加了难度。大家知道,汇编器在处理、计算汇编语言中的标号与跳转指令的偏移量时要进行至少两次的扫描,高级语言就更复杂了。因此,采用前一种方法既容易理解,又容易实现。

  下面我们来看看例子中三种循环的具体实现。有关IL代码的基本运行机制,请参看《IL代码底层运行机制》一文。IL_0027到IL_003f是进行数组初始化的,比较难懂一点。我们暂且放下,以后我还会介绍。

1.  for语句

可以看出,程序段中IL_0000到IL_0008是执行变量初始化工作的。从IL_0009开始,就是循环体了。IL_00009是一条直接(绝对)跳转语句,跳转到IL00_13。我们看看这里的内容:

IL_0013:  ldloc.3

   IL_0014:  ldloc.0

   IL_0015:  ble.s  IL_000b

加载局部变量3(也即k),再加载局部变量0(即j)。后面是一条比较转移指令ble.s。不难看出,这三条语句用于比较k与j的大小。如果比较结果为真(小于等于),则转入循环体内(IL_000b处),为假则继续执行直接出循环体。过程如行云流水,简洁直观,不多作解释。从这里我们也可以看出,for语句是先进行条件测试,后执行循环体的。

 

 ...  ldloc.3    ldloc.0   ble.s

 

top

 

 

  top

  k

 …

 

  top

  j

  k

 …

 

 

  top

 …

 

 

   

 

 

 

 load指令将变量逐个加至程序栈。ble.s指令进行比较。值得我们注意的是,ble.s还要进行清栈操作。不仅是ble.s,其他条件转移指令也都是如此。

2.  foreach语句

foreach语句和for语句处理过程大致相当。我们感兴趣的是foreach怎样处理边界条件。从IL0041开始,就进入了foreach循环体。同样一条直接跳转指令把我们带到了IL_0055处,让我们看看这里是什么。

IL_0055:  ldloc.s  CS$00000008$00000002

     IL_0057:  ldloc.s  CS$00000007$00000001

  IL_0059:  ldlen

    IL_005a:  conv.i4

   IL_005b:  blt.s  IL_0043

前面我介绍过CS$00000008$00000002是存储数组索引的,CS$00000007$00000001是数组‘array’的引用。IL0055到IL005b的过程操作是这样的:首先向程序栈加载当前索引,再加载数组引用(32位的HashCode)。ldlen指令根据数组引用取得数组长度(64位长整型)并将之转换成32为整型,将索引与此长度进行比较。如果小于,则转入循环体继续执行;否则出循环。从这里我们也可以看出,IL对数组操作给予了很强的支持,直接为它提供了相应的指令。

3.  while语句和do-while语句

从例子中可以看出,while和for循环处理方式是一样的。这里没有给出do-while例子,但是可以想见它跟for语句处理是一样的。但是,do-while循环要注意,在其循环首部没有像for和foreach循环那样的直接跳转指令跳转到条件测试代码处。因此,不管什么情况,do-while循环都是至少执行一次的。

 

  在这篇文章中,我介绍了几条有关条件跳转指令,以及C#编译器是怎样处理C#语言中的循环的。其实,本文不能完全算是IL底层机制相关文章,但是要深入了解IL,这点基础还是必要的。


来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/10752043/viewspace-963560/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/10752043/viewspace-963560/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值