.NET 2.0中的堆棧遍歷執行須知
堆棧遍歷
本文面向的是對構建用于檢查托管應用程序的分析器感興趣的讀者。我將描述如何編寫分析器,以在 .NET Framework 的公共語言運行庫 (CLR) 中遍歷托管堆棧。我將盡力保持輕松的心情,因為主題本身的進展有時會非常艱難。
在 CLR 的 2.0 版本中分析 API 有一個名為 DoStackSnapshot 的新方法,它允許分析器遍歷正在分析的應用程序的調用堆棧。CLR 的 1.1 版通過進程內調試接口提供了類似功能。但使用 DoStackSnapshot 遍歷調用堆棧更容易、更準確且更穩定。DoStackSnapshot 方法使用的堆棧遍歷器與垃圾收集器、安全系統、異常系統等使用的堆棧遍歷器相同。因此,您知道它必須運轉正常。
訪問完整的堆棧跟蹤可使分析器用戶在發生值得關注的事件時,對應用程序運行情況有一個全面的了解。根據應用程序及用戶想要分析的內容,您可以假設用戶在分配對象、加載類、引發異常時需要調用堆棧。即使所獲得的是應用程序事件以外事件(例如計時器事件)的調用堆棧,也仍然會引起采樣分析器的關注。如果您看到誰調用了包含熱點的函數,則查看代碼形式的熱點將會變得更加有啟迪作用。
我將側重于通過 DoStackSnapshot API 獲取堆棧跟蹤。獲取堆棧跟蹤的另一方法是通過構建影子堆棧:可掛接 FunctionEnter 和 FunctionLeave,以保存當前線程的托管調用堆棧的副本。如果您在應用程序執行期間始終需要堆棧信息,如果您不介意在每次執行托管調用及返回時運行分析器的代碼所產生的性能成本,則影子堆棧構建將會非常有用。如果很少需要報告堆棧(例如,為了響應事件),則 DoStackSnapshot 將是極佳的方法。即使采樣分析器每隔幾毫秒便拍一次堆棧快照,其頻率也要比構建影子堆棧低。因此,DoStackSnapshot 非常適合采樣分析器。
謹慎地進行堆棧遍歷
如果您希望能夠在需要時隨時獲取調用堆棧,這將非常有用。但是與能力隨之而來的還有責任。分析器用戶不會希望堆棧遍歷在運行時導致訪問違例 (AV) 或死鎖。作為分析器編寫者,您必須謹慎行使您的權力。我將討論如何使用 DoStackSnapshot,以及如何小心地執行此操作。如您所見,您想利用此方法執行的操作越多,操作就越難以正確執行。
讓我們看一下我們的主題。以下是分析器調用的內容(可在 Corprof.idl 的 ICorProfilerInfo2 接口中找到):
- HRESULT DoStackSnapshot(
- [in] ThreadID thread,
- [in] StackSnapshotCallback *callback,
- [in] ULONG32 infoFlags,
- [in] void *clientData,
- [in, size_is(contextSize), length_is(contextSize)] BYTE context[],
- [in] ULONG32 contextSize);
下列代碼是 CLR 在分析器上調用的內容(也可在 Corprof.idl 中找到)。向上例的 callback 參數中的此函數實現傳遞指針。
- typedef HRESULT __stdcall StackSnapshotCallback(
- FunctionID funcId,
- UINT_PTR ip,
- COR_PRF_FRAME_INFO frameInfo,
- ULONG32 contextSize,
- BYTE context[],
- void *clientData);
這像是一塊三明治。在分析器想要遍歷堆棧時,調用 DoStackSnapshot。在 CLR 從該調用返回之前,它調用 StackSnapshotCallback 函數多次,即,為堆棧上的每一個托管幀或每一組非托管幀調用一次該函數。圖 1 顯示了此三明治結構。
堆棧遍歷: 分析期間的調用“三明治”
正如您從我的注釋中所看到的,CLR 會將這些幀告知給您,但告知的順序與這些幀被推入到堆棧中的順序正好相反。即,最先告知葉節點幀(被最后推入),最后告知主節點幀(被最先推入)。
這些函數的所有參數有何意義?我不準備對它們進行逐一討論,但我將從 DoStackSnapshot 開始討論其中的一部分(我將利用一小部分時間討論余下部分)。infoFlags 值來自 Corprof.idl 中的 COR_PRF_SNAPSHOT_INFO 枚舉,它允許您控制 CLR 是否為您提供它所報告的幀的寄存器上下文。您可為 clientData 指定您所需要的任何值,并且 CLR 將在 StackSnapshotCallback 調用中返回該值。
在 StackSnapshotCallback 中,CLR 使用 funcId 參數向您傳遞當前遍歷的幀的 FunctionID 值。如果當前幀是一組非托管幀,則該值為 0(我稍后會加以介紹)。如果 funcId 值是一個非零值,則您可向其他方法(例如 GetFunctionInfo2 和 GetCodeInfo2)傳遞 funcId 和 frameInfo,以獲得有關該函數的更多信息。您可在堆棧遍歷過程中立即獲得此函數信息,或者保存 funcId 值并在以后獲取函數信息,以減少對運行中的應用程序的影響。如果您是在以后獲取函數信息,請記住 frameInfo 值僅在為您提供的回調內有效。盡管可以保存 funcId 值以供以后使用,但是切勿保存 frameInfo 值以備日后使用。
當您從 StackSnapshotCallback 返回時,通常會返回 S_OK,并且 CLR 將繼續遍歷堆棧。如果需要的話,也可返回 S_FALSE,這將停止堆棧遍歷。然后,DoStackSnapshot 調用會返回 CORPROF_E_STACKSNAPSHOT_ABORTED。
【編輯推薦】