最简单的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. ExtJS 4.2 组件的查找方式

    组件创建了,就有方法找到这些组件.在DOM.Jquery都有各自的方法查找元素/组件,ExtJS也有自己独特的方式查找组件.元素.本次从全局查找.容器内查找.form表单查找.通用组件等4个方面介绍组 ...

  2. Laravel Composer and ServiceProvider

    Composer and: 创建自定义类库时,按命名空间把文件夹结构组织好 composer.json>autoload>classmap>psr-4 composer dump-a ...

  3. C++中的引用

    一,C++中引用的基础知识 1.引用的基本概念 1.所谓的引用其实就是对变量起“别名”.引用和变量对应得是相同的内存,修改引用的值,变量的值也会改变,和指针类似. 2.引用在定义的时候必须要初始化,初 ...

  4. 从零开始编写自己的C#框架(27)——什么是开发框架

    前言 做为一个程序员,在开发的过程中会发现,有框架同无框架,做起事来是完全不同的概念,关系到开发的效率.程序的健壮.性能.团队协作.后续功能维护.扩展......等方方面面的事情.很多朋友在学习搭建自 ...

  5. 第一个移动前端开源项目-dailog

    你还在为手机上没有忙碌光标而发愁吗?你还在抱怨弹出框组件要依赖zepto/jqery吗?你还在纠结是否要自己写一套还是去网上寻找成现成的UI组件吗?YouA为你轻松解决所有烦恼.YouA是我为移动前端 ...

  6. Windows API 设置窗口下控件Enable属性

    参考页面: http://www.yuanjiaocheng.net/webapi/create-crud-api-1-put.html http://www.yuanjiaocheng.net/we ...

  7. Android Studio:Failed to resolve ***

    更换电脑后,也更新了所有的SDK的tool,仍然报错:Failed to resolve  各种jar包,出现这种问题主要是因为在Android studio中默认不允许在线更新,修改方法如下:

  8. Linux目录结构

  9. UGUI Text(Label)

    环境 Unity 5.3.6f1 关于Best Fit 如果勾选了 Best Fit ,当有大量的文本填充在Text上时,那么文字是不会自动换行的. 打字机效果 在github上已有现成的:https ...

  10. EF里Guid类型数据的自增长、时间戳和复杂类型的用法

    通过前两章Lodging和Destination类的演示,大家肯定基本了解Code First是怎么玩的了,本章继续演示一些很实用的东西.文章的开头提示下:提供的demo为了后面演示效果,前面代码有些 ...