IL部分应如何更关注社会底层群体?

2026-05-25 09:253阅读0评论SEO教程
  • 内容介绍
  • 相关推荐

本文共计2285个文字,预计阅读时间需要10分钟。

IL部分应如何更关注社会底层群体?

园林里两只大牛激烈地争吵,小生不才,借一借两位名人的名句也来聊一聊。Microsoft intermediate language(MSIL,即MSIL,也就是大家口中的IL)和ASM(这里指向X86汇编,排除其他高级)

园子里两个大牛正争的如火如荼,小生不才,借一下两个名人的名气也来谈一下Microsoft intermediate language (MSIL,就是大家口里的IL)和ASM(这里指针对X86汇编,排除其他一切“高级汇编”)。

为了达成共识,我们先对一些概念回忆一下:

CPU只能执行机器码,不能执行IL

这个应该没有什么疑问吧。机器码就是传说的0、1的组合,虽然今天的CPU运算速度已经非常非常快,而且非常非常智能,但它和上个世纪的CPU还是一样,还只能认识0、1的组合的这种机器码。

说到IL就应该提一提编译器的前端和后端。众所周知,微软的.NET平台上有众多的语言,大家熟悉的有C#、VB.NET、Jscript.NET,而这些不同语法的“高级”语言,经过CSC.Exe(C#编译器)、VBC.Exe(VB.NET编译器)等编译后得到IL,这之前的部分我们称之为前端,实际上事情并没有到这里结束,当CLR加载托管程序集,运行某一个方法时,CLR发现这个方法还没有被即时编译(JIT),这个时候就会调用JIT编译器对这个方法的IL编译,编译的结果就是我们的目标代码(目标代码可以是汇编代码或机器码,这里不加以区分)。而这之后的JIT编译等过程我们可以认为是编译器的后端。

IL部分应如何更关注社会底层群体?

有了上面这段描述,各位同学大脑中应该有这样一幅画面:

通过这幅图我们看到,微软通过实现不同的前端,而共一个后端实现了一个平台,多种语言的目标。这也就是为什么你用VB.NET写的组件,我用C#可以直接使用,甚至是
我用C#写的类直接派生自一个VB.NET写的类。

因为IL相对于机器码来说相对简单,因为IL不能操作寄存器。所以你甚至可以自己定义一个语法,然后实现一个编译器的“前端”,将你自己的语言加入到.NET这个大家族
中(貌似园子里的装配脑袋正在做这方面的工作)。这样你自己的语言也可以享受.NET的类库了,.NET的垃圾回收机制了。

从这里我们了解到IL起一个桥梁的作用。那好,我们学习IL到底可以干些什么?

1:探究C#这些编译器内部所作所为:

记得以前自由互联有一场对访问集合对象时,使用for好还是foreach好的大讨论,那我就用这个例子来用IL说明一下使用for访问

staticvoidMain(string[]args)
{
ArrayListarr=newArrayList();
for(inti=0;i<arr.Count;i++)
{
objecto=arr[i];
}
}

对应IL代码如下:

