最简单的IL程序

.assembly test {}
.method void Func()
{
.entrypoint
ldstr "hello world"
call void [mscorlib]System.Console::WriteLine(string)
ret
}

对上面的程序说明如下:

  • .assemble伪指令用来声明程序集,该关键字是必须的
  • .method伪指令用来申明方法
  • .entrypoint伪指令用来表示程序的入口函数(方法),一个程序只能有一个入口,且不能放在实例方法中
  • ldstr指令是 load a string on stock 的缩写,表示把一个字符串入栈
  • call指令用来调用方法,参数取值栈顶。[mscorlib]表示调用方法所在程序集,类名和方法名之间用::隔开。
  • ret指令表示方法返回,应该是 return 的缩写

打开VS20XX开发人员命令提示工具,输入如下命令可编译IL程序为可执行文件:

ilasm test.il

函数和方法

上面的例子中,入口“方法”Func不属于任何一个类,为了区别于实例方法和静态方法,下文中称之为“函数”,IL中既可以写方法也可以写函数,而且函数可以作为程序的入口点,有点类似C++。声明一个方法的语法如下:

.method [public|private|family|assembly|famorassem|famandassem|privatescope] [hidebysig] [static|instance] 返回值类型 方法名(参数类型, 参数类型2...)[il managed]
{
//方法体
ret
}

访问限制修饰符

  • public 等同于C#的public
  • private 等同于C#的private
  • family 等同于C#的protected
  • assembly 等同于C#的internal
  • famorassem fam是family的缩写,assem是assembly的缩写,等同于C#的protected internal
  • famandassem C#中没有对应的修饰符
  • privatescope 当前的module(一个程序集中可以包含多个module)中随处可访问

其他常见修饰符

  • hidebysig 相当于C#方法前面的new修饰符
  • static和instance 前者表示静态方法,后者表示实例方法

方法的调用

请看实例:

namespace ILTest
{
public sealed class Program
{
public static void Main(string[] args)
{
int result = Program.Add(1, 2);
} public static int Add(int a, int b)
{
return a + b;
}
}
}

IL:

.method public hidebysig static void Main(string[] args) cil managed
{
.maxstack 2
.entrypoint
.locals init (
[0] int32 result
) IL_0001: ldc.i4.1 //将第1参数入栈
IL_0002: ldc.i4.2 //将第2参数入栈
IL_0003: call int32 ILTest.Program::Add(int32, int32) //调用方法Add,参数pop自栈顶
IL_0008: stloc.0 //Add方法内已经将计算结果放到了栈顶,此处将栈顶数据赋值变量result
IL_0009: ret //方法结束
} .method public hidebysig static int32 Add(int32 a, int32 b) cil managed
{
.maxstack 2
.locals init (
[0] int32 CS$1$0000
) IL_0001: ldarg.0 //将第一个参数入栈
IL_0002: ldarg.1 //将第二个参数入栈
IL_0003: add //从栈中pop两个数,执行加法计算后将结果入栈
IL_0004: stloc.0 //将栈顶数据pop并赋值给变量CS$1$0000
IL_0005: br.s IL_0007 //跳转到行IL_0007 IL_0007: ldloc.0 //将变量CS$1$0000入栈
IL_0008: ret //方法结束,将返回值推送到调用者栈
}

上例可见,返回值在子方法中被入栈,父方法中再从栈顶出栈,方法的返回值通过栈来实现传递。

命名空间和类

下面是一个最简单的具有命名空间和类的例子:

