当前位置: 首页 > 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实战之密码验证
  • 玩转“网上邻居”之网络配置(一)
  • 标准整数类型的取值范围
  • 玩转“网上邻居”之网络配置(二)
  • __proto__ 和 prototype的关系
  • 【跃迁之路】【444天】程序员高效学习方法论探索系列(实验阶段201-2018.04.25)...
  • HTML-表单
  • JDK9: 集成 Jshell 和 Maven 项目.
  • Linux快速配置 VIM 实现语法高亮 补全 缩进等功能
  • Linux中的硬链接与软链接
  • Vim 折腾记
  • 对象引论
  • 猫头鹰的深夜翻译:JDK9 NotNullOrElse方法
  • 三分钟教你同步 Visual Studio Code 设置
  • 设计模式走一遍---观察者模式
  • 译自由幺半群
  • 格斗健身潮牌24KiCK获近千万Pre-A轮融资,用户留存高达9个月 ...
  • 通过调用文摘列表API获取文摘
  • ​​​​​​​​​​​​​​Γ函数
  • #ifdef 的技巧用法
  • #宝哥教你#查看jquery绑定的事件函数
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (vue)el-checkbox 实现展示区分 label 和 value(展示值与选中获取值需不同)
  • (二)正点原子I.MX6ULL u-boot移植
  • (附源码)spring boot基于Java的电影院售票与管理系统毕业设计 011449
  • (附源码)springboot车辆管理系统 毕业设计 031034
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (实战)静默dbca安装创建数据库 --参数说明+举例
  • (未解决)macOS matplotlib 中文是方框
  • (一)Neo4j下载安装以及初次使用
  • (转)setTimeout 和 setInterval 的区别
  • (转)Sublime Text3配置Lua运行环境
  • .Net Core/.Net6/.Net8 ,启动配置/Program.cs 配置
  • .NET Framework 3.5中序列化成JSON数据及JSON数据的反序列化,以及jQuery的调用JSON
  • .net 写了一个支持重试、熔断和超时策略的 HttpClient 实例池
  • .NET/C# 获取一个正在运行的进程的命令行参数
  • .NET处理HTTP请求
  • /proc/stat文件详解(翻译)
  • @EnableWebMvc介绍和使用详细demo
  • [ vulhub漏洞复现篇 ] GhostScript 沙箱绕过(任意命令执行)漏洞CVE-2019-6116
  • [1] 平面(Plane)图形的生成算法
  • [23] GaussianAvatars: Photorealistic Head Avatars with Rigged 3D Gaussians
  • [AIGC] Spring Interceptor 拦截器详解
  • [BZOJ2850]巧克力王国