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