浅谈C#转IL语言

前言

突发奇想,想研究下IL语言
本来以为会挺难的,结果发现貌似还行
注意:本博客不打算详细讲解IL的原理之类的
只是大概讲讲我研究的成果
这里推荐两篇博客,他们那讲的更详细
一个是来自Zery的IL指令详细
另一个是布鲁克石的30分钟?不需要,轻松读懂IL

<0x01> 什么是IL语言

这就要从.net的运行结构开始讲起
.net框架下面有好多们语言,但.net的运行时就一个
所以这个运行时怎么能同时跑这么多的语言,是每个语言一个实现吗?
肯定不是,这太麻烦了
所以.net编译器会将它托管的语言先编译成一个中间语言,这个中间语言就是IL
.net运行时会动态解释这个IL文件(这个跟Java差不多的流程)

通过IL语言,.net实现了多语言共用一个运行时,也让不同语言能非常方便地整合进一个项目
(反正编译出来的东西都是一样的)
这个具体的过程可以看本叫《CLR via C#》(俗称C#圣经😂,最近在看)
里面讲到过

<0x02> 怎么查看IL语言

第一种方法,ILDasm
Visual 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#编译有两种模式
DebugRelease
前者编译出的代码并不是最优化的,里面会有很多的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#》讲的很详细

Licensed under CC BY-NC-SA 4.0