【BotR】CLR堆棧遍歷(Stackwalking in CLR)

来源:https://www.cnblogs.com/netry/archive/2022/09/25/clr-stack-worker-chinese.html
-Advertisement-
Play Games

前言 在上一篇文章CLR類型系統概述里提到,當運行時掛起時, 垃圾回收會執行堆棧遍歷器(stack walker)去拿到堆棧上值類型的大小和堆棧根。這裡我們來翻譯BotR里一篇專門介紹Stackwalking的文章,希望能加深理解。 順便說一句,StackWalker在中文里似乎還沒有統一的翻譯,J ...


前言

在上一篇文章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)頂部增長):
image

使幀可抓取

托管幀

因為運行時擁有和控制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()方法,這個方法的調用者要提供下麵三個主要的輸入:

  1. 一些上下文指示遍歷的起點。 這是一個初始寄存器集(例如,如果你已暫停目標線程並可以在其上調用GetThreadContext())或一個初始Frame(在你知道有問題的代碼是在運行時非托管代碼中的情況下)。 儘管大多數堆棧遍歷都是從堆棧頂部進行的,但如果你可以確定正確的起始上下文,則可以從較低位置開始。
  2. 一個函數指針和其關聯的上下文。函數是堆棧遍歷器為每個有趣的幀調用提供的函數(按從最新到最舊的順序), 提供的上下文值被傳遞給回調的每次調用,以便它可以在遍歷期間記錄或建立狀態。
  3. 指示應觸發回調的幀類型的標誌。 這允許調用者指定僅應報告的純托管方法幀。完整的列表請看threads.h (就在StackWalkFramesEx()聲明的上方).

StackWalkFramesEx()返回一個枚舉值,該值指示遍歷是否正常終止(到達堆棧基並用完要報告的方法),是否被某一種回調中止(回調函數將同一類型的枚舉返回到堆棧遍歷)或遇到一些其它錯誤。

除了傳遞給StackWalkFramesEx() 的上下文值之外,堆棧回調函數還傳遞了另一段上下文:CrawlFrame,這個類定義在 stackwalk.h ,這個類包含了在堆棧遍歷過程中收集的各種上下文。例如,CrawlFrame為托管幀指示 MethodDesc* ,為非托管Frames指示 Frame*。它還提供了通過虛擬展開幀推斷出的當前寄存器集到該點。

實現細節

堆棧遍歷實現的更多低級細節目前不在本文檔的範圍內。 如果您瞭解這些知識並願意分享這些知識,請隨時更新此文檔。

作者: 幾秋

出處: https://www.cnblogs.com/netry/p/clr-stack-worker-chinese.html

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 拉格朗日插值原理及實現(Python) 一. 前言 Lagrange插值是利用n次多項式來擬合**(n+1)個數據點**從而得到插值函數的方法。(註意n次多項式的定義是未知數最高次冪為n,但是多項式繫數有n+1個,因為還有個常數項) **Lagrange插值和Newton插值本質上相同,都是用(n- ...
  • ###一、背景知識 爬蟲的本質就是一個socket客戶端與服務端的通信過程,如果我們有多個url待爬取,只用一個線程且採用串列的方式執行,那隻能等待爬取一個結束後才能繼續下一個,效率會非常低。 需要強調的是:對於單線程下串列N個任務,並不完全等同於低效,如果這N個任務都是純計算的任務,那麼該線程對c ...
  • ##Invalid bound statement (not found)出現原因和解決方法 ###前言: 想必各位小伙伴在碼路上經常會碰到奇奇怪怪的事情,比如出現Invalid bound statement (not found),那今天我就來分析以下出現此問題的原因。 其實出現這個問題實質就是 ...
  • 二叉樹查找指定的節點 前序查找的思路 1.先判斷當前節點的no是否等於要查找的 2.如果是相等,則返回當前節點 3.如果不等,則判斷當前節點的左子節點是否為空,如果不為空,則遞歸前序查找 4.如果左遞歸前序查找,找到節點,則返回,否繼續判斷,當前的節點的右子節點是否為空,如果不為空,則繼續向右遞歸前 ...
  • 說明 onlyoffice為一款開源的office線上編輯組件,提供word/excel/ppt編輯保存操作 以下操作均基於centos8系統,officeonly鏡像版本7.1.2.23 鏡像下載地址:https://yunpan.360.cn/surl_y87CKKcPdY4 (提取碼:1f92 ...
  • 首先CR3是什麼,CR3是一個寄存器,該寄存器內保存有頁目錄表物理地址(PDBR地址),其實CR3內部存放的就是頁目錄表的記憶體基地址,運用CR3切換可實現對特定進程記憶體地址的強制讀寫操作,此類讀寫屬於有痕讀寫,多數驅動保護都會將這個地址改為無效,此時CR3讀寫就失效了,當然如果能找到CR3的正確地址... ...
  • 什麼是Git Git 是一個開源的分散式版本控制系統,用於敏捷高效地處理任何或小或大的項目。 Git 是 Linus Torvalds 為了幫助管理 Linux 內核開發而開發的一個開放源碼的版本控制軟體。 Git 與常用的版本控制工具 CVS, Subversion 等不同,它採用了分散式版本庫的 ...
  • 使用過 nginx 的小伙伴應該都知道,這個中間件是可以設置跨域的,作為今天的主角,同樣的 反向代理中間件的 YARP 毫無意外也支持了跨域請求設置。 有些小伙伴可能會問了,怎樣才算是跨域呢? 在 HTML 中,一些標簽,例如 img、a 等,還有我們非常熟悉的 Ajax,都是可以指向非本站的資源的 ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...