【小白学C#】浅谈.NET中的IL代码

一、前言

  前几天群里有位水友提问:”C#中,当一个方法所传入的参数是一个静态字段的时候,程序是直接到静态字段拿数据还是从复制的函数栈中拿数据“。其实很明显,这和方法参数的传递方式有关,如果是引用传递的话,肯定是会去静态字段直接拿值的;如果方法是以传值的方式使用参数的话,一定是从复制的栈中拿值的。

  但是这位水友就是不相信这个结论(后来发现,这货整一个杠精啊!天天在群里跟人抬杠~),这个时候我忽然想到了可以反向看一下C#的IL代码,来了解一下程序运行的过程。的确,当我们对运行结果有异议的时候,可以通过IL代码透过表面看本质。

二、IL简介

  在我们分析查看IL之前首先要了解下什么是IL?IL的全称是Intermediate Language (IL)即将.NET代码转化为机器语言的一个中间语言的缩写。在一定程度上,我们可以将其理解为伪汇编语言。我们在使用.NET框架中的C#、VB.NET、F#等语言的时候,编译过程并不是像C/C++一样直接编译出原生代码,而是编译成IL中间语言。通过IL中间语言这种方式,可以实现跨平台、提高程序灵活性等多种优点。

  下面我们以C#语言为例,大致了解了解一下我们的源代码是如何编译成IL语言,继而运行在电脑上面的。

  

  图1:.NET语言编译过程示意图

  上图为C#语言的编译运行过程示意图。首先编译器将我们编写好的源代码编译成IL中间语言,这些IL中间语言的主要内容是一些元数据和中间语言指令。然后再由我们的JIT编译器加载这些IL中间语言,JIT编译器会根据系统环境将IL中间语言指令转换为机器码,继而运行在不同的目标平台上,实现跨平台功能。(JIT编译器将IL中间语言即时编译成原生语言的过程和解释性语言的读取一条执行一条又有些不同,JIT会对编译结果进行缓存以便下次调取的时候直接使用)这也是为什么有些ASP.NET网站第一次运行时会较慢,而后面的执行速度则会相对快很多的一个原因。

  再总结一下上面所说的编译过程:

    • 首先,编译器要编历源代码,通过大量的计算生成IL中间代码,这些代码并不能直接地被CPU使用,还需要第二步操作;
    • 接下来,运行时将这些IL代码通过JIT编译器进一步编译成原生的CPU指令。

  在上文中我们提到了一个JIT编译器,它的全名叫即时编译器。顾名思义,它是在运行时环境中发生的编译行为。读到这里,相信很多朋友可能都会像马三一样产生疑问了。相比传统的直接将源代码编译成原生代码,C#将源代码编译成了中间语言不会降低效率嘛?原来直接一步到位的过程,现在偏要拆成两个部分。这不仅要花费更多的时间、占用更多的内存,还有可能降低性能,那用JIT编译器的好处到底有什么呢?

  其实,使用JIT编译器的好处多多。如果我们不用JIT即时编译将编译后的程序放在运行时中的话,那就只能生成在在某一种CPU平台上运行的原生代码,如果程序要运行在多种目标平台上的话,就要编译多种目标平台的原生代码,这样C#也就失去了跨平台能力。其次,JIT即时编译生成原生代码发生在运行时阶段,因此即时编译器会对生成的原生代码进行性能优化。经过优化的原生代码要比不优化的代码性能好。

  在我们的Unity游戏开发中就存在着AOT编译和JIT编译两种编译方式,以后我们会单独开篇博客来详细探讨一下这两种编译方式的异同,这里就不再赘述了。

三、如何使用ILDasm工具查看IL代码

  上面说了一大堆概念和理论,相信大家早已经技痒,别急,下面,马三就和大家一起使用ILDasm工具反编译并查看IL代码。ILDasm工具一般在我们安装Visual Studio的时候就已经默认安装好了,查看IL代码通常只需下面的几步操作:

  1.首先,需要打开ILDasm工具,点击电脑桌面的开始,然后在程序中找到对应版本的Visual Studio的目录,接着在里面找到VS开发人员命令提示,英文版的名称是:Developer Command Prompt 。点击打开它,然后在命令行里面输入ILDasm并回车,就会打开ILDasm工具了。

  

  图2:VS开发人员命令提示示意图

  2.然后,在ILDasm工具的界面,点击文件-->打开,然后选择我们预先编译出来的.exe文件,ILDasm工具就会自动的帮我们分析出IL代码及其代码组织结构,如下图所示:

  

  图3:解析出来的IL代码及相关代码组织结构

   解析出来的IL代码,会以一些小图标区分标识出函数、接口等不同的部分,ILDasm中图标含义如下图所示:

  

  图4:ILDasm中图标含义

  通过上面的两步,我们就可以轻松的反编译出程序的IL代码,下面我们通过一个小案例,来解读一下IL代码的运行流程以及分析IL代码的方式。