.methodprivatehidebysigstaticvoidMain(string[]args)cilmanaged
{
//标明这是本程序的入口点
.entrypoint
//Codesize32(0x20)
.maxstack2
//声明两个局部变量,一个ArrayList类型的arr,一个用在for循环中的整型i
.localsinit([0]class[mscorlib]System.Collections.ArrayListarr,
[1]int32i)
//调用ArrayList的构造函数,实例化对象,并将对象的引用放到IL的运算栈上
IL_0000:newobjinstancevoid[mscorlib]System.Collections.ArrayList::.ctor()
//将IL运算栈上顶部的一项弹出,赋值给本方法的第一个局部变量
IL_0005:stloc.0
//将一个四字节的整型0放到IL的运算栈上
IL_0006:ldc.i4.0
//将IL运算栈顶部的一项弹出,复制给本方法的第二个局部变量
IL_0007:stloc.1
//跳转到IL_0016处
IL_0008:br.sIL_0016
//将本方法的第一个局部变量加载到运算栈顶部
IL_000a:ldloc.0
//将本方法第二个局部变量加载到运算栈顶部
IL_000b:ldloc.1
//弹出运算栈最顶部项,在这里就是arr,调用arr的get_Item(int32)方法,并且将结果放//到运算栈顶部
IL_000c:callvirtinstanceobject[mscorlib]System.Collections.ArrayList::get_Item(int32)
//弹出运算栈顶部,在这里注意到,C#的代码将arr[i]赋值给了object对象,但为什么没有//对应IL代码呢,原来C#编译器发现这个object对象o并没有什么用,所以就直接丢弃了,//呵呵,还挺智能的
IL_0011:pop
//加载本方法第二个局部变量到运算栈顶部
IL_0012:ldloc.1
//加载四字节整型1到运算栈顶部
IL_0013:ldc.i4.1
//弹出运算栈顶部两项,相加,将相加后的结果放到运算栈顶部
IL_0014:add
//将运算栈顶部一项赋值给本方法的第二个局部变量(聪明的你从这里应该可以猜测得到,//这里就是for循环中的i++了)
IL_0015:stloc.1
//将本方法的第二个局部变量加载到IL运算栈
IL_0016:ldloc.1
//将本方法的第一个局部变量加载到IL运算栈
IL_0017:ldloc.0
//弹出运算栈顶部一项,调用该项的get_Count()方法,从上面代码可以看出运算栈顶部一//项就是本方法的第一个局部变量,也就是arr,方法调用后的结果会保存到运算栈顶部(从//这里也可以看出读取C#里的Count属性原来就是调用get_Count()方法,这也是IL的一个作//用,你现在应该明白了为什么大家都说.NET中的属性其实是两个方法吧)
IL_0018:callvirtinstanceint32[mscorlib]System.Collections.ArrayList::get_Count()
//弹出运算栈顶部的两项,比较大小,在这里就是arr.Count和i,如果i小于arr.Count,则//跳转到IL_000a
IL_001d:blt.sIL_000a
//不用多说,退出方法
IL_001f:ret
}

上面就是Main这个方法对应的IL代码,从注释中可以看出,IL是基于一个运算栈的,所有的操作数就是从这运算栈里倒来倒去,运算栈也是一个虚拟的概念,
不要想着把它与物理计算机里的内存或寄存器相对应起来,因为IL本来就是运行在CLR这个虚拟机之上的。看了使用for访问的代码,我们来看看使用foreach访问的代码
(与上面相同的部分我就不注释了):

.methodprivatehidebysigstaticvoidMain(string[]args)cilmanaged
{
.entrypoint
//Codesize50(0x32)
.maxstack1
//声明四个局部变量啊
.localsinit([0]class[mscorlib]System.Collections.ArrayListarr,
[1]objectitem,
[2]class[mscorlib]System.Collections.IEnumeratorCS$5$0000,
[3]class[mscorlib]System.IDisposableCS$0$0001)
IL_0000:newobjinstancevoid[mscorlib]System.Collections.ArrayList::.ctor()
IL_0005:stloc.0
IL_0006:ldloc.0
//注意,调用ArrayList的GetEnumerator()方法,获取ArrayList的迭代器
IL_0007:callvirtinstanceclass[mscorlib]System.Collections.IEnumerator[mscorlib]System.Collections.ArrayList::GetEnumerator()
IL_000c:stloc.2
//呵呵,还加了一个try,为的是能在finally的时候调用Dispose()方法
.try
{
IL_000d:br.sIL_0016
IL_000f:ldloc.2
//不再是使用ArrayList的get_Item(int32)方法了,是使用迭代器的get_Current()
IL_0010:callvirtinstanceobject[mscorlib]System.Collections.IEnumerator::get_Current()
IL_0015:stloc.1
IL_0016:ldloc.2
//调用迭代器的MoveNext方法
IL_0017:callvirtinstancebool[mscorlib]System.Collections.IEnumerator::MoveNext()
IL_001c:brtrue.sIL_000f
IL_001e:leave.sIL_0031
}//end.try
finally
{
IL_0020:ldloc.2
IL_0021:isinst[mscorlib]System.IDisposable
IL_0026:stloc.3
IL_0027:ldloc.3
IL_0028:brfalse.sIL_0030
IL_002a:ldloc.3
IL_002b:callvirtinstancevoid[mscorlib]System.IDisposable::Dispose()
IL_0030:endfinally
}//endhandler
IL_0031:ret
}

哦,这样一比较,for和foreach的不同点就自然而然的明白了。对于这些“语法糖”,我们只要打开IL一切都明明白白,C# 3.0的语法糖为数众多,为什么很多
人知道他到底是怎样实现的呢,还有很多人叫嚷着,这没什么,语法糖而已,因为大家都明白IL没变啥,只是编译器使坏。

2.加密解密

加密解密在Win32时代就有之,不过自从.NET的出现这块就更容易了,很多加密的方法,只需要打开IL,然后改一点东西,再用微软提供的ILAsmer汇编回去,这样就破解了。
这块内容很复杂,园子里也有很多文章。

那学IL有没有用呢?当然有用。学习IL你可以看到表面上看不到的东西,如果你是一位痴迷于技术的同学,那IL就应该好好学学了,把CLR看做“计算机”,那IL就是CLR的“本地代码”
(实际上不正确,CLR自己并不直接运行IL,CLR还是需要将IL编译为真正的本地代码然后执行)。

.NET的水很深,实际上针对编译器前端这部分水不是很深。内核或原理部分都是在CLR这部分,比如程序集如何加载?方法如何编译?多态的实现方法?那要探究这部分的原理怎么办?这个
时候从IL就无法知晓了,当然你可以google之或百度之,你也可以买很多大牛的书籍看看。不过你想不想自己弄明白呢?毕竟如果不自己加以试验得出结论,那些东西我觉得还是书本上的,
我们获得的知识也是靠记忆获得的,如果你能使用一个调试工具,亲自打开JIT之后的结果,你就知道多态到底是咋回事?

要知后事,请听下回分解!

本来想写三篇的:

关注底层(上):IL部分

关注底层(中):汇编 for .NETer

关注底层(下):Why and How

不过发现我想讨论的内容老赵已经说了,而且说得更好,所以觉得后面两篇也没有必要出了。所以就留此一篇作为纪念吧。

本文共计2285个文字,预计阅读时间需要10分钟。

IL部分应如何更关注社会底层群体?

园林里两只大牛激烈地争吵,小生不才,借一借两位名人的名句也来聊一聊。Microsoft intermediate language(MSIL,即MSIL,也就是大家口中的IL)和ASM(这里指向X86汇编,排除其他高级)

园子里两个大牛正争的如火如荼,小生不才,借一下两个名人的名气也来谈一下Microsoft intermediate language (MSIL,就是大家口里的IL)和ASM(这里指针对X86汇编,排除其他一切“高级汇编”)。

为了达成共识,我们先对一些概念回忆一下:

CPU只能执行机器码,不能执行IL

这个应该没有什么疑问吧。机器码就是传说的0、1的组合,虽然今天的CPU运算速度已经非常非常快,而且非常非常智能,但它和上个世纪的CPU还是一样,还只能认识0、1的组合的这种机器码。

说到IL就应该提一提编译器的前端和后端。众所周知,微软的.NET平台上有众多的语言,大家熟悉的有C#、VB.NET、Jscript.NET,而这些不同语法的“高级”语言,经过CSC.Exe(C#编译器)、VBC.Exe(VB.NET编译器)等编译后得到IL,这之前的部分我们称之为前端,实际上事情并没有到这里结束,当CLR加载托管程序集,运行某一个方法时,CLR发现这个方法还没有被即时编译(JIT),这个时候就会调用JIT编译器对这个方法的IL编译,编译的结果就是我们的目标代码(目标代码可以是汇编代码或机器码,这里不加以区分)。而这之后的JIT编译等过程我们可以认为是编译器的后端。

IL部分应如何更关注社会底层群体?

有了上面这段描述,各位同学大脑中应该有这样一幅画面:

通过这幅图我们看到,微软通过实现不同的前端,而共一个后端实现了一个平台,多种语言的目标。这也就是为什么你用VB.NET写的组件,我用C#可以直接使用,甚至是
我用C#写的类直接派生自一个VB.NET写的类。

因为IL相对于机器码来说相对简单,因为IL不能操作寄存器。所以你甚至可以自己定义一个语法,然后实现一个编译器的“前端”,将你自己的语言加入到.NET这个大家族
中(貌似园子里的装配脑袋正在做这方面的工作)。这样你自己的语言也可以享受.NET的类库了,.NET的垃圾回收机制了。

从这里我们了解到IL起一个桥梁的作用。那好,我们学习IL到底可以干些什么?

1:探究C#这些编译器内部所作所为:

记得以前自由互联有一场对访问集合对象时,使用for好还是foreach好的大讨论,那我就用这个例子来用IL说明一下使用for访问

staticvoidMain(string[]args)
{
ArrayListarr=newArrayList();
for(inti=0;i<arr.Count;i++)
{
objecto=arr[i];
}
}

对应IL代码如下:

.methodprivatehidebysigstaticvoidMain(string[]args)cilmanaged
{
//标明这是本程序的入口点
.entrypoint
//Codesize32(0x20)
.maxstack2
//声明两个局部变量,一个ArrayList类型的arr,一个用在for循环中的整型i
.localsinit([0]class[mscorlib]System.Collections.ArrayListarr,
[1]int32i)
//调用ArrayList的构造函数,实例化对象,并将对象的引用放到IL的运算栈上
IL_0000:newobjinstancevoid[mscorlib]System.Collections.ArrayList::.ctor()
//将IL运算栈上顶部的一项弹出,赋值给本方法的第一个局部变量
IL_0005:stloc.0
//将一个四字节的整型0放到IL的运算栈上
IL_0006:ldc.i4.0
//将IL运算栈顶部的一项弹出,复制给本方法的第二个局部变量
IL_0007:stloc.1
//跳转到IL_0016处
IL_0008:br.sIL_0016
//将本方法的第一个局部变量加载到运算栈顶部
IL_000a:ldloc.0
//将本方法第二个局部变量加载到运算栈顶部
IL_000b:ldloc.1
//弹出运算栈最顶部项,在这里就是arr,调用arr的get_Item(int32)方法,并且将结果放//到运算栈顶部
IL_000c:callvirtinstanceobject[mscorlib]System.Collections.ArrayList::get_Item(int32)
//弹出运算栈顶部,在这里注意到,C#的代码将arr[i]赋值给了object对象,但为什么没有//对应IL代码呢,原来C#编译器发现这个object对象o并没有什么用,所以就直接丢弃了,//呵呵,还挺智能的
IL_0011:pop
//加载本方法第二个局部变量到运算栈顶部
IL_0012:ldloc.1
//加载四字节整型1到运算栈顶部
IL_0013:ldc.i4.1
//弹出运算栈顶部两项,相加,将相加后的结果放到运算栈顶部
IL_0014:add
//将运算栈顶部一项赋值给本方法的第二个局部变量(聪明的你从这里应该可以猜测得到,//这里就是for循环中的i++了)
IL_0015:stloc.1
//将本方法的第二个局部变量加载到IL运算栈
IL_0016:ldloc.1
//将本方法的第一个局部变量加载到IL运算栈
IL_0017:ldloc.0
//弹出运算栈顶部一项,调用该项的get_Count()方法,从上面代码可以看出运算栈顶部一//项就是本方法的第一个局部变量,也就是arr,方法调用后的结果会保存到运算栈顶部(从//这里也可以看出读取C#里的Count属性原来就是调用get_Count()方法,这也是IL的一个作//用,你现在应该明白了为什么大家都说.NET中的属性其实是两个方法吧)
IL_0018:callvirtinstanceint32[mscorlib]System.Collections.ArrayList::get_Count()
//弹出运算栈顶部的两项,比较大小,在这里就是arr.Count和i,如果i小于arr.Count,则//跳转到IL_000a
IL_001d:blt.sIL_000a
//不用多说,退出方法
IL_001f:ret
}

上面就是Main这个方法对应的IL代码,从注释中可以看出,IL是基于一个运算栈的,所有的操作数就是从这运算栈里倒来倒去,运算栈也是一个虚拟的概念,
不要想着把它与物理计算机里的内存或寄存器相对应起来,因为IL本来就是运行在CLR这个虚拟机之上的。看了使用for访问的代码,我们来看看使用foreach访问的代码
(与上面相同的部分我就不注释了):

.methodprivatehidebysigstaticvoidMain(string[]args)cilmanaged
{
.entrypoint
//Codesize50(0x32)
.maxstack1
//声明四个局部变量啊
.localsinit([0]class[mscorlib]System.Collections.ArrayListarr,
[1]objectitem,
[2]class[mscorlib]System.Collections.IEnumeratorCS$5$0000,
[3]class[mscorlib]System.IDisposableCS$0$0001)
IL_0000:newobjinstancevoid[mscorlib]System.Collections.ArrayList::.ctor()
IL_0005:stloc.0
IL_0006:ldloc.0
//注意,调用ArrayList的GetEnumerator()方法,获取ArrayList的迭代器
IL_0007:callvirtinstanceclass[mscorlib]System.Collections.IEnumerator[mscorlib]System.Collections.ArrayList::GetEnumerator()
IL_000c:stloc.2
//呵呵,还加了一个try,为的是能在finally的时候调用Dispose()方法
.try
{
IL_000d:br.sIL_0016
IL_000f:ldloc.2
//不再是使用ArrayList的get_Item(int32)方法了,是使用迭代器的get_Current()
IL_0010:callvirtinstanceobject[mscorlib]System.Collections.IEnumerator::get_Current()
IL_0015:stloc.1
IL_0016:ldloc.2
//调用迭代器的MoveNext方法
IL_0017:callvirtinstancebool[mscorlib]System.Collections.IEnumerator::MoveNext()
IL_001c:brtrue.sIL_000f
IL_001e:leave.sIL_0031
}//end.try
finally
{
IL_0020:ldloc.2
IL_0021:isinst[mscorlib]System.IDisposable
IL_0026:stloc.3
IL_0027:ldloc.3
IL_0028:brfalse.sIL_0030
IL_002a:ldloc.3
IL_002b:callvirtinstancevoid[mscorlib]System.IDisposable::Dispose()
IL_0030:endfinally
}//endhandler
IL_0031:ret
}

哦,这样一比较,for和foreach的不同点就自然而然的明白了。对于这些“语法糖”,我们只要打开IL一切都明明白白,C# 3.0的语法糖为数众多,为什么很多
人知道他到底是怎样实现的呢,还有很多人叫嚷着,这没什么,语法糖而已,因为大家都明白IL没变啥,只是编译器使坏。

2.加密解密

加密解密在Win32时代就有之,不过自从.NET的出现这块就更容易了,很多加密的方法,只需要打开IL,然后改一点东西,再用微软提供的ILAsmer汇编回去,这样就破解了。
这块内容很复杂,园子里也有很多文章。

那学IL有没有用呢?当然有用。学习IL你可以看到表面上看不到的东西,如果你是一位痴迷于技术的同学,那IL就应该好好学学了,把CLR看做“计算机”,那IL就是CLR的“本地代码”
(实际上不正确,CLR自己并不直接运行IL,CLR还是需要将IL编译为真正的本地代码然后执行)。

.NET的水很深,实际上针对编译器前端这部分水不是很深。内核或原理部分都是在CLR这部分,比如程序集如何加载?方法如何编译?多态的实现方法?那要探究这部分的原理怎么办?这个
时候从IL就无法知晓了,当然你可以google之或百度之,你也可以买很多大牛的书籍看看。不过你想不想自己弄明白呢?毕竟如果不自己加以试验得出结论,那些东西我觉得还是书本上的,
我们获得的知识也是靠记忆获得的,如果你能使用一个调试工具,亲自打开JIT之后的结果,你就知道多态到底是咋回事?

要知后事,请听下回分解!

本来想写三篇的:

关注底层(上):IL部分

关注底层(中):汇编 for .NETer

关注底层(下):Why and How

不过发现我想讨论的内容老赵已经说了,而且说得更好,所以觉得后面两篇也没有必要出了。所以就留此一篇作为纪念吧。