【BotR】CLR堆栈遍历(Stackwalking in CLR)
前言
在上一篇文章CLR类型系统概述里提到,当运行时挂起时, 垃圾回收会执行堆栈遍历器(stack walker)去拿到堆栈上值类型的大小和堆栈根。这里我们来翻译BotR里一篇专门介绍Stackwalking的文章,希望能加深理解。
顺便说一句,StackWalker
在中文里似乎还没有统一的翻译,Java里有把它翻译成堆栈步行器
,微软有的(机翻)文档把它翻译为堆栈查看器
,我这里暂且将它翻译为堆栈遍历器
,如有更合适的翻译,欢迎评论区指出。
.NET运行时之书(Book of the Runtime,简称BotR)是一系列描述.NET运行时的文档,2007年左右在微软内部创建,最初目的是为了帮助其新员工快速上手.NET运行时;随着.NET开源,BotR也被公开了出来,如果想深入理解CLR,这系列文章不可错过。
BotR系列目录:
[1] CLR类型加载器设计(Type Loader Design)
[2] CLR类型系统概述(Type System Overview)
[3] CLR堆栈遍历(Stackwalking in CLR)
CLR堆栈遍历(Stackwalking in CLR)
原文:https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/stackwalking.md
作者: Rudi Martin - 2008
翻译:几秋 (https://www.cnblogs.com/netry/)
CLR大量使用了一种称为堆栈遍历(或者也叫stack crawling)的技术,这涉及迭代特定线程的调用帧(call frames)序列,从最近的调用帧(线程的当前函数)后退到堆栈的底部。
运行时出于多种目的使用堆栈遍历:
- 在垃圾回收期间,运行时遍历所有线程的堆栈寻找托管根(局部变量在托管方法的帧中拥有对象的引用,需要被报告给GC,以保持对象活跃和跟踪,并且如果GC决定压缩堆,则可能跟踪它们的动向)。
- 在一些平台上,异常处理的过程中,会使用堆栈遍历器(第一遍寻找句柄,第二遍展开堆栈(unwinding the stack))。
- 各种各样的方法,通常是那些靠近某些公共托管API的方法,执行堆栈遍历以获取有关其调用者的信息(例如调用者的方法、类或者程序集)。
堆栈模型
在这里,我们定义了一些常用术语并描述了线程堆栈的典型布局。
逻辑上,一个堆栈被拆分成若干个帧(frame),每一帧代表若干函数(托管或非托管),这些函数要么是当前正在执行的,要么是已经调用了其它函数,正在等待返回。帧包含了其关联函数的特定调用所需的状态。通常包括局部变量的空间、调用另一个函数的推送参数、保存的调用者寄存器等。
帧的具体定义因平台而异,在很多平台上,并没有一个所有函数都严格遵守的帧格式定义(x86平台就是其中一个例子)。相反,编译器通常可以自由优化帧的具体格式,在这样的系统上,无法保证堆栈遍历返回100正确或者完整的结果(出于调试目的,会使用像pdb文件这样的符号表来填补空白,以便调试器可以生成更准确的堆栈跟踪)。
然而这对CLR来说不是一个问题,因为我们不需要完全广义(fully generalized)的的堆栈遍历,相反我们只对来自以下情况的帧感兴趣:
- 被托管的方法
- 在某种程度上,来自用于实现运行时本身的非托管代码
特别是不保证第三方非托管帧的保真度(fidelity),除非知道到这些帧在何处转换到运行时本身或从运行时本身转换出来(也就是我们感兴趣的一种帧)。
因为我们控制我们感兴趣帧的格式(我们稍后再详细讨论这个问题),我们可以确保这些帧可抓取(crawlable),且具有100%的保真度。唯一的额外要求是一种将不相交的运行时帧(disjoint groups of runtime frames)链接在一起的机制,这样我们就可以跳过任何干预的非托管帧(和不可抓取的)。
下图说明了包含所有帧类型的堆栈(请注意,本文档使用了一种惯例,即堆栈向叶(page)顶部增长):
使帧可抓取
托管帧
因为运行时拥有和控制JIT(Just-in-Time编译器),它可以安排托管方法始终留下可以抓取的帧。这里的一种解决方案是对所有方法使用严格的(rigid)帧格式。然而在实践中,这可能低效,尤其是对于小叶子(small leaf)方法(例如典型的属性访问器)。
因为方法的调用次数通常多于其帧被抓取的次数(抓取堆栈在运行时中是相对较少的,至少就通常调用方法的速率而言),用方法调用性能换取一些额外的抓取时间是有合理的。因此,JIT会为其编译的每个方法生成额外的元数据,其中包括足够的信息,供堆栈爬虫解码属于该方法的堆栈帧。
这些元数据可以通过以方法中某处的指令指针(instruction pointer)作为键,查找哈希表得到。JIT使用压缩技术来最小化这种额外的每方法元数据的影响。
给定几个重要寄存器的初始值(例如,基于 x86 的系统上的 EIP、ESP 和 EBP),堆栈爬虫可以定位托管方法和其关联的JIT元数据,并使用这些信息将寄存器值回滚到方法调用者中的当前值。用这种方式,可以从最近的调用者到最老的调用者,遍历一系列托管方法帧,此操作有时称为虚拟展开(virtual unwind)(虚拟的是因为我们实际上并没有更新ESP等的真实值,堆栈保持不变)。
运行时非托管帧
运行时(有)部分是以非托管代码实现的(例如coreclr.dll). 大多数这些代码的特殊之处在于,它是作为手动托管的代码运行,也就是说,它遵守托管代码的许多规则和协议,但以显式控制的方式。例如,此类代码可以显式地启用或禁用GC抢占模式(pre-emptive mode),并且需要相应地管理其对象引用的使用。
与托管代码进行这种谨慎交互的另一个区域是在堆栈遍历过程中。由于大多数运行时的非托管代码是用C++编写的,因此我们对方法帧格式的控制不如托管代码。同时,在很多情况下,运行时非托管帧包含了堆栈遍历期间非常重要的信息,这包括非托管函数在局部变量中保存对象引用(必须在垃圾回收期间报告)和异常处理的情况。
非托管函数不是试图使每个非托管帧变得抓取,而是将有趣的数据报告到堆栈爬虫,
与其试图使每个非托管帧可抓取,带有有趣信息的非托管函数,堆栈爬取将信息捆绑到数据结构中,将信息捆绑到称为Frame的数据结构中,这个名称非常有歧义,因此本文档总是将该数据结构变量称为大写的Frame。
Frame实际上是整个Frame类型层次结构的抽象基类。 Frame被子类型化,以表达堆栈遍历可能感兴趣的不同类型的信息。但是堆栈遍历器如何找到这些Frame,并且它们与托管方法使用的帧有何关系?
每个Frame都是单链表的一部分,单链表有一个next指针,指向这个线程的堆栈上下一个更老的Frame(或者是null,如果这个Frame以及是最老的了)。CLR Thread结构持有一个指向最新Frame的指针。非托管运行时代码可以根据需要通过操作线程(Thread)结构和Frame列表来推送(push)或弹出(pop)Frame。
按照这种方式,堆栈遍历器可以按照最新到最旧的顺序迭代非托管Frames, 但是托管和非托管的方法可以被交叉使用,并且处理后面跟着非托管Frames的所有托管帧将会出错,反之亦然,因为它不能准确地表示真正的调用序列。
为了解决这个问题,Frame被进一步限制,它们必须被分配到堆栈上的方法帧中,该方法帧将它们推送到Frame列表中。由于堆栈遍历器知道每个托管帧的堆栈边界,因此它可以执行简单的指针比较,以判断给定Frame是否比给定托管帧旧或新。
本质上,堆栈遍历器在解码当前帧后,对于下一个(更老的)帧总是有两种可能选择:通过寄存器集(register set)的虚拟展开(virtual unwind)确定下一个托管帧,或者线程Frame列表上的下一个更老的Frame。这可以通过判断哪个占用更靠近栈顶的栈空间来决定哪个合适。所涉及的(involved)实际计算是平台相关的,但通常转移(devolves)到一个或两个指针比较上。
当托管代码调用非托管运行时时,非托管目标方法通常会推送数种形式的转换Frame中的一种,这被下面两种情况需要:
- 记录调用托管方法的寄存器状态(以便堆栈遍历器在完成枚举(enumerating)非托管Frames后可以恢复托管帧的虚拟展开)。
- 许多情况下因为托管对象引用作为参数传递给非托管方法,必须在垃圾回收时报告给GC。
可用Frame类型及其用途的完整描述超出了本文档的范围,更多的细节可以在frames.h头文件里找到。
堆栈遍历器接口
完整的堆栈遍历接口仅公开给运行时非托管代码(System.Diagnostics.StackTrace
类是一个对托管代码可用的简化子集),典型的入口点是通过运行时 Thread类上的StackWalkFramesEx()
方法,这个方法的调用者要提供下面三个主要的输入:
- 一些上下文指示遍历的起点。 这是一个初始寄存器集(例如,如果你已暂停目标线程并可以在其上调用
GetThreadContext()
)或一个初始Frame(在你知道有问题的代码是在运行时非托管代码中的情况下)。 尽管大多数堆栈遍历都是从堆栈顶部进行的,但如果你可以确定正确的起始上下文,则可以从较低位置开始。 - 一个函数指针和其关联的上下文。函数是堆栈遍历器为每个有趣的帧调用提供的函数(按从最新到最旧的顺序), 提供的上下文值被传递给回调的每次调用,以便它可以在遍历期间记录或建立状态。
- 指示应触发回调的帧类型的标志。 这允许调用者指定仅应报告的纯托管方法帧。完整的列表请看threads.h (就在
StackWalkFramesEx()
声明的上方).
StackWalkFramesEx()
返回一个枚举值,该值指示遍历是否正常终止(到达堆栈基并用完要报告的方法),是否被某一种回调中止(回调函数将同一类型的枚举返回到堆栈遍历)或遇到一些其它错误。
除了传递给StackWalkFramesEx()
的上下文值之外,堆栈回调函数还传递了另一段上下文:CrawlFrame
,这个类定义在 stackwalk.h ,这个类包含了在堆栈遍历过程中收集的各种上下文。例如,CrawlFrame
为托管帧指示 MethodDesc*
,为非托管Frames指示 Frame*
。它还提供了通过虚拟展开帧推断出的当前寄存器集到该点。
实现细节
堆栈遍历实现的更多低级细节目前不在本文档的范围内。 如果您了解这些知识并愿意分享这些知识,请随时更新此文档。
【BotR】CLR堆栈遍历(Stackwalking in CLR)的更多相关文章
- 重温CLR(十六) CLR寄宿和AppDomain
寄宿(hosting)使任何应用程序都能利用clr的功能.特别要指出的是,它使现有应用程序至少能部分使用托管代码编写.另外,寄宿还为应用程序提供了通过编程来进行自定义和扩展的能力. 允许可扩展性意味着 ...
- Clr Via C#读书笔记---CLR寄宿和应用程序域
#1 CLR寄宿: 开发CLR时,Microsoft实际是将他实现成包含在一个dll中的COM服务器.Microsoft为CLR定义了一个标准的COM接口,并为该接口和COM服务器分配了GUID.安装 ...
- SQL Server CLR全功略之一---CLR介绍和配置
Microsoft SQL Server 现在具备与 Microsoft Windows .NET Framework 的公共语言运行时 (CLR) 组件集成的功能.CLR 为托管代码提供服务,例如跨 ...
- 【BotR】CLR类型系统
.NET运行时之书(Book of the Runtime,简称BotR)是一系列描述.NET运行时的文档,2007年左右在微软内部创建,最初目的是为了帮助其新员工快速上手.NET运行时:随着.NET ...
- CLR线程概览(一)
托管 vs. 原生线程 托管代码在“托管线程”上执行,(托管线程)与操作系统提供的原生线程不同.原生线程是在物理机器上执行的原生代码序列:而托管线程则是在CLR虚拟机上执行的虚拟线程. 正如JIT解释 ...
- CLR总览
Contents 第1章CLR的执行模型... 4 1.1将源代码编译成托管代码模块... 4 1.2 将托管模块合并成程序集... 6 1.3加载公共语言运行时... 7 1.4执行程序集的代码.. ...
- CLR寄宿和AppDomain
一.CLR寄宿 .net framework在windows平台的顶部允许.者意味着.net framework必须用windows能理解的技术来构建.所有托管模块和程序集文件必须使用windows ...
- 第22章 CLR寄宿和AppDomain
22.1 CLR寄宿 CLR Hosting(CLR 宿主)的概念:初始启动.Net Application时,Windows进程的执行和初始化跟传统的Win32程序是一样的,执行的还是非托管代码,只 ...
- CLR内部异常(下)
直接使用SEH 有些情况里直接使用SEH会更合适一些.特别是,如果需要在第一次遍历(first pass - SEH异常处理流程里的第一遍处理)时需要执行某些操作时,也就是在堆栈向上展开之前,SEH是 ...
随机推荐
- 5-7 分页查询PageHelper
1. PageHelper实现分页查询 Day08 1.1 PH作用: PageHelper框架可以实现我们提供页码和每页条数, 自动实现分页效果,收集分页信息 1.2 PH原理: PageHelpe ...
- noi-2.2基本算法之递归和自调用函数:放苹果
先看一下题目: http://noi.openjudge.cn/ch0202/666/http://noi.openjudge.cn/ch0202/666/ 把M个同样的苹果放在N个同样的盘子里,允许 ...
- maven exclusion 理解
结论:exclusion 表示对传递性依赖进行排除,排除后当前项目的依赖jar中,就不会包含该传递性依赖. 扩展:项目中的jar 都会在classpath下,排除后的传递性依赖,相当于在classpa ...
- H5移动端实现一键复制或长摁复制
今天接到了一个新的需求,要求我们对表单中的某一个字段进行复制,这个表单是不可选的,拿到需求的时候有点懵,不清楚下手点在哪,后来网上找了找,终于有了点眉目,感觉网上有些是实现不了的,特地在这里记录下进行 ...
- linux-0.11分析:init文件 main.c的第一个初始化函数mem_int 第四篇随笔
init文件夹 mian.c 参考 [github这个博主的 厉害][ https://github.com/sunym1993/flash-linux0.11-talk ] 首先先看看这个mian. ...
- Flutter-填平菜鸟和高手之间的沟壑
Flutter-填平菜鸟和高手之间的沟壑 准备写作中... 1.Flutter-skia-影像,Flutter skia-图形渲染层.应用渲染层2.方法通道使用示例,用于演示如何使用方法通道实现与原生 ...
- identity4 系列————启航篇[二]
前言 开始identity的介绍了. 正文 前文介绍了一些概念,如果概念不清的话,可以去前文查看. https://www.cnblogs.com/aoximin/p/13475444.html 对一 ...
- [网鼎杯2018]Unfinish-1|SQL注入|二次注入
1.进入题目之后只有一个登录界面,检查源代码信息并没有发现有用的信息,尝试万能密码登录也不行,结果如下: 2.进行目录扫描,发现了注册界面:register.php,结果如下: 3.那就访问注册界面, ...
- springBoot项目实现发送邮件功能
需要的依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId> ...
- 深入理解 Spring 事务:入门、使用、原理
大家好,我是树哥. Spring 事务是复杂一致性业务必备的知识点,掌握好 Spring 事务可以让我们写出更好地代码.这篇文章我们将介绍 Spring 事务的诞生背景,从而让我们可以更清晰地了解 S ...