前言
突发奇想,想研究下IL语言
本来以为会挺难的,结果发现貌似还行
注意:本博客不打算详细讲解IL的原理之类的
只是大概讲讲我研究的成果
这里推荐两篇博客,他们那讲的更详细
一个是来自Zery的IL指令详细
另一个是布鲁克石的30分钟?不需要,轻松读懂IL
<0x01> 什么是IL语言
这就要从.net的运行结构开始讲起.net框架下面有好多们语言,但.net的运行时就一个
所以这个运行时怎么能同时跑这么多的语言,是每个语言一个实现吗?
肯定不是,这太麻烦了
所以.net编译器会将它托管的语言先编译成一个中间语言,这个中间语言就是IL.net运行时会动态解释这个IL文件(这个跟Java差不多的流程)
通过IL语言,.net实现了多语言共用一个运行时,也让不同语言能非常方便地整合进一个项目
(反正编译出来的东西都是一样的)
这个具体的过程可以看本叫《CLR via C#》(俗称C#圣经😂,最近在看)
里面讲到过
<0x02> 怎么查看IL语言
第一种方法,ILDasmVisual Studio自带的反汇编软件(但我没找到)
参考微软的文档就好
第二种方法,sharplab.io
这是个给C#用的实时转换成IL的网页工具
这个还可以转换成编译器优化后的代码甚至是给CLR执行的汇编代码
在线执行也是可以的,而且它可以列出所写代码的所有语法点并列出微软的说明文档链接
不过毕竟是网页工具,并不完全支持所有的FCL库,所以可能有些库用不了
总之这是个非常方便的工具,有什么小测试都可以在上面测试
第三种方法,ILSpy
这个工具也挺好用的,有支持Visual Studio的插件
装上就可以写代码的时候直接反编译
(@ 23-08-22)
只是看核心库源代码的话还有一种办法,官方的源代码网站
具体我没怎么用过,就先补充在这里吧
我现在在用第二种和第三种方法
平时做小测试网页工具用用就好了
但在研究.net的底层api的时候就需要用ILSpy了
(虽然.net core开源了,但自己找效率确实低,反编译哪里不会点哪里)
<0x03> C#的编译过程
C#编译到IL分两步
第一步:编译器优化代码
第二步:编译成IL
在第一步中,编译器将对原代码进行彻头彻尾的改造
比方说代码有一句int a = 5;,在优化过程中会改名为int num = 5;
还有就是将一些编译期就知道结果的值计算出来,比如"a"+"b"就会直接优化成"ab"
在这个过程中,编译器还会加上非常多的,用于跟CLR通信的指令
这些指令主要是指导CLR正确执行代码,具体我也不是很熟,以后再说
第二步编译成IL就看靠编译器了
这两步具体实现可以看Roslyn的源码,在GitHub上
(我还没开始研究这玩意)
另外还有就是C#编译有两种模式Debug和Release
前者编译出的代码并不是最优化的,里面会有很多的nop指令
(就是执行这条指令啥也不干,方便调试打断点用的)
后者是用来发布的,编译出来的代码是经过编译器完全优化的
<0x04> 开始研究IL
首先先是一段简单的代码
using System;
public class C {
public void M() {
int a=3;
string str="abc";
Console.WriteLine(a+str);
}
}
看看它编译后的IL语言(Debug下编译)
.assembly _
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = (
01 00 08 00 00 00 00 00
)
.custom instance void [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = (
01 00 01 00 54 02 16 57 72 61 70 4e 6f 6e 45 78
63 65 70 74 69 6f 6e 54 68 72 6f 77 73 01
)
.custom instance void [System.Runtime]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggableAttribute/DebuggingModes) = (
01 00 07 01 00 00 00 00
)
.permissionset reqmin = (
2e 01 80 8a 53 79 73 74 65 6d 2e 53 65 63 75 72
69 74 79 2e 50 65 72 6d 69 73 73 69 6f 6e 73 2e
53 65 63 75 72 69 74 79 50 65 72 6d 69 73 73 69
6f 6e 41 74 74 72 69 62 75 74 65 2c 20 53 79 73
74 65 6d 2e 52 75 6e 74 69 6d 65 2c 20 56 65 72
73 69 6f 6e 3d 37 2e 30 2e 30 2e 30 2c 20 43 75
6c 74 75 72 65 3d 6e 65 75 74 72 61 6c 2c 20 50
75 62 6c 69 63 4b 65 79 54 6f 6b 65 6e 3d 62 30
33 66 35 66 37 66 31 31 64 35 30 61 33 61 15 01
54 02 10 53 6b 69 70 56 65 72 69 66 69 63 61 74
69 6f 6e 01
)
.hash algorithm 0x00008004 // SHA1
.ver 0:0:0:0
}
.class private auto ansi '<Module>'
{
} // end of class <Module>
.class public auto ansi beforefieldinit C
extends [System.Runtime]System.Object
{
// Methods
.method public hidebysig
instance void M () cil managed
{
// Method begins at RVA 0x206c
// Code size 29 (0x1d)
.maxstack 2
.locals init (
[0] int32 a,
[1] string str
)
IL_0000: nop
IL_0001: ldc.i4.3
IL_0002: stloc.0
IL_0003: ldstr "abc"
IL_0008: stloc.1
IL_0009: ldloca.s 0
IL_000b: call instance string [System.Runtime]System.Int32::ToString()
IL_0010: ldloc.1
IL_0011: call string [System.Runtime]System.String::Concat(string, string)
IL_0016: call void [System.Console]System.Console::WriteLine(string)
IL_001b: nop
IL_001c: ret
} // end of method C::M
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x2095
// Code size 8 (0x8)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
} // end of method C::.ctor
} // end of class C
.class private auto ansi sealed beforefieldinit Microsoft.CodeAnalysis.EmbeddedAttribute
extends [System.Runtime]System.Attribute
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
)
.custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = (
01 00 00 00
)
// Methods
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x2050
// Code size 8 (0x8)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
IL_0006: nop
IL_0007: ret
} // end of method EmbeddedAttribute::.ctor
} // end of class Microsoft.CodeAnalysis.EmbeddedAttribute
.class private auto ansi sealed beforefieldinit System.Runtime.CompilerServices.RefSafetyRulesAttribute
extends [System.Runtime]System.Attribute
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
)
.custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = (
01 00 00 00
)
.custom instance void [System.Runtime]System.AttributeUsageAttribute::.ctor(valuetype [System.Runtime]System.AttributeTargets) = (
01 00 02 00 00 00 02 00 54 02 0d 41 6c 6c 6f 77
4d 75 6c 74 69 70 6c 65 00 54 02 09 49 6e 68 65
72 69 74 65 64 00
)
// Fields
.field public initonly int32 Version
// Methods
.method public hidebysig specialname rtspecialname
instance void .ctor (
int32 ''
) cil managed
{
// Method begins at RVA 0x2059
// Code size 15 (0xf)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
IL_0006: nop
IL_0007: ldarg.0
IL_0008: ldarg.1
IL_0009: stfld int32 System.Runtime.CompilerServices.RefSafetyRulesAttribute::Version
IL_000e: ret
} // end of method RefSafetyRulesAttribute::.ctor
} // end of class System.Runtime.CompilerServices.RefSafetyRulesAttribute
(注释是Sharpio自动生成的)
额,看着是有点多,但其实很多只是自动生成的代码
如果只关注我们自己方法实现的话,实际只要看这么点就好
.method public hidebysig instance void M () cil managed //方法签名
{
.maxstack 2 //定义计算栈大小
.locals init (
[0] int32 a,
[1] string str
) //初始化变量链表
IL_0000: nop //啥也不干(Debug下编译)
IL_0001: ldc.i4.3 //初始化int32的数值3,并加载到计算栈上
IL_0002: stloc.0 //弹出一个栈顶元素,并存储到变量列表的0号位上(就是变量a)
IL_0003: ldstr "abc" //初始化字符串"abc",并加载到计算栈上
IL_0008: stloc.1 //弹出栈顶元素,存储到变量链表1号上(变量str)
IL_0009: ldloca.s 0 //加载变量列表0号位(变量a)到计算栈上
IL_000b: call instance string [System.Runtime]System.Int32::ToString()
//调用方法
IL_0010: ldloc.1 //加载变量列表1号位(变量str)到计算栈上
IL_0011: call string [System.Runtime]System.String::Concat(string, string)
//调用方法,有几个参就弹出几个栈顶元素,返回值再压入栈
IL_0016: call void [System.Console]System.Console::WriteLine(string)
//调用方法
IL_001b: nop //啥也不干
IL_001c: ret //方法结束,返回
}
(已经打上了我自己的注释了)
所以其实也不是挺难的,就是IL的指令差不多都是简写,乍一看确实不明觉厉
(所以会看IL就可以出去装逼了😅)
IL还有很多指令,具体可以看我最上面推荐的两篇博客
这里的计算栈其实就是个可以放任何类型的栈,计算用的
具体的CLR执行细节可以看看《CLR via C#》讲的很详细