CIL 中是如何进行数值加法操作的

C# 是一门简单易学的语言,其背后的 CIL 更是博大精深。某些在 C# 层面很难理解的语法,比如装箱、拆箱所带来的 GC 开销,值类型和引用类型的区别,类型强转等等问题,我们从 CIL 的角度去思考,能够更好的理解其背后的原理。
我们用一个最简单的 Add 操作来了解一下 CIL 的指令。

using System;

namespace boxing
{
    class Program
    {
        static void Main(string[] args)
        {
            int n = 145;
            int x = 9 + n;
        }
    }
}

我们可以用任何 DLL 反编译工具,得到下面的 CIL 代码:

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 13 (0xd)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] int32,
        [1] int32
    )

    IL_0000: nop
    IL_0001: ldc.i4 145
    IL_0006: stloc.0
    IL_0007: ldc.i4.s 9
    IL_0009: ldloc.0
    IL_000a: add
    IL_000b: stloc.1
    IL_000c: ret
} // end of method Program::Main

CIL 类似于汇编语言,是有一条一条的指令构成的,比如 .method 就是用来声明一个函数。
.maxstack 是用来声明需要为这个 Main 函数保留多大的栈。比如我们执行 int y = 1 + 2; 这样的操作,需要先把 1 推到栈上,然后把 2 推到栈上,在执行 add 操作,这样我们的栈就至少需要 2 个空间,所以这个时候就需要声明为 .maxstack 2

.locals init (
        [0] int32,
        [1] int32
    )

.locals init 用来声明并初始化局部变量,这些变量在后面的 add 操作中会被加载和使用。
ldc、stloc 等,这些都是一些操作指令,关于这些指令是什么意思,以及是哪些单词的缩写,请参考下面这张表格。
比如 ldc.i4 145 = ld(load) + c(const) + i4(int 4 bytes) + 145,这样我们就可以很容易的理解这条指令了:把一个4字节的整数常量145加载到栈上。
+ ld = load ,相当于汇编中的 push,把值加载到栈上;
+ i4 = int32, i8 = int64
+ r4 = real32, r8 = real64
+ .0 = 第0个参数,类似的有 .1, .2 等等
+ .s = short
+ st = store, 相当于汇编中的 pop, 把栈内容出栈,并存储到操作数中;
+ ret = return
+ loc = local

更多的指令请参考 CIL Standard

理解了上面的指令,我们就可以很好的理解整个 Main 函数 CIL 代码的执行流程了:

  1. ldc.i4 145

    加载 int32 的常量 145 到栈上;

  2. stloc.0

    把栈顶的145出栈,并存储到局部变量索引为0的位置;

  3. ldc.i4.s 9

    加载 short 类型的常量 9 到栈上;

  4. ldloc.0

    把索引为0的位置的局部变量加载到栈上;

  5. add

    进行加法操作,add 需要两个操作数,分别从栈上获取并出栈,并把计算结果推入到栈顶;

  6. stloc.1

    把栈顶的计算结果弹出,并存入到索引为1的局部变量上;

  7. ret

    函数 return;

这样我们就理解了 CIL 是如何进行加法操作的。