.assembly test {}
.namespace test.com.joey
{
.class Program
{
.method static void Main()
{
.entrypoint
ldstr "hello world"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
}

类的常见修饰符有如下这些:

  • public|private 相当于C#的public和Internal
  • abstract 表示抽象类
  • sealed 表示封闭类
  • ansi 类中字符串编码格式,可选项还有unicodeautochar(根据不同的平台,会自动转换成相对应的字符集)
  • auto 告诉运行时(CLR)自动为非托管内存中的对象的成员选择适当的布局,可选项还有sequentialexplicit

字段和构造函数

编写如下C#代码:

namespace ILTest
{
public class Program
{
private int _var = 100;
private static int _staticVar = 100;
public static void Main(string[] args)
{
}
}
}

编译为IL:



请看第5和第6行代码,.field伪指令用来声明字段,虽然C#代码中为这个两个字段赋了默认值,但是IL代码中并没有,真正的赋值操作是在默认构造函数和静态构造函数完成的。

请看从行号23开始的默认构造函数(.ctor),ldc.i4.s 100表示将100入栈(凡是ld开头的指令多是入栈指令),stfld int32 ILTest.Program::_var表示将栈顶数据赋值给字段_var(凡是st打头的指令多是赋值指令)。再看从39行开始的静态构造函数(.cctor),静态字段声明是赋值其实也是在静态构造函数里完成的。

在做一个有趣的实验,将上面的C#代码中添加构造函数如下:

public Program()
{
_var = 200;
}

再看生产的IL代码中的构造函数:

_var变量居然被赋值了2次,2次!可见,C#编译器并未就这种情况作出优化,不过貌似没人会写这么傻瓜的代码吧,声明时赋值后又在无参构造函数中赋值。大家有兴趣可以试试静态变量,情况也是一样一样的。

还有一点需要注意,字段声明时赋值总是放在构造函数的最前面,然后是调用父类构造函数(第20行),最后才是自己写的代码,而静态构造函数的又在非静态构造函数之前执行,所有,实例化一个类时,正确的执行顺序就是这样的:

  1. 静态字段赋值(如果声明的同时就赋值的话)
  2. 静态构造函数
  3. 非静态字段赋值(如果声明的同时就赋值的话)
  4. 非静态父类构造函数
  5. 非静态子类构造函数

学了IL后妈妈再也不用担心我记不住类的实例化执行顺序了。

另外需要注意的地方是,如果C#类中没有无参构造函数,那么C#编译器会自动生成,但是静态构造函数就不会制动时生成了。

局部变量

编写C#代码如下:

namespace ILTest
{
public class Program
{
public static void Main(string[] args)
{
int intVar = 100;
float floatVar = 101f;
String stringVar = "hello"; Console.WriteLine(intVar);
Console.WriteLine(floatVar);
Console.WriteLine(stringVar);
}
}
}

编译为IL:

由上图可见,局部变量相关的步骤可分为3步:

第一步:编号

.locals init伪指令负责把局部变量从0开始编号(第14行),后面的代码中只使用这个编号,变量名称不再使用

第二步:赋值

ldc.i4.s 100(第21行)将100入栈,接下来stloc.0表示把栈顶值赋值给第0个变量

第三步:使用

ldloc.0指令表示把第0个变量入栈(第27行),接下来System.Console::WriteLine方法从栈顶得到该变量的值

条件判断与循环

IL没有提供ifwhile等循环,只有br跳转指令或brXXX等有条件跳转指令,C#编译器或将C#的条件判断和循环编译为br跳转指令,请看如下的例子:

public static void Main(string[] args)
{
bool symbol = false;
if (symbol)
{
Console.WriteLine("hello");
}
}

IL:

.method public hidebysig static void Main (string[] args) cil managed
{
.maxstack 2
.entrypoint
.locals init (
[0] bool symbol, //C#代码声明的变量
[1] bool CS$4$0000 //编译器自动生成一个变量
) IL_0001: ldc.i4.0 //把0入栈,0用来表示false
IL_0002: stloc.0 //把0出栈并赋值给变量symbol
IL_0003: ldloc.0 //把变量symbol的值0入栈
IL_0004: ldc.i4.0 //把0入栈,0用来表示false
IL_0005: ceq //对栈顶和栈中第二个数出栈并比较其是否相等,如果相等将1入栈,否则将0入栈
IL_0007: stloc.1 //将栈顶元素出栈并赋值给变量CS$4$0000,该变量存储指令ceq执行的结果
IL_0008: ldloc.1 //将变量CS$4$0000的值入栈,该变量存储指令ceq执行的结果
IL_0009: brtrue.s IL_0018 //如果栈顶元素是1,那么跳转到IL_0018行 IL_000c: ldstr "hello"
IL_0011: call void [mscorlib]System.Console::WriteLine(string) IL_0018: ret
}

循环语句亦是如此,不在赘述。

常用指令备忘

上面所说的是IL中最为基础的部分,如果要想再深入学习,只需编写C#代码并编译为IL查看即可,有了上面的基础应该可以看懂了。下面列举一些常用的IL指令以供不时之查:

指令 说明
Add
Sub
Mul
Div
Rem 取余
Xor 按位异或
And 按位与
Or 按位或
Not 按位补
Dup 复制计栈顶端值,然后将副本入栈
Neg 按位反
Ret 从当前方法返回,并将返回值(如果存在)从子方法栈推送到调用方法的栈上
Jmp 退出当前方法并跳至指定方法
Newobj 创建对象新实例,并将对象引用推送到栈上
Newarr 创建数组,并将数组引用推送到栈上
Nop Debug模式下生成,断点设置辅助
Initobj 将位于指定地址的值类型的每个字段初始化为空引用或适当的基元类型的 0
Isinst 测试对象引用是否为特定类的实例
Sizeof
Box 装箱
Unbox 拆箱
Castclass 类型转换
Switch 实现跳转表
Throw 引发异常
Call 调用静态方法
Callvirt 调用实例方法或虚方法

参考资料

(翻译) 《C# to IL》

Introduction to IL Assembly Language

IL汇编语言介绍(译)

30分钟?不需要,轻松读懂IL

CLR via C# 摘要二:IL速记的更多相关文章

  1. CLR via C# 摘要一:托管程序的执行模型

    托管程序的执行模型大致如下: 编译源代码为程序集(dll或exe文件),程序集包括了记录相关信息的元数据和IL代码 执行程序集文件时,启动CLR,JIT负责把IL编译为本地代码并执行 IL是微软推出的 ...

  2. 阅读《LEARNING HARD C#学习笔记》知识点总结与摘要二

    今天继续分享我的阅读<LEARNING HARD C#学习笔记>知识点总结与摘要二,仍然是基础知识,但可温故而知新. 七.面向对象 三大基本特性: 封装:把客观事物封装成类,并隐藏类的内部 ...

  3. - 高级篇:二,IL设置静态属性,字段和类型转换

    - 高级篇:二,IL设置静态属性,字段和类型转换 静态属性赋值 先来看 Reflector反射出的IL源码(感谢Moen的提示),这次用 Release模式编译,去掉那些无用的辅助指令 public ...

  4. 四、CLR执行程序集中代码和IL代码简介

    三.加载公共语言运行时中介绍了在安装了.Net Framework中加载公共语言运行时,公共语言运行时加载程序集的过程.以及通过vs stdio设置源码编译的目标平台的过程. 本问主要介绍公共语言加载 ...

  5. 公共语言运行库(CLR)和中间语言(IL)(一)

    公共语言运行库(.net运行库)即CLR 1.C#先编译为IL,IL为ms的中间语言,IL是平台无关性的. 2.CLR再将IL编译为平台专用语言. 3.CLR在编译IL时为即时编译(JIT) VB.V ...

  6. 玩转动态编译 - 高级篇:二,IL设置静态属性,字段和类型转换

    静态属性赋值 先来看 Reflector反射出的IL源码(感谢Moen的提示),这次用 Release模式编译,去掉那些无用的辅助指令 public void AAA(string s) { MyCl ...

  7. 重温CLR(二)生成、部署以及程序集

    将类型生成到模块中 class Program { static void Main(string[] args) { Console.WriteLine("Hi"); } } 该 ...

  8. CLR总览

    Contents 第1章CLR的执行模型... 4 1.1将源代码编译成托管代码模块... 4 1.2 将托管模块合并成程序集... 6 1.3加载公共语言运行时... 7 1.4执行程序集的代码.. ...

  9. 读懂IL代码就这么简单(三)完结篇

    一 前言 写了两篇关于IL指令相关的文章,分别把值类型与引用类型在 堆与栈上的操作区别详细的写了一遍 这第三篇也是最后一篇,之所以到第三篇就结束了,是因为以我现在的层次,能理解到的都写完了,而且个人认 ...

随机推荐

  1. Linux学习之探索文件系统

    Linux,一起学习进步-    ls With it, we can see directory contents and determine a variety of important file ...

  2. css居中div的几种常用方法

    在开发过程中,很多需求需要我们居中一个div,比如html文档流当中的一块div,比如弹出层内容部分这种脱离了文档流等.不同的情况有不同的居中方式,接下来就分享下一下几种常用的居中方式. 1.text ...

  3. 在知乎上看到 Web Socket这篇文章讲得确实挺好,从头看到尾都非常形象生动,一口气看完,没有半点模糊,非常不错

    在知乎上看到这篇文章讲得确实挺好,从头看到尾都非常形象生动,一口气看完,没有半点模糊,非常不错,所以推荐给大家,非常值得一读. 作者:Ovear链接:https://www.zhihu.com/que ...

  4. MATLAB中绘制质点轨迹动图并保存成GIF

    工作需要在MATLAB中绘制质点轨迹并保存成GIF以便展示. 绘制质点轨迹动图可用comet和comet3命令,使用例子如下: t = 0:.01:2*pi;x = cos(2*t).*(cos(t) ...

  5. JQuery实现表格的增加行和删除行

    利用JQuery实现datatables插件的增加和删除行操作 在学习过程中遇到了这个利用JQuery对表格行的增加和删除,特记录下来以供初学者参考. 下面是主要的代码: <meta http- ...

  6. 如何解决流程开发中SheetRadioButtonList页面取值问题

    分享一个常见的取值问题. 应用场景: SheetRadioButtonList控件,点击其中一项执行事件操作.如果是页面加载的情况下,值就无法取到. 具体原因如下: 我给SheetRadioButto ...

  7. CocoaPods的安装、使用、以及遇到的问题

    CocoaPods是什么? 当你开发iOS应用时,会经常使用到很多第三方开源类库,比如JSONKit,AFNetWorking等等.可能某个类库又用到其他类库,所以要使用它,必须得另外下载其他类库,而 ...

  8. Form 表单提交参数

    今天因为要额外提交参数数组性的参数给form传到后台而苦恼了半天,结果发现,只需要在form表单对应的字段html空间中定义name = 后台参数名 的属性就ok了. 后台本来是只有模型参数的,但是后 ...

  9. linux下使用shell 自动执行脚本文件

    以下实例本人在Centos6.5 64位操作系统中使用 一.定时复制文件 a.在/usr/local/wfjb_web_back目录下创建 tomcatBack.sh文件 文件内容: #将tomcat ...

  10. BZOJ1012: [JSOI2008]最大数maxnumber [线段树 | 单调栈+二分]

    1012: [JSOI2008]最大数maxnumber Time Limit: 3 Sec  Memory Limit: 162 MBSubmit: 8748  Solved: 3835[Submi ...