中间语言IL.docx
- 文档编号:12270448
- 上传时间:2023-04-17
- 格式:DOCX
- 页数:38
- 大小:37.02KB
中间语言IL.docx
《中间语言IL.docx》由会员分享,可在线阅读,更多相关《中间语言IL.docx(38页珍藏版)》请在冰豆网上搜索。
中间语言IL
中间语言(IL)
分类:
.NETFramework2009-10-2519:
241211人阅读评论(0)收藏举报
一、IL与汇编语言
IL是微软.NET平台上衍生出的一门中间语言,.NET平台上的各种高级语言(如C#,VB,F#)的编译器会将各自的代码转化为IL。
,其中包含了.NET平台上的各种元素,如“范型”,“类”、、“接口”、“模块”、“属性”等等。
值得注意的是,各种高级语言本身可能根本没有这些“概念”在里头,如IronScheme是一个在.NET平台上的Scheme语言实现,其中根本没有前面提到的这些IL——亦或说是.NET平台上的名词。
IL本身并不知道自己是由哪种高级语言转化而来的,哪种语言中有哪些特性,IL也根本不会关心。
各种语言的编译器将:
高级语言=>IL。
汇编是让CPU直接使用的“语言”,请注意“直接”二字:
一条汇编指令便是让CPU作一件事情(如寄存器的复制,从内存中读取数据等等),毫无二义。
不同族CPU拥有不同的指令集,但是它们都有一样的特征:
指令的数量相对较少,每个指令功能都简单之至。
由于CPU只认识汇编代码(机器码和汇编其实也是一一对应的,您可以这样理解:
汇编是机器码的文字表现形式,提供了一些方便人们记忆的“助记符”),因此就算是IL也需要再次进行转化,才能被CPU执行。
这次转化便由“JITCompiler”(即时编译器)完成。
CLR加载了IL之后,当每个方法——请注意这是IL中的概念——第一次被执行时,就会使用JIT将IL代码进行编译为机器码。
与IL不同的是,CLR,JIT都是真正了解CPU的,对于同样的IL,JIT会把它为不同的CPU架构(如x86/IA64等等)生成不同的机器码。
这也是Java/.NET中“CompileOnce,RunEverywhere”这一口号的技术基础:
它们为不同的CPU架构提供了不同的“IL转化器”,仅此而已。
与高级语言到IL的转化类似,CPU也完全不知道自己在执行的指令是从哪里来的,可能是JIT从IL转化而来,可能是JVM从JavaBytecode转化而来,也有可能是C语言编译得来,也有可能是由MIT/GNUScheme解释而来。
这就是.NET平台上的高级语言在机器上运行的第二次转化:
IL=>汇编(机器码)。
因此,IL和汇编的区别是显著的。
IL拥有各种高级特性,它知道什么是范型,什么是类和方法(以及它们的“名称”),什么是继承,什么是字符串,布尔值,什么是User对象。
而CPU只知道寄存器,地址,内存,01010101。
与汇编相比,IL简直太高级了,几乎完全是一个高级语言,比C语言还要高级。
因此,您会看到.NETReflector几乎可以把IL代码“一五一十”地反编译为可读性良好的C#代码,包括类,属性,方法等等;而从汇编只能勉勉强强地反编译为C语言——而且其中的“方法名”等信息已经完全不可恢复了,更别说“模块”等高级抽象的内容。
您想要把汇编反编译成C#代码?
相信在将来这是可行的,不过现在这还是天方夜谭。
二、IL并不是万能的,CLR还有很多内容IL都无法看到
示例一:
探究泛型在某些情况下的性能问题
namespaceTestConsole
{
publicclassMyArrayList
{
publicMyArrayList(intlength)
{
this.m_items=newobject[length];
}
privateobject[]m_items;
publicobjectthis[intindex]
{
[MethodImpl(MethodImplOptions.NoInlining)]
get
{
returnthis.m_items[index];
}
[MethodImpl(MethodImplOptions.NoInlining)]
set
{
this.m_items[index]=value;
}
}
}
publicclassMyList
{
publicMyList(intlength)
{
this.m_items=newT[length];
}
privateT[]m_items;
publicTthis[intindex]
{
[MethodImpl(MethodImplOptions.NoInlining)]
get
{
returnthis.m_items[index];
}
[MethodImpl(MethodImplOptions.NoInlining)]
set
{
this.m_items[index]=value;
}
}
}
classProgram
{
staticvoidMain(string[]args)
{
MyArrayListarrayList=newMyArrayList
(1);
arrayList[0]=arrayList[0]?
?
newobject();
MyList
(1);
list[0]=list[0]?
?
newobject();
Console.WriteLine("Herecomesthetestingcode.");
vara=arrayList[0];
varb=list[0];
Console.ReadLine();
}
}
}
示例目的是证明“.NET中,就算在使用Object作为泛型类型的时候,也不会比直接使用Object类型性能差”。
类MyList泛型容器,类MyArrayList直接使用Object类型的容器。
在Main方法中将对MyList
至此,便出现了一些疑问,为泛型容器使用Object类型,是否比直接使用Object类型性能要差?
看MyArrayList.get_Item和MyList
//MyArrayList的get_Item方法
.methodpublichidebysigspecialnameinstanceobjectget_Item(int32index)cilmanagednoinlining
{
.maxstack8
L_0000:
ldarg.0
L_0001:
ldfldobject[]TestConsole.MyArrayList:
:
m_items
L_0006:
ldarg.1
L_0007:
ldelem.ref
L_0008:
ret
}
//MyList
.methodpublichidebysigspecialnameinstance!
Tget_Item(int32index)cilmanagednoinlining
{
.maxstack8
L_0000:
ldarg.0
L_0001:
ldfld!
0[]TestConsole.MyList`1:
:
m_items
L_0006:
ldarg.1
L_0007:
ldelem.any!
T
L_000c:
ret
}
这两个方法的区别只在于红色的两句。
我们“默认”ldfld指令的功能在两段代码中产生的效果完全相同(毕竟是相同的指令嘛),但是您觉得ldelem.ref指令和ldelem.any两条指令的效果如何,它们是一样的吗?
我们通过查阅一些资料可以了解到说,ldelem.any的作用是加载一个泛型向量或数组中的元素。
不过它的性能如何?
您能得出结果说,它就和ldelem.ref指令一样吗?
除非您了解到JIT对待这两个指令的具体方式,否则您是无法得出其中性能高低的。
因为IL还是过于高级,您看到了一条IL指令,您可以知道它的作用,但是您还是不知道它最终造成了何种结果。
您还是无法证明“Object泛型集合的性能不会低于直接存放Object的非泛型集合”。
因此,比较MyArrayList.get_Item方法和MyList
由于汇编代码和机器代码一一对应,因此观察汇编代码就可以完全了解CPU是如何执行这两个方法的。
汇编代码一模一样,就意味着CPU对待这两个方法的方式一模一样,它们的性能怎么会有不同呢?
结论:
.NET的Object泛型容器的性能不会低于直接使用Object的容器,因为CLR在处理Object泛型的时候,会生成与直接使用Object类型时一模一样的类型,因此性能是不会降低的。
但是您是通过学习IL可以了解这些吗?
显然不是,如果您只是学习了IL,最终还是要“听别人说”才能知道这些,而即使您不学IL,在“听别人说”了之后您也了解了这些——同时也不会因为不了解IL而变得“易忘”等等。
同样道理,IL的call指令和callvirt指令的区别是什么呢?
“别人会告诉你”call指令直接就去调用了那个方法,而callvirt还需要去虚方法表里去“寻找”那个真正的方法;“别人可能还会告诉你”,查找虚方法是靠方法表地址加偏移量;《Essential.NET》还会将方法表的实现结构告诉给你,而这些都是IL不会告诉您的。
您就算了解再多IL,也不如“别人告诉你”的这些来得重要。
您要了解“别人告诉你”的东西,也不需要了解多少IL。
示例二:
只有经过调用的方法才能获得其汇编代码吗?
许多资料都告诉我们,在一个方法被第一次调用之前,它是不会被JIT的。
也就是说,直到第一次调用时它才会被转化为机器码。
不过,这个真是这样吗?
我们还是准备一段简单的C#代码:
namespaceTestConsole
{
classProgram
{
[MethodImpl(MethodImplOptions.NoInlining)]
privatestaticvoidSomeMethod()
{
Console.WriteLine("HelloWorld!
");
}
staticvoidMain(string[]args)
{
Console.WriteLine("BeforeJITed.");
Console.ReadLine();
SomeMethod();
Console.WriteLine("AfterJITed");
Console.ReadLine();
}
}
}
那么Main方法的IL代码是怎么样的呢?
.methodprivatehidebysigstaticvoidMain(string[]args)cilmanaged
{
.entrypoint
.maxstack8
//分配字符串"BeforeJITed"
L_0000:
ldstr"BeforeJITed."
//调用Console.WriteLine方法
L_0005:
callvoid[mscorlib]System.Console:
:
WriteLine(string)
//调用Console.ReadLine方法
L_000a:
callstring[mscorlib]System.Console:
:
ReadLine()
L_000f:
pop
//调用Program.SomeMethod方法
L_0010:
callvoidTestConsole.Program:
:
SomeMethod()
//分配字符串"AfterJITed"
L_0015:
ldstr"AfterJITed"
//调用Console.WriteLine方法
L_001a:
callvoid[mscorlib]System.Console:
:
WriteLine(string)
//调用Console.ReadLine方法
L_001f:
callstring[mscorlib]System.Console:
:
ReadLine()
L_0024:
pop
L_0025:
ret
}
IL代码多容易懂呀,这段IL代码基本上就和我们的C#一样。
没错,这就是IL的作用。
IL和C#一样,都是用于表现程序逻辑。
C#使用if...else、while、for等等丰富语法,而在IL中就会变成判断+跳转语句。
但是,您从一段几十行的IL语句中,看出一句十几行的while逻辑——收获在哪里?
除此之外,C#分配一个变量,IL也分配一个。
C#调用一个方法,IL就call或callvirt一下。
C#里new一个,IL中就newobj一下(自然也会有一些特殊,例如可以使用jmp或tailcall一个方法——是为尾递归,但也只是及其特殊的情况)。
可以发现IL的功能大部分就是C#可以表现的功能。
而C#隐藏掉的一些细节,在IL这里同样没有显示出来!
那么我们又该如何发现一些细节呢?
例如“书本”告诉我们的JIT的工作方式:
方法第一次调用之后才会生成机器码。
这段程序会打印三行文字,在打印出BeforeJITed和AfterJITed字样之后都会有一次停止,需要用户按回车之后才能继续。
在进行试验的时候,您可以在程序暂停的时候使用WinDbg的File-AttachtoProcess命令附加到TestConsole.exe进程中,或者在两次暂停时各生成一个dump文件,这样便可不断地重现一些过程。
否则的话,应用程序两次启动所生成的地址很可能会完全不同——因为JIT的工作是动态的,有时候很难提前把握。
好,我们已经进入了第一个Console.ReadLine暂停,在点击回车继续下去之前。
我们先使用WinDbg进行调试。
以下是Main方法的汇编代码:
0:
000>!
name2ee*!
TestConsole.Program
Module:
70f61000(mscorlib.dll)
--------------------------------------
Module:
00172c5c(TestConsole.exe)
Token:
0x02000002
MethodTable:
00173010
EEClass:
001712d0
Name:
TestConsole.Program
0:
000>!
dumpmt-md00173010
EEClass:
001712d0
Module:
00172c5c
Name:
TestConsole.Program
mdToken:
02000002(.../bin/Release/TestConsole.exe)
BaseSize:
0xc
ComponentSize:
0x0
NumberofIFacesinIFaceMap:
0
SlotsinVTable:
7
--------------------------------------
MethodDescTable
EntryMethodDescJITName
71126ab070fa4944PreJITSystem.Object.ToString()
71126ad070fa494cPreJITSystem.Object.Equals(System.Object)
71126b4070fa497cPreJITSystem.Object.GetHashCode()
7119754070fa49a0PreJITSystem.Object.Finalize()
0017c01900173008NONETestConsole.Program..ctor()
0017c01100172ff0NONETestConsole.Program.SomeMethod()
003e007000172ffcJITTestConsole.Program.Main(System.String[])
0:
000>!
u003e0070
NormalJITgeneratedcode
TestConsole.Program.Main(System.String[])
Begin003e0070,size4d
>>>003e007055pushebp
003e00718becmovebp,esp
***WARNING:
Unabletoverifychecksumformscorlib.ni.dll
003e0073e8a8d3da70callmscorlib_ni+0x22d420(7118d420)(System.Console.get_Out(),...)
003e00788bc8movecx,eax
003e007a8b153020d102movedx,dwordptrds:
[2D12030h]("BeforeJITed.")
003e00808b01moveax,dwordptr[ecx]
003e0082ff90d8000000calldwordptr[eax+0D8h]
003e0088e8971b2571callmscorlib_ni+0x6d1c24(71631c24)(System.Console.get_In(),...)
003e008d8bc8movecx,eax
003e008f8b01moveax,dwordptr[ecx]
003e0091ff5064calldwordptr[eax+64h]
003e0094ff15f82f1700calldwordptrds:
[172FF8h](TestConsole.Program.SomeMethod(),...)
003e009ae881d3da70callmscorlib_ni+0x22d420(7118d420)(System.Console.get_Out(),...)
003e009f8bc8movecx,eax
003e00a18b153420d102movedx,dwordptrds:
[2D12034h]("AfterJITed")
003e00a78b01moveax,dwordptr[ecx]
003e00a9ff90d8000000calldwordptr[eax+0D8h]
003e00afe8701b2571callmscorlib_ni+0x6d1c24(71631c24)(System.Console.get_In(),...)
003e00b48bc8movecx,eax
003e00b68b01moveax,dwordptr[ecx]
003e00b8ff5064calldwordptr[eax+64h]
003e00bb5dpopebp
003e00bcc3ret
请关注上面那个被标红的call语句,它的含义是:
先从读取172FF8地址中的值,这才是方法调用的目标地址(即SomeMethod方法)。
使用call指令调用刚才读取到的目标地址
那么在第一次调用SomeMethod方法之前,目标地址的指令是什么呢?
0:
000>dd172FF8
00172ff80017c0117103000200200006003e0070
001730080006000300000004000000000000000c
001730180005001100000004711d077000172c5c
001730280017304c001712d00000000000000000
0017303871126ab071126ad071126b4071197540
001730480017c019000000800000000000000000
0017305800000000000000000000000000000000
0017306800000000000000000000000000000000
0:
000>!
u0017c011
Unmanagedcode
0017c011b000moval,0
0017c013eb08jmp0017c01d
0017c015b003moval,3
0017c017eb04jmp0017c01d
0017c019b006moval,6
0017c01beb00jmp0017c01d
0017c01d0fb6c0movzxeax,al
0017c020c1e002shleax,2
0017c02305f02f1700addeax,172FF0h
0017c028e9d7478c00jmp00a40804
这是什么,不像是SomeMethod的内容阿,SomeMethod是会调用Console.WriteLine方法的,怎么变成了一些跳转了呢?
于是我们想起书本(例如《CLRviaC#》)中的话来,在方法第一次调用时,将会跳转到JIT的指令处,对方法的IL代码进行编译。
再想想书中的示意图,于是恍然大悟,原来这段代码的作用是“让JIT编译IL”啊。
那么在JIT后,同样的调用会产生什么结果呢?
我们在WinDbg中Debug-DetachDebuggee,让程序继续运行。
单击回车,您会发现屏幕上出现了HelloWord和AfterJIT的字样。
于是我们继续AttachtoProcess,重复上面的命令。
由于Main方法已经被编译好了,它的汇编代码不会改变,因此在调用SomeMethod方法时的步骤还是不变:
先去内存172FF8中读取目标地址,再call至目标地址。
0:
000>dd172FF8
00172ff8003e00d07103000200200006003e0070
001730080006000300000004000000000000000c
001730180005001100000004711d077000172c5c
001730280017304c001712d00000000000000000
0017303871126ab071126ad071126b4071197540
001730480017c019000000800000000000000000
0017305800000000000000000000000000000000
0017306800000000000000000000000000000000
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 中间 语言 IL
![提示](https://static.bdocx.com/images/bang_tan.gif)