最简单的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. 使用python抓取婚恋网用户数据并用决策树生成自己择偶观

    最近在看<机器学习实战>的时候萌生了一个想法,自己去网上爬一些数据按照书上的方法处理一下,不仅可以加深自己对书本的理解,顺便还可以在github拉拉人气.刚好在看决策树这一章,书里面的理论 ...

  2. Linux学习之文件操作

    Linux,一起学习进步-    mkdir The mkdir command is used to create directories.It works like this: mkdir命令是用 ...

  3. await and async

    Most people have already heard about the new “async” and “await” functionality coming in Visual Stud ...

  4. jQuery2.x源码解析(缓存篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...

  5. interpreter(解释器模式)

    一.引子 其实没有什么好的例子引入解释器模式,因为它描述了如何构成一个简单的语言解释器,主要应用在使用面向对象语言开发编译器中:在实际应用中,我们可能很少碰到去构造一个语言的文法的情况. 虽然你几乎用 ...

  6. (转载) Linux IO模式及 select、poll、epoll详解

    注:本文是对众多博客的学习和总结,可能存在理解错误.请带着怀疑的眼光,同时如果有错误希望能指出. 同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案 ...

  7. test

    http://img.ivsky.com/img/bizhi/pic/201009/07/fangaoyouhua-015.jpghttp://desk.fd.zol-img.com.cn/t_s16 ...

  8. 从is(":checked")说起

    *此文所用jQuery版本应大于1.6.1   如何判断一个单选(复选)框是否选中. 对于刚接触jQuery的人,第一反应必然是. <input id="checkbox1" ...

  9. 【Knockout.js 学习体验之旅】(3)模板绑定

    本文是[Knockout.js 学习体验之旅]系列文章的第3篇,所有demo均基于目前knockout.js的最新版本(3.4.0).小茄才识有限,文中若有不当之处,还望大家指出. 目录: [Knoc ...

  10. Thinking in Unity3D:渲染管线中的Rendering Path

      关于<Thinking in Unity3D> 笔者在研究和使用Unity3D的过程中,获得了一些Unity3D方面的信息,同时也感叹Unity3D设计之精妙.不得不说,笔者最近几年的 ...