四、浅析IL代码

  好了,现在让我们回到博客最初抛出的那个问题上面来:“C#中,当一个方法所传入的参数是一个静态字段的时候,程序是直接到静态字段拿数据还是从复制的函数栈中拿数据?”下面我们就以这个问题为例子,通过反编译出IL代码,简单地验证一下。

  首先贴出我们的C#代码,很简单,在两个方法中分别以传值和传递引用的方式传入同一个静态变量:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6
 7 namespace ILDemo
 8 {
 9
10     class Program
11     {
12         public static int n = 0;
13         static void Main(string[] args)
14         {
15             Func1(n);
16             Console.WriteLine(n);
17             Func2(ref n);
18             Console.WriteLine(n);
19         }
20
21         public static void Func1(int n)
22         {
23             n += 5;
24         }
25
26         public static void Func2(ref int n)
27         {
28             n += 5;
29         }
30     }
31
32 }

  然后我们对上面的代码进行编译,之后再按照第三节的操作,将其反编译成IL代码,以下是反编译出来的IL代码结构:

  

  图5:反编译IL代码结构

  首先让我们观察一下Func1反编译出来的IL代码,如下图所示:

  

  图6:Func1反编译出来的IL代码

  马三简单地给大家分析一下上面的IL代码的意思:

  nop:没有什么意义;

  ldarg.0:将索引为 0 的参数加载到计算堆栈上,也就是把参数 n 放到堆栈上;

  ldc.i4.5:将整数值 5 作为 int32 推送到计算堆栈上,因为我们在程序中执行了+5的操作(上面的C#代码),所以需要先把5推送到堆栈上,以便下一步进行add操作;

  add:将两个值相加并将结果推送到计算堆栈上,这里是将 n和5 相加,然后把结果推送到堆栈上;

  Starg.S n:将位于计算堆栈顶部的值存储在参数槽中的指定索引处,即将n进行存储

  ret:从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。

  接着,我们再来看看Func2反编译出来的IL代码:

  

  图7:Func2反编译出来的IL代码

  可以看到,因为我们的C#代码中使用了ref参数,所以在IL代码中将其翻译成了int32& n的形式,和C++是不是很类似?

  nop:没有什么意义;

  ldarg.0:将索引为 0 的参数加载到计算堆栈上,也就是把参数 n的地址放到堆栈上;

  ldind.i4:将 int32 类型的值作为 int32 间接加载到计算堆栈上

  ldc.i4.5:将整数值 5 作为 int32 推送到计算堆栈上,因为我们在程序中执行了+5的操作(上面的C#代码),所以需要先把5推送到堆栈上,以便下一步进行add操作;

  add:将两个值相加并将结果推送到计算堆栈上,这里是将 n和5 相加,然后把结果推送到堆栈上;

  stind.i4:在所提供的地址存储 int32 类型的值,即把我们计算出来的结果,再存储回静态变量n的地址里面;

  ret:从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。

  通过比较,我们可以很明显地发现由于参数传递的方式不同,Func1和Func2调用的IL指令也不近相同,最后让我们再看一下Main函数的执行过程再做分析。

  

  图8:Main函数反编译出来的IL代码

  反编译出来的Main函数IL指令执行流程如下:

  nop:没有什么意义;

  Ldsfld:将静态字段的值推送到计算堆栈上,这里就是把n的值推送到计算堆栈上;

  call:调用由传递的方法说明符指示的方法,也就是调用了Func1函数;

  nop:没有什么意义;

  Ldsfld:将静态字段的值推送到计算堆栈上,这里就是把n的值推送到计算堆栈上;

  call:调用由传递的方法说明符指示的方法,调用打印方法,将n的值输出到控制台

  nop:没有什么意义;

  Ldsflda:将静态字段的地址推送到计算堆栈上,就是把静态变量n在内存中的地址推送到计算堆栈上;

  call:调用由传递的方法说明符指示的方法,也就是调用了Func2函数;

  nop:没有什么意义;

  Ldsfld:将静态字段的值推送到计算堆栈上,这里就是把n的值推送到计算堆栈上;

  call:调用由传递的方法说明符指示的方法,调用打印方法,将n的值输出到控制台

  经过上面的一系列漫长地分析,我们可以得出结论:C#中,当一个方法所传入的参数是一个静态字段的时候,如果是引用传递的话,肯定是会去静态字段直接拿值的;如果方法是以传值的方式使用参数的话,一定是从复制的栈中拿值的。“”

五、IL代码指令对照表

  为了方便大家对照查看IL代码,理解IL指令的意义,马三在这里给大家提供了一个详细的IL指令对照表(Zery提供),原版的IL指令对照表大家可以到MSDN上面查看,附上链接

名称 说明
Add 将两个值相加并将结果推送到计算堆栈上。
Add.Ovf 将两个整数相加,执行溢出检查,并且将结果推送到计算堆栈上。
Add.Ovf.Un 将两个无符号整数值相加,执行溢出检查,并且将结果推送到计算堆栈上。
And 计算两个值的按位“与”并将结果推送到计算堆栈上。
Arglist 返回指向当前方法的参数列表的非托管指针。
Beq 如果两个值相等,则将控制转移到目标指令。
Beq.S 如果两个值相等,则将控制转移到目标指令(短格式)。
Bge 如果第一个值大于或等于第二个值,则将控制转移到目标指令。
Bge.S 如果第一个值大于或等于第二个值,则将控制转移到目标指令(短格式)。
Bge.Un 当比较无符号整数值或不可排序的浮点型值时,如果第一个值大于第二个值,则将控制转移到目标指令。
Bge.Un.S 当比较无符号整数值或不可排序的浮点型值时,如果第一个值大于第二个值,则将控制转移到目标指令(短格式)。
Bgt 如果第一个值大于第二个值,则将控制转移到目标指令。
Bgt.S 如果第一个值大于第二个值,则将控制转移到目标指令(短格式)。
Bgt.Un 当比较无符号整数值或不可排序的浮点型值时,如果第一个值大于第二个值,则将控制转移到目标指令。
Bgt.Un.S 当比较无符号整数值或不可排序的浮点型值时,如果第一个值大于第二个值,则将控制转移到目标指令(短格式)。
Ble 如果第一个值小于或等于第二个值,则将控制转移到目标指令。
Ble.S 如果第一个值小于或等于第二个值,则将控制转移到目标指令(短格式)。
Ble.Un 当比较无符号整数值或不可排序的浮点型值时,如果第一个值小于或等于第二个值,则将控制转移到目标指令。
Ble.Un.S 当比较无符号整数值或不可排序的浮点值时,如果第一个值小于或等于第二个值,则将控制权转移到目标指令(短格式)。
Blt 如果第一个值小于第二个值,则将控制转移到目标指令。
Blt.S 如果第一个值小于第二个值,则将控制转移到目标指令(短格式)。
Blt.Un 当比较无符号整数值或不可排序的浮点型值时,如果第一个值小于第二个值,则将控制转移到目标指令。
Blt.Un.S 当比较无符号整数值或不可排序的浮点型值时,如果第一个值小于第二个值,则将控制转移到目标指令(短格式)。
Bne.Un 当两个无符号整数值或不可排序的浮点型值不相等时,将控制转移到目标指令。
Bne.Un.S 当两个无符号整数值或不可排序的浮点型值不相等时,将控制转移到目标指令(短格式)。
Box 将值类转换为对象引用(O 类型)。
Br 无条件地将控制转移到目标指令。
Br.S 无条件地将控制转移到目标指令(短格式)。
Break 向公共语言结构 (CLI) 发出信号以通知调试器已撞上了一个断点。
Brfalse 如果 value 为 false、空引用(Visual Basic 中的 Nothing)或零,则将控制转移到目标指令。
Brfalse.S 如果 value 为 false、空引用或零,则将控制转移到目标指令。
Brtrue 如果 value 为 true、非空或非零,则将控制转移到目标指令。
Brtrue.S 如果 value 为 true、非空或非零,则将控制转移到目标指令(短格式)。
Call 调用由传递的方法说明符指示的方法。
Calli 通过调用约定描述的参数调用在计算堆栈上指示的方法(作为指向入口点的指针)。
Callvirt 对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。
Castclass 尝试将引用传递的对象转换为指定的类。
Ceq 比较两个值。如果这两个值相等,则将整数值 1 (int32) 推送到计算堆栈上;否则,将 0 (int32) 推送到计算堆栈上。
Cgt 比较两个值。如果第一个值大于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。
Cgt.Un 比较两个无符号的或不可排序的值。如果第一个值大于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。
Ckfinite 如果值不是有限数,则引发 ArithmeticException。
Clt 比较两个值。如果第一个值小于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。
Clt.Un 比较无符号的或不可排序的值 value1 和 value2。如果 value1 小于 value2,则将整数值 1 (int32 ) 推送到计算堆栈上;反之,将 0 ( int32 ) 推送到计算堆栈上。
Constrained 约束要对其进行虚方法调用的类型。
Conv.I 将位于计算堆栈顶部的值转换为 native int。
Conv.I1 将位于计算堆栈顶部的值转换为 int8,然后将其扩展(填充)为 int32。
Conv.I2 将位于计算堆栈顶部的值转换为 int16,然后将其扩展(填充)为 int32。
Conv.I4 将位于计算堆栈顶部的值转换为 int32。
Conv.I8 将位于计算堆栈顶部的值转换为 int64。
Conv.Ovf.I 将位于计算堆栈顶部的有符号值转换为有符号 native int,并在溢出时引发 OverflowException。
Conv.Ovf.I.Un 将位于计算堆栈顶部的无符号值转换为有符号 native int,并在溢出时引发 OverflowException。
Conv.Ovf.I1 将位于计算堆栈顶部的有符号值转换为有符号 int8 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I1.Un 将位于计算堆栈顶部的无符号值转换为有符号 int8 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I2 将位于计算堆栈顶部的有符号值转换为有符号 int16 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I2.Un 将位于计算堆栈顶部的无符号值转换为有符号 int16 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I4 将位于计算堆栈顶部的有符号值转换为有符号 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I4.Un 将位于计算堆栈顶部的无符号值转换为有符号 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I8 将位于计算堆栈顶部的有符号值转换为有符号 int64,并在溢出时引发 OverflowException。
Conv.Ovf.I8.Un 将位于计算堆栈顶部的无符号值转换为有符号 int64,并在溢出时引发 OverflowException。
Conv.Ovf.U 将位于计算堆栈顶部的有符号值转换为 unsigned native int,并在溢出时引发 OverflowException。
Conv.Ovf.U.Un 将位于计算堆栈顶部的无符号值转换为 unsigned native int,并在溢出时引发 OverflowException。
Conv.Ovf.U1 将位于计算堆栈顶部的有符号值转换为 unsigned int8 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.U1.Un 将位于计算堆栈顶部的无符号值转换为 unsigned int8 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.U2 将位于计算堆栈顶部的有符号值转换为 unsigned int16 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.U2.Un 将位于计算堆栈顶部的无符号值转换为 unsigned int16 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.U4 将位于计算堆栈顶部的有符号值转换为 unsigned int32,并在溢出时引发 OverflowException。
Conv.Ovf.U4.Un 将位于计算堆栈顶部的无符号值转换为 unsigned int32,并在溢出时引发 OverflowException。
Conv.Ovf.U8 将位于计算堆栈顶部的有符号值转换为 unsigned int64,并在溢出时引发 OverflowException。
Conv.Ovf.U8.Un 将位于计算堆栈顶部的无符号值转换为 unsigned int64,并在溢出时引发 OverflowException。
Conv.R.Un 将位于计算堆栈顶部的无符号整数值转换为 float32。
Conv.R4 将位于计算堆栈顶部的值转换为 float32。
Conv.R8 将位于计算堆栈顶部的值转换为 float64。
Conv.U 将位于计算堆栈顶部的值转换为 unsigned native int,然后将其扩展为 native int。
Conv.U1 将位于计算堆栈顶部的值转换为 unsigned int8,然后将其扩展为 int32。
Conv.U2 将位于计算堆栈顶部的值转换为 unsigned int16,然后将其扩展为 int32。
Conv.U4 将位于计算堆栈顶部的值转换为 unsigned int32,然后将其扩展为 int32。
Conv.U8 将位于计算堆栈顶部的值转换为 unsigned int64,然后将其扩展为 int64。
Cpblk 将指定数目的字节从源地址复制到目标地址。
Cpobj 将位于对象(&、* 或 native int 类型)地址的值类型复制到目标对象(&、* 或 native int 类型)的地址。
Div 将两个值相除并将结果作为浮点(F 类型)或商(int32 类型)推送到计算堆栈上。
Div.Un 两个无符号整数值相除并将结果 ( int32 ) 推送到计算堆栈上。
Dup 复制计算堆栈上当前最顶端的值,然后将副本推送到计算堆栈上。
Endfilter 将控制从异常的 filter 子句转移回公共语言结构 (CLI) 异常处理程序。
Endfinally 将控制从异常块的 fault 或 finally 子句转移回公共语言结构 (CLI) 异常处理程序。
Initblk 将位于特定地址的内存的指定块初始化为给定大小和初始值。
Initobj 将位于指定地址的值类型的每个字段初始化为空引用或适当的基元类型的 0。
Isinst 测试对象引用(O 类型)是否为特定类的实例。
Jmp 退出当前方法并跳至指定方法。
Ldarg 将参数(由指定索引值引用)加载到堆栈上。
Ldarg.0 将索引为 0 的参数加载到计算堆栈上。
Ldarg.1 将索引为 1 的参数加载到计算堆栈上。
Ldarg.2 将索引为 2 的参数加载到计算堆栈上。
Ldarg.3 将索引为 3 的参数加载到计算堆栈上。
Ldarg.S 将参数(由指定的短格式索引引用)加载到计算堆栈上。
Ldarga 将参数地址加载到计算堆栈上。
Ldarga.S 以短格式将参数地址加载到计算堆栈上。
Ldc.I4 将所提供的 int32 类型的值作为 int32 推送到计算堆栈上。
Ldc.I4.0 将整数值 0 作为 int32 推送到计算堆栈上。
Ldc.I4.1 将整数值 1 作为 int32 推送到计算堆栈上。
Ldc.I4.2 将整数值 2 作为 int32 推送到计算堆栈上。
Ldc.I4.3 将整数值 3 作为 int32 推送到计算堆栈上。
Ldc.I4.4 将整数值 4 作为 int32 推送到计算堆栈上。
Ldc.I4.5 将整数值 5 作为 int32 推送到计算堆栈上。
Ldc.I4.6 将整数值 6 作为 int32 推送到计算堆栈上。
Ldc.I4.7 将整数值 7 作为 int32 推送到计算堆栈上。
Ldc.I4.8 将整数值 8 作为 int32 推送到计算堆栈上。
Ldc.I4.M1 将整数值 -1 作为 int32 推送到计算堆栈上。
Ldc.I4.S 将提供的 int8 值作为 int32 推送到计算堆栈上(短格式)。
Ldc.I8 将所提供的 int64 类型的值作为 int64 推送到计算堆栈上。
Ldc.R4 将所提供的 float32 类型的值作为 F (float) 类型推送到计算堆栈上。
Ldc.R8 将所提供的 float64 类型的值作为 F (float) 类型推送到计算堆栈上。
Ldelem 按照指令中指定的类型,将指定数组索引中的元素加载到计算堆栈的顶部。
Ldelem.I 将位于指定数组索引处的 native int 类型的元素作为 native int 加载到计算堆栈的顶部。
Ldelem.I1 将位于指定数组索引处的 int8 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelem.I2 将位于指定数组索引处的 int16 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelem.I4 将位于指定数组索引处的 int32 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelem.I8 将位于指定数组索引处的 int64 类型的元素作为 int64 加载到计算堆栈的顶部。
Ldelem.R4 将位于指定数组索引处的 float32 类型的元素作为 F 类型(浮点型)加载到计算堆栈的顶部。
Ldelem.R8 将位于指定数组索引处的 float64 类型的元素作为 F 类型(浮点型)加载到计算堆栈的顶部。
Ldelem.Ref 将位于指定数组索引处的包含对象引用的元素作为 O 类型(对象引用)加载到计算堆栈的顶部。
Ldelem.U1 将位于指定数组索引处的 unsigned int8 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelem.U2 将位于指定数组索引处的 unsigned int16 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelem.U4 将位于指定数组索引处的 unsigned int32 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelema 将位于指定数组索引的数组元素的地址作为 & 类型(托管指针)加载到计算堆栈的顶部。
Ldfld 查找对象中其引用当前位于计算堆栈的字段的值。
Ldflda 查找对象中其引用当前位于计算堆栈的字段的地址。
Ldftn 将指向实现特定方法的本机代码的非托管指针(native int 类型)推送到计算堆栈上。
Ldind.I 将 native int 类型的值作为 native int 间接加载到计算堆栈上。
Ldind.I1 将 int8 类型的值作为 int32 间接加载到计算堆栈上。
Ldind.I2 将 int16 类型的值作为 int32 间接加载到计算堆栈上。
Ldind.I4 将 int32 类型的值作为 int32 间接加载到计算堆栈上。
Ldind.I8 将 int64 类型的值作为 int64 间接加载到计算堆栈上。
Ldind.R4 将 float32 类型的值作为 F (float) 类型间接加载到计算堆栈上。
Ldind.R8 将 float64 类型的值作为 F (float) 类型间接加载到计算堆栈上。
Ldind.Ref 将对象引用作为 O(对象引用)类型间接加载到计算堆栈上。
Ldind.U1 将 unsigned int8 类型的值作为 int32 间接加载到计算堆栈上。
Ldind.U2 将 unsigned int16 类型的值作为 int32 间接加载到计算堆栈上。
Ldind.U4 将 unsigned int32 类型的值作为 int32 间接加载到计算堆栈上。
Ldlen 将从零开始的、一维数组的元素的数目推送到计算堆栈上。
Ldloc 将指定索引处的局部变量加载到计算堆栈上。
Ldloc.0 将索引 0 处的局部变量加载到计算堆栈上。
Ldloc.1 将索引 1 处的局部变量加载到计算堆栈上。
Ldloc.2 将索引 2 处的局部变量加载到计算堆栈上。
Ldloc.3 将索引 3 处的局部变量加载到计算堆栈上。
Ldloc.S 将特定索引处的局部变量加载到计算堆栈上(短格式)。
Ldloca 将位于特定索引处的局部变量的地址加载到计算堆栈上。
Ldloca.S 将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)。
Ldnull 将空引用(O 类型)推送到计算堆栈上。
Ldobj 将地址指向的值类型对象复制到计算堆栈的顶部。
Ldsfld 将静态字段的值推送到计算堆栈上。
Ldsflda 将静态字段的地址推送到计算堆栈上。
Ldstr 推送对元数据中存储的字符串的新对象引用。
Ldtoken 将元数据标记转换为其运行时表示形式,并将其推送到计算堆栈上。
Ldvirtftn 将指向实现与指定对象关联的特定虚方法的本机代码的非托管指针(native int 类型)推送到计算堆栈上。
Leave 退出受保护的代码区域,无条件将控制转移到特定目标指令。
Leave.S 退出受保护的代码区域,无条件将控制转移到目标指令(缩写形式)。
Localloc 从本地动态内存池分配特定数目的字节并将第一个分配的字节的地址(瞬态指针,* 类型)推送到计算堆栈上。
Mkrefany 将对特定类型实例的类型化引用推送到计算堆栈上。
Mul 将两个值相乘并将结果推送到计算堆栈上。
Mul.Ovf 将两个整数值相乘,执行溢出检查,并将结果推送到计算堆栈上。
Mul.Ovf.Un 将两个无符号整数值相乘,执行溢出检查,并将结果推送到计算堆栈上。
Neg 对一个值执行求反并将结果推送到计算堆栈上。
Newarr 将对新的从零开始的一维数组(其元素属于特定类型)的对象引用推送到计算堆栈上。
Newobj 创建一个值类型的新对象或新实例,并将对象引用(O 类型)推送到计算堆栈上。
Nop 如果修补操作码,则填充空间。尽管可能消耗处理周期,但未执行任何有意义的操作。
Not 计算堆栈顶部整数值的按位求补并将结果作为相同的类型推送到计算堆栈上。
Or 计算位于堆栈顶部的两个整数值的按位求补并将结果推送到计算堆栈上。
Pop 移除当前位于计算堆栈顶部的值。
Prefix1 基础结构。此指令为保留指令。
Prefix2 基础结构。此指令为保留指令。
Prefix3 基础结构。此指令为保留指令。
Prefix4 基础结构。此指令为保留指令。
Prefix5 基础结构。此指令为保留指令。
Prefix6 基础结构。此指令为保留指令。
Prefix7 基础结构。此指令为保留指令。
Prefixref 基础结构。此指令为保留指令。
Readonly 指定后面的数组地址操作在运行时不执行类型检查,并且返回可变性受限的托管指针。
Refanytype 检索嵌入在类型化引用内的类型标记。
Refanyval 检索嵌入在类型化引用内的地址(& 类型)。
Rem 将两个值相除并将余数推送到计算堆栈上。
Rem.Un 将两个无符号值相除并将余数推送到计算堆栈上。
Ret 从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。
Rethrow 再次引发当前异常。
Shl 将整数值左移(用零填充)指定的位数,并将结果推送到计算堆栈上。
Shr 将整数值右移(保留符号)指定的位数,并将结果推送到计算堆栈上。
Shr.Un 将无符号整数值右移(用零填充)指定的位数,并将结果推送到计算堆栈上。
Sizeof 将提供的值类型的大小(以字节为单位)推送到计算堆栈上。
Starg 将位于计算堆栈顶部的值存储到位于指定索引的参数槽中。
Starg.S 将位于计算堆栈顶部的值存储在参数槽中的指定索引处(短格式)。
Stelem 用计算堆栈中的值替换给定索引处的数组元素,其类型在指令中指定。
Stelem.I 用计算堆栈上的 native int 值替换给定索引处的数组元素。
Stelem.I1 用计算堆栈上的 int8 值替换给定索引处的数组元素。
Stelem.I2 用计算堆栈上的 int16 值替换给定索引处的数组元素。
Stelem.I4 用计算堆栈上的 int32 值替换给定索引处的数组元素。
Stelem.I8 用计算堆栈上的 int64 值替换给定索引处的数组元素。
Stelem.R4 用计算堆栈上的 float32 值替换给定索引处的数组元素。
Stelem.R8 用计算堆栈上的 float64 值替换给定索引处的数组元素。
Stelem.Ref 用计算堆栈上的对象 ref 值(O 类型)替换给定索引处的数组元素。
Stfld 用新值替换在对象引用或指针的字段中存储的值。
Stind.I 在所提供的地址存储 native int 类型的值。
Stind.I1 在所提供的地址存储 int8 类型的值。
Stind.I2 在所提供的地址存储 int16 类型的值。
Stind.I4 在所提供的地址存储 int32 类型的值。
Stind.I8 在所提供的地址存储 int64 类型的值。
Stind.R4 在所提供的地址存储 float32 类型的值。
Stind.R8 在所提供的地址存储 float64 类型的值。
Stind.Ref 存储所提供地址处的对象引用值。
Stloc 从计算堆栈的顶部弹出当前值并将其存储到指定索引处的局部变量列表中。
Stloc.0 从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。
Stloc.1 从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中。
Stloc.2 从计算堆栈的顶部弹出当前值并将其存储到索引 2 处的局部变量列表中。
Stloc.3 从计算堆栈的顶部弹出当前值并将其存储到索引 3 处的局部变量列表中。
Stloc.S 从计算堆栈的顶部弹出当前值并将其存储在局部变量列表中的 index 处(短格式)。
Stobj 将指定类型的值从计算堆栈复制到所提供的内存地址中。
Stsfld 用来自计算堆栈的值替换静态字段的值。
Sub 从其他值中减去一个值并将结果推送到计算堆栈上。
Sub.Ovf 从另一值中减去一个整数值,执行溢出检查,并且将结果推送到计算堆栈上。
Sub.Ovf.Un 从另一值中减去一个无符号整数值,执行溢出检查,并且将结果推送到计算堆栈上。
Switch 实现跳转表。
Tailcall 执行后缀的方法调用指令,以便在执行实际调用指令前移除当前方法的堆栈帧。
Throw 引发当前位于计算堆栈上的异常对象。
Unaligned 指示当前位于计算堆栈上的地址可能没有与紧接的 ldind、stind、ldfld、stfld、ldobj、stobj、initblk 或 cpblk 指令的自然大小对齐。
Unbox 将值类型的已装箱的表示形式转换为其未装箱的形式。
Unbox.Any 将指令中指定类型的已装箱的表示形式转换成未装箱形式。
Volatile 指定当前位于计算堆栈顶部的地址可以是易失的,并且读取该位置的结果不能被缓存,或者对该地址的多个存储区不能被取消。
Xor 计算位于计算堆栈顶部的两个值的按位异或,并且将结果推送到计算堆栈上。

引用资料:

http://www.cnblogs.com/zery/p/3368460.html
https://msdn.microsoft.com/en-us/library/812xyxy2

作者:马三小伙儿
出处:http://www.cnblogs.com/msxh/p/7819645.html
请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!

原文地址:https://www.cnblogs.com/msxh/p/7819645.html

时间: 2024-10-08 21:14:46

【小白学C#】浅谈.NET中的IL代码的相关文章

浅谈数据库系统中的cache(转)

http://www.cnblogs.com/benshan/archive/2013/05/26/3099719.html 浅谈数据库系统中的cache(转) Cache和Buffer是两个不同的概念,简单的说,Cache是加速"读",而buffer是缓冲"写",前者解决读的问题,保存从磁盘上读出 的数据,后者是解决写的问题,保存即将要写入到磁盘上的数据.在很多情况下,这两个名词并没有严格区分,常常把读写混合类型称为buffer cache,本文后续的论述中,统一

【转】浅谈Java中的equals和==

浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str2 = new String("hello"); 3 4 System.out.println(str1==str2); 5 System.out.println(str1.equals(str2)); 为什么第4行和第5行的输出结果不一样?==和equals方法之间的区别是什么?如果在初

浅谈oracle中rowid和rownum

[ 概要 ] 刚刚接触oracle的同学可能常常会被rowid和rownum这两个词弄混, 弄清楚这两个家伙对于我们写sql会有很大的帮助, 下面偶就抛砖引玉, 简单地谈谈他们之间的区别吧. [ 比较 ] rowid和rownum都是oracle中的伪列, 但他们还是存在本质区别: rowid: 是物理地址, 用于定位数据表中数据的位置, 它是唯一的且不会改变. rownum: 是根据查询的结果集给每行分配的一个逻辑编号, 查询结果不同, rownum自然不同. 对于同一条记录, 查询条件不同,

转 浅谈C++中指针和引用的区别

浅谈C++中指针和引用的区别 浅谈C++中指针和引用的区别 指针和引用在C++中很常用,但是对于它们之间的区别很多初学者都不是太熟悉,下面来谈谈他们2者之间的区别和用法. 1.指针和引用的定义和性质区别: (1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元:而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已.如: int a=1;int *p=&a; int a=1;int &b=a; 上面定义了一个整形变量和一个指针变量p,该指针变量指向a

C++ 浅谈C++中指针和引用

浅谈C++中指针和引用的区别 指针和引用在C++中很常用,但是对于它们之间的区别很多初学者都不是太熟悉,下面来谈谈他们2者之间的区别和用法. 1.指针和引用的定义和性质区别: (1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元:而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已.如: int a=1;int *p=&a; int a=1;int &b=a; 上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单

( 转)浅谈QT中窗口刷新事件

浅谈QT中窗口刷新事件 [日期:2011-06-25] 来源:Linux社区  作者:袁硕 [字体:大 中 小] 经过一个星期的项目初步开发,写此文就开发时遇到的一些常见问题,给出些资料和自己的观点,希望能给其他的初学者或者参赛的选手一点帮助,当然,也算是一种抛砖引玉,大家有什么好的技巧经验什么的,也能多多分享,借助这次比赛,我们共同进步~ 如果大家都是跟我一样,刚刚开始接触QT,开始开发QT的程序,肯定也会有很多不习惯的地方,今天我重点想谈的就是这么一个不习惯的地方——QT中窗口刷新事件. 对

浅谈Linux中的信号机制(二)

首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇<浅谈Linux中的信号机制(一)>的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉.本人自知功力不够,尚且不能对着Linux内核源码评头论足.以后的路还很长,我还是一步一个脚印的慢慢走着吧,Linux内核这座山,我才刚刚抵达山脚下. 好了,言归正传,我接着昨天写下去.如有错误还请各位看官指正,先此谢过. 上篇末尾,我们看到了这样的现象:send进程总共发送了500次SIGINT信号给rcv进程,但是实际过程中rcv只接受/处理了1

浅谈Java中的对象和引用

浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是"对象和对象引用",很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起了解一下对象和对象引用之间的区别和联系. 1.何谓对象? 在Java中有一句比较流行的话,叫做"万物皆对象",这是Java语言设计之初的理念之一.要理解什么是对象,需要跟类一起结合起来理解.下面这段话引自<Java编程思想>中的一段原话: "按照通

浅谈Java中的equals和==

浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str2 = new String("hello"); 3 4 System.out.println(str1==str2); 5 System.out.println(str1.equals(str2)); 为什么第4行和第5行的输出结果不一样?==和equals方法之间的区别是什么?如果在初