最简单的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. 80 端口被占用 pid=4

    80端口被pid=4的系统进程给占用的解决方法: 一般开发的时候我们都会安装sqlserver ,也会把Sql server Reporting Services 安装上去.原因就是这个服务占用了80 ...

  2. 用scikit-learn学习DBSCAN聚类

    在DBSCAN密度聚类算法中,我们对DBSCAN聚类算法的原理做了总结,本文就对如何用scikit-learn来学习DBSCAN聚类做一个总结,重点讲述参数的意义和需要调参的参数. 1. scikit ...

  3. DBA成长路线

    从开发转为数据库管理,即人们称为DBA的已经有好几年,有了与当初不一样的体会.数据是企业的血液,数据是石油,数据是一切大数据.云计算的基础.作为DBA是数据的保卫者.管理者,是企业非常重要的角色.对于 ...

  4. 数据图表插件Echarts(一)

    一.引言 最近做一个智慧城市项目,项目中需要图表和报表进行数据分析,从网上找了很多,最后找到了百度开放的echarts,一个很强大的插件. 二.介绍 ECharts,缩写来自Enterprise Ch ...

  5. FullCalendar应用——整合农历节气和节日

    FullCalendar用来做日程管理功能非常强大,但是唯一不足的地方是没有将中国农历历法加进去,今天我将结合实例和大家分享如何将中国农历中的节气和节日整合到FullCalendar中,从而增强其实用 ...

  6. KOTLIN开发语言文档(官方文档) -- 2.基本概念

    网页链接:https://kotlinlang.org/docs/reference/basic-types.html 2.   基本概念 2.1.  基本类型 从可以在任何变量处理调用成员函数和属性 ...

  7. SQLServer2005创建定时作业任务

    SQLServer定时作业任务:即数据库自动按照定时执行的作业任务,具有周期性不需要人工干预的特点 创建步骤:(使用最高权限的账户登录--sa) 一.启动SQL Server代理(SQL Server ...

  8. Linux虚拟化学习笔记<一>

    关于虚拟化,原理的东西是非常复杂的,要想完全理解,没有足够的耐心是不不能完全学透这部分内容的.那下面我主要以资源汇总的形式把一些资料罗列出来,帮助大家快速理解虚拟化,快速使用和配置. 为什么要虚拟化: ...

  9. 用Taurus.MVC 做个企业站(下)

    前言: 上一篇完成了首页,这一篇就把剩下的几个功能给作了吧. 包括文章列表.文章详情和产品展示. 1:文章列表: 原来的ArticleList.aspx 1:现在的articlelist.html 除 ...

  10. 在同一个硬盘上安装多个 Linux 发行版及 Fedora 21 、Fedora 22 初体验

    在同一个硬盘上安装多个 Linux 发行版 以前对多个 Linux 发行版的折腾主要是在虚拟机上完成.我的桌面电脑性能比较强大,玩玩虚拟机没啥问题,但是笔记本电脑就不行了.要在我的笔记本电脑上折腾多个 ...