分享
 
 
 

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

王朝other·作者佚名  2006-01-30
窄屏简体版  字體: |||超大  

IL代码底层运行机制之

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<j)

{

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 '<PrivateImplementationDetails>'/'$$struct0x6000002-1'

'<PrivateImplementationDetails>'::'$$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<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,这点基础还是必要的。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有