当前位置: 首页 > news >正文

.NET值类型变量“活”在哪?

.NET值类型变量“活”在哪个堆栈中?
——MSIL学习笔记(一)
金旭亮
不管是什么语言编的.NET程序,最后都会被各自的编译器编译成MSIL。当程序运行时,.NET JIT编译器从程序集中读入IL指令并将其动态编译为可被本地CPU执行的机器指令再执行。
程序集中的IL代码以二进制方式存在,人阅读起来相当不便,正如传统的Win32程序可以被反汇编成汇编程序,.NET程序集中的IL代码也可以被反汇编成易于阅读的IL汇编程序。如果您愿意的话,可以用任意一个文本编辑器直接撰写IL汇编源代码,然后使用ilasm.exe程序将其编译为包含二进制形式的IL指令。CLR只能执行二进制的IL指令。
.NET SDK的另一个工具ildasm.exe可以用于将一个程序集反汇编为IL程序,在学习.NET时,这个工具非常有用,可以展示出高级语言(如C#和VB.NET)编写的程序是如何被CLR执行的。
然而,相比C#和VB.NET的资料满天飞,MSIL的技术资料少得可怜。我能够查阅的只有MSDN中有关IL指令的文档(还只是针对Reflection.Emit名字空间中的类的),以及一本由Serge Lidin著的《inside Microsoft .NET IL assembler》, Serge Lidin是汇编器ilasm.exe工具的主要开发者,因此,他的书应具有相当的权威性,然而,这位技术牛人的写作水平实在不敢恭维,整本书象是一本参考手册。此书国内引进了中文版,然而翻译得很不好。幸运的是其光盘中附上了英文原版,实乃国人之大幸。
IL可以看成是一个“面向对象的汇编语言”,它提供了许多指令直接对对象进行操作,比如newobj指令创建对象,box指令进行装箱等。
IL指令的一个最重要特性是它是基于堆栈的。几乎每一条指令都要与堆栈打交道:或者向堆栈中Push一些数据,或者从中Pop一些数据。
请看以下C#代码段:
class Program
{
static void Main(string[] args)
{
int i = 100;
int j = 200;
int reslut = i + j;
}
}
C#编译器将生成以下IL指令,其功能我在注释中有详细说明:
.method private hidebysig static voidMain(string[] args) cil managed
{
.entrypoint
// 代码大小 15 (0xf)
.maxstack2
.locals init ([0] int32 i,
[1] int32 j,
[2] int32 reslut)
IL_0000:nop
IL_0001:ldc.i4.s 100 //将100压入堆栈
IL_0003:stloc.0 //从堆栈中弹出先前压入的100,传给局部变量i
IL_0004:ldc.i4 0xc8 //将200压入堆栈
IL_0009:stloc.1 //从堆栈中弹出先前压入的200,传给局部变量j
IL_000a:ldloc.0 //将局部变量i的值压入堆栈
IL_000b:ldloc.1 //将局部变量j的值压入堆栈
IL_000c:add //连继弹出两个整数,相加得300,又压入堆栈
IL_000d:stloc.2 //从堆栈中弹出结果,保存到局部变量reslut中
IL_000e:ret //返回指令
} // end of method Program::Main
可以看到,所有的指令都涉及到堆栈。
然而,我在研究IL汇编程序的时候,却被“堆栈”两个字弄糊涂了。
几乎所有的C#书,都说值类型变量是生存在堆栈中,当函数结束时会自动销毁。那么,这里的堆栈与上述IL代码中的堆栈是不是一回事?
请看上述IL程序中有一个MaxStack指令,查看资料,得知其含义是为evaluation stack保留两个槽(slot),注意,这里的堆栈英文原文是evaluation stack,MSDN中文版译为“计算堆栈”,slot可用于存放值对象,大小是可变的。换句话说,evaluation stack中的每一个slot可以存放一个值对象(对象引用也可看成是一种“特殊”的值变量,其值代表内存地址)或各种CLR直接支持的基本类型数据。
从上述IL程序中可以很明显地看到,局部变量i,j和result绝不会生存于evaluation stack,因为它只有2个slot,而我们有3个变量。那它们“活在”在哪儿?
IL程序中引人注目的一句是locals init指令,这提醒我们函数拥有另一块内存区域专用于存放局部变量,所以,声明为局部变量的值类型并不“活”在evaluation stack中。那么,为何所有的 C#书(包括大名鼎鼎的Jeffrey Richter所著之《.NET框架程序设计》)都说值类型变量“活”在堆栈中?此堆栈在哪?至少有一点可以肯定,这个堆栈不会指的是evaluation stack。
用ildasm.exe查看程序集清单(manifest),发现其中有一句:
.stackreserve 0x00100000
上述语句让CLR在装入程序集时保存1M的堆栈空间,这个空间供托管进程的托管线程使用,称为线程堆栈(Thread Stack)。既是线程堆栈,自然与线程相关,由于.NET托管进程可以创建多个托管线程,因此,每个线程也应该有自己的堆栈(Jeffrey Richter说也是1M,查看也是这位老先生写的《Windows核心编程》,说在Win2000在创建线程时其堆栈大小是可调整的)。
.NET下每个托管线程都对应着一个线程函数,因此函数中定义的局部变量是在它拥有的线程堆栈中分配,而IL程序中的maxstack指令则从这一个1M的线程堆栈中再划出一块空间来作为evaluation stack。
考虑一下函数调用的问题。
IL使用call和callvirt两条指令调用特定类型所提供的方法。这就有一个函数参数传送的问题。以call指令为例,MSDN说在调用call指令之前,要将所有的实参压入evaluation stack,然后call指令再将其弹出,之后控制才会转到被调用的函数,而当被调用的函数执行完毕时,ret指令负责“将函数的返回值”从“被调用者的堆栈”(callee’s evaluation stack)复制到“调用者堆栈”(caller evaluation stack)中。您看MSDN文档中居然又出现了两个堆栈,是否有点晕了吗?
查看Serge Lidin的书,他给出了这样一个图:
如上图所示:CLR会给每一个被调用的方法分配三块内存,除了上面讲到的两块(Evaluation stack和局部变量表Local Variable table),还有一块是参数表(Argument table)。
问题终于明晰了,call指令完成的工作应该是这样的:
调用者按要调用函数的参数准备好实参,将它们压入“自己的”evaluation stack中,然后,call指令执行,它从调用者的evaluation stack弹出这些参数,放入被调用函数的Argument Table中。一切准备工作就绪,这时才开始执行被调用函数的第一条IL指令。
当被调用函数执行完毕,如果有返回值,这个值应该被放在被调用函数自己的evaluation stack中(因为IL指令总是与堆栈打交道),然后,ret指令(每个函数最后一定是这条指令)将其弹出,再压入调用者的evaluation stack中,完成这一工作之后,执行流程转回到调用者。
因此,线程每调用一个函数,将导致图中所示的三块区域在1M的线程堆栈中分配给调用函数,对于递归调用的情况,后调用的函数占用的内存区域将“压”在其调用者内存区域之上,每执行完一个函数,对应的栈顶指针移动一个位移(大小刚好等于此函数先前所占用的内存),从而导致这些内存被释放,其中的局部变量不再有效。
分析.NET程序的IL指令还会得到一些有趣的结果,后面我会有更多的文章与网友们进行技术交流。
注:由于手头的资料不足, 此文所述内容仅是本人对CLR内部运行机理的一个推测,如有错误,敬请指正。by the way,望有网友能提供更多的MSIL技术资料信息,在此谢谢了。:-)
转载请注明作者及出处。

相关文章:

  • Lua do-end
  • VS Code 安装 VSIX 插件
  • 平台为王:Microsoft Office System为什么成功?
  • VS Code 对 Lua 代码格式化
  • 2020-拥抱经历,磨砺己身
  • 个人理财规划五步曲
  • AS报错:Didn‘t find class “okhttp3.OkHttpClient$Builder“ on path: DexPathList[[...]]
  • 九段理财:投资人才是高手
  • 视频格式转换软件 XMedia Recode
  • 40年:你也能成为亿万富翁
  • Lua 字符与ASCII码互转
  • Lua实战之密码验证
  • 玩转“网上邻居”之网络配置(一)
  • 标准整数类型的取值范围
  • 玩转“网上邻居”之网络配置(二)
  • C语言笔记(第一章:C语言编程)
  • Docker: 容器互访的三种方式
  • go语言学习初探(一)
  • MySQL QA
  • React as a UI Runtime(五、列表)
  • spark本地环境的搭建到运行第一个spark程序
  • XML已死 ?
  • Zepto.js源码学习之二
  • 仿天猫超市收藏抛物线动画工具库
  • 欢迎参加第二届中国游戏开发者大会
  • 看域名解析域名安全对SEO的影响
  • 买一台 iPhone X,还是创建一家未来的独角兽?
  • 容器镜像
  • ​香农与信息论三大定律
  • # SpringBoot 如何让指定的Bean先加载
  • ###C语言程序设计-----C语言学习(3)#
  • #NOIP 2014# day.1 T3 飞扬的小鸟 bird
  • #pragam once 和 #ifndef 预编译头
  • $(function(){})与(function($){....})(jQuery)的区别
  • (1)STL算法之遍历容器
  • (10)ATF MMU转换表
  • (android 地图实战开发)3 在地图上显示当前位置和自定义银行位置
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (附源码)springboot青少年公共卫生教育平台 毕业设计 643214
  • (附源码)计算机毕业设计SSM疫情下的学生出入管理系统
  • (亲测成功)在centos7.5上安装kvm,通过VNC远程连接并创建多台ubuntu虚拟机(ubuntu server版本)...
  • (算法)求1到1亿间的质数或素数
  • (转)chrome浏览器收藏夹(书签)的导出与导入
  • (转)IIS6 ASP 0251超过响应缓冲区限制错误的解决方法
  • ***测试-HTTP方法
  • **PyTorch月学习计划 - 第一周;第6-7天: 自动梯度(Autograd)**
  • .CSS-hover 的解释
  • .Net Attribute详解(上)-Attribute本质以及一个简单示例
  • .Net 垃圾回收机制原理(二)
  • .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)
  • .NET开源全面方便的第三方登录组件集合 - MrHuo.OAuth
  • .NET开源项目介绍及资源推荐:数据持久层
  • .stream().map与.stream().flatMap的使用
  • .vimrc 配置项
  • @DateTimeFormat 和 @JsonFormat 注解详解