附加和中斷連結程式碼剖析工具
在 .NET Framework 4 版之前的 .NET Framework 版本中,應用程式及其程式碼剖析工具必須同時載入。 當應用程式啟動時,執行階段就會檢查環境變數和登錄設定,藉以判斷是否應該剖析應用程式,然後載入必要的程式碼剖析工具。 接著,此程式碼剖析工具會保留在處理序空間中,直到應用程式結束為止。
從 .NET Framework 4 開始,您可以在應用程式已經啟動之後,將程式碼剖析工具附加至應用程式、剖析應用程式,然後在應用程式結束之前,中斷連結程式碼剖析工具。 中斷連結程式碼剖析工具之後,應用程式會繼續以附加程式碼剖析工具之前的方式執行。 換言之,暫時存在處理序空間中的程式碼剖析工具不會留下任何追蹤。
此外,從 .NET Framework 4 開始,程式碼剖析附加和中斷連結方法以及相關的程式碼剖析 API 增強功能可讓您將以程式碼剖析工具為基礎的工具當做 Just-In-Time 立即可用的診斷工具使用。
下列各節將討論這些增強功能:
附加程式碼剖析工具
附加詳細資料
逐步附加
找出 AttachProfiler 方法
載入執行階段之前附加至目標應用程式
附加限制
程式碼剖析工具附加範例
附加之後初始化程式碼剖析工具
完成程式碼剖析工具附加
中斷連結程式碼剖析工具
中斷連結期間的執行緒考量
逐步中斷連結
中斷連結限制
偵錯
附加程式碼剖析工具
若要將程式碼剖析工具附加至執行中應用程式,需要使用兩個個別的處理序:觸發處理序和目標處理序。
觸發處理序是一個可執行檔,可將程式碼剖析工具 DLL 載入執行中應用程式的處理序空間中。 觸發處理序會使用 ICLRProfiling::AttachProfiler 方法來要求 Common Language Runtime (CLR) 載入程式碼剖析工具 DLL。 當程式碼剖析工具 DLL 已經載入目標處理序中之後,觸發處理序可能會依照程式碼剖析工具開發人員的決定而保留或結束。
目標處理序包含您想要剖析的應用程式以及 CLR。 呼叫 AttachProfiler 方法之後,程式碼剖析工具就會連同目標應用程式一起載入這個處理序空間中。
附加詳細資料
AttachProfiler 會將指定的程式碼剖析工具附加至指定的處理序,並選擇性地傳遞一些資料給程式碼剖析工具。 它會等候指定的逾時,讓附加完成。
在目標處理序中,在附加時載入程式碼剖析工具的程序與在啟動時載入程式碼剖析工具的程序很相似:CLR CoCreates 給定的 CLSID、查詢 ICorProfilerCallback3 介面,並且呼叫 ICorProfilerCallback3::InitializeForAttach 方法。 如果這些作業都在指定的逾時內成功完成,AttachProfiler 就會傳回 S_OK HRESULT。 因此,觸發處理序應該選擇足以讓程式碼剖析工具完成初始化的逾時。
注意事項 |
---|
因為目標處理序中的完成項執行時間超過逾時值,而產生 HRESULT_FROM_WIN32 (ERROR_TIMEOUT),所以可能會發生逾時。如果您收到這項錯誤,可以重試附加作業。 |
如果附加完成之前超過逾時,AttachProfiler 就會傳回 HRESULT 錯誤。不過,附加可能已經成功。 在這類情況下,還有其他方式可判斷附加是否成功。 程式碼剖析工具開發人員通常會在其觸發處理序與目標應用程式之間設有自訂通訊通道。 這類通訊通道可用來偵測成功的附加。 觸發處理序也可以再次呼叫 AttachProfiler 並接收 CORPROF_E_PROFILER_ALREADY_ACTIVE,藉以偵測成功。
此外,目標處理序必須擁有足夠的存取權限,才能載入程式碼剖析工具的 DLL。 不過,附加至在存取權限受限之帳戶 (例如 NETWORK SERVICE) 底下執行的服務 (例如 W3wp.exe 處理序) 時,這可能會發生問題。 在這類情況下,檔案系統上的某些目錄可能會有安全性限制,讓目標處理序無法存取。 因此,程式碼剖析工具開發人員必須負責將程式碼剖析工具 DLL 安裝在允許適當存取的位置中。
失敗會記錄至應用程式事件記錄檔。
逐步附加
觸發處理序會呼叫 AttachProfiler,並識別目標處理序以及要載入的程式碼剖析工具,並且在附加之後選擇性地傳遞要提供給程式碼剖析工具的資料。
在目標處理序中,CLR 會接收附加要求。
在目標處理序中,CLR 會建立程式碼剖析工具物件並從中取得 ICorProfilerCallback3 介面。
然後,CLR 會呼叫 InitializeForAttach 方法,並傳遞觸發處理序加入附加要求中的資料。
程式碼剖析工具會初始化它本身、啟用它所需要的回呼,並成功傳回。
在觸發處理序中,AttachProfiler 會傳回 S_OK HRESULT,表示程式碼剖析工具已經成功附加。 觸發處理序將不再相關。
從這點開始,程式碼剖析會繼續進行,如同所有先前的版本一樣。
找出 AttachProfiler 方法
觸發處理序可以依照下列步驟找出 AttachProfiler 方法:
呼叫 LoadLibrary (英文) 方法來載入 mscoree.dll 並尋找 CLRCreateInstance 方法。
使用 CLSID_CLRMetaHost 和 IID_ICLRMetaHost 引數來呼叫 CLRCreateInstance 方法,以便傳回 ICLRMetaHost 介面。
針對處理序中的每個 CLR 執行個體,呼叫 ICLRMetaHost::EnumerateLoadedRuntimes 方法來擷取 ICLRRuntimeInfo 介面。
逐一查看 ICLRRuntimeInfo 介面,直到找到要附加程式碼剖析工具的所需介面為止。
使用 IID_ICLRProfiling 引數來呼叫 ICLRRuntimeInfo::GetInterface 方法,以便取得 ICLRProfiling 介面,而這個介面會提供 AttachProfiler 方法。
載入執行階段之前附加至目標應用程式
目前不支援這項功能。 如果觸發處理序嘗試透過指定尚未載入執行階段的處理序來呼叫 AttachProfiler 方法,AttachProfiler 方法就會傳回失敗 HRESULT。 如果使用者想要從載入執行階段開始就剖析應用程式,就應該先設定適當的環境變數 (或執行程式碼剖析工具開發人員針對此作業所撰寫的應用程式),然後再啟動目標應用程式。
附加限制
只有 ICorProfilerCallback 和 ICorProfilerInfo 方法的子集可用來附加程式碼剖析工具,如下列各節所述。
ICorProfilerCallback 限制
當程式碼剖析工具呼叫 ICorProfilerInfo::SetEventMask 方法時,它必須單獨指定出現在 COR_PRF_ALLOWABLE_AFTER_ATTACH 列舉中的事件旗標。 沒有其他回呼可用來附加程式碼剖析工具。 如果附加的程式碼剖析工具嘗試指定不在 COR_PRF_ALLOWABLE_AFTER_ATTACH 位元遮罩中的旗標,ICorProfilerInfo::SetEventMask 就會傳回 CORPROF_E_UNSUPPORTED_FOR_ATTACHING_PROFILER HRESULT。
ICorProfilerInfo 限制
如果透過呼叫 AttachProfiler 方法所載入的程式碼剖析工具嘗試呼叫下列任何限制的 ICorProfilerInfo 或 ICorProfilerInfo2 方法,該方法就會傳回 CORPROF_E_UNSUPPORTED_FOR_ATTACHING_PROFILER HRESULT。
限制的 ICorProfilerInfo 方法:
限制的 ICorProfilerInfo2 方法:
限制的 ICorProfilerInfo3 方法:
程式碼剖析工具附加範例
這個範例會假設觸發處理序已經知道目標處理序的處理序識別項 (PID)、它想要等候逾時的毫秒數、要載入之程式碼剖析工具的 CLSID,以及程式碼剖析工具之 DLL 檔案的路徑。 它也會假設程式碼剖析工具開發人員已經定義了名為 MyProfilerConfigData 的結構 (保存程式碼剖析工具的組態資料) 以及名為 PopulateMyProfilerConfigData 的函式 (將組態資料放入該結構中)。
HRESULT CallAttachProfiler(DWORD dwProfileeProcID, DWORD dwMillisecondsTimeout,
GUID profilerCLSID, LPCWSTR wszProfilerPath)
{
// This can be a data type of your own choosing for sending configuration data
// to your profiler:
MyProfilerConfigData profConfig;
PopulateMyProfilerConfigData(&profConfig);
LPVOID pvClientData = (LPVOID) &profConfig;
DWORD cbClientData = sizeof(profConfig);
ICLRMetaHost * pMetaHost = NULL;
IEnumUnknown * pEnum = NULL;
IUnknown * pUnk = NULL;
ICLRRuntimeInfo * pRuntime = NULL;
ICLRProfiling * pProfiling = NULL;
HRESULT hr = E_FAIL;
hModule = LoadLibrary(L"mscoree.dll");
if (hModule == NULL)
goto Cleanup;
CLRCreateInstanceFnPtr pfnCreateInterface =
(CLRCreateInstanceFnPtr)GetProcAddress(hModule, "CLRCreateInterface");
if (pfnCreateInterface == NULL)
goto Cleanup;
hr = (*pfnCreateInterface)(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID *)&pMetaHost);
if (FAILED(hr))
goto Cleanup;
hr = pMetaHost->EnumerateLoadedRuntimes(hProcess, &pEnum);
if (FAILED(hr))
goto Cleanup;
while (pEnum->Next(1, &pUnk, NULL) == S_OK)
{
hr = pUnk->QueryInterface(IID_ICLRRuntimeInfo, (LPVOID *) &pRuntime);
if (FAILED(hr))
{
pUnk->Release();
pUnk = NULL;
continue;
}
WCHAR wszVersion[30];
DWORD cchVersion = sizeof(wszVersion)/sizeof(wszVersion[0]);
hr = pRuntime->GetVersionString(wszVersion, &cchVersion);
if (SUCCEEDED(hr) &&
(cchVersion >= 3) &&
((wszVersion[0] == L'v') || (wszVersion[0] == L'V')) &&
((wszVersion[1] >= L'4') || (wszVersion[2] != L'.')))
{
hr = pRuntime->GetInterface(CLSID_CLRProfiling, IID_ICLRProfiling, (LPVOID *)&pProfiling);
if (SUCCEEDED(hr))
{
hr = pProfiling->AttachProfiler(
dwProfileeProcID,
dwMillisecondsTimeout,
profilerCLSID,
wszProfilerPath,
pvClientData,
cbClientData
);
pProfiling->Release();
pProfiling = NULL;
break;
}
}
pRuntime->Release();
pRuntime = NULL;
pUnk->Release();
pUnk = NULL;
}
Cleanup:
if (pProfiling != NULL)
{
pProfiling->Release();
pProfiling = NULL;
}
if (pRuntime != NULL)
{
pRuntime->Release();
pRuntime = NULL;
}
if (pUnk != NULL)
{
pUnk->Release();
pUnk = NULL;
}
if (pEnum != NULL)
{
pEnum->Release();
pEnum = NULL;
}
if (pMetaHost != NULL)
{
pMetaHost->Release();
pMetaHost = NULL;
}
if (hModule != NULL)
{
FreeLibrary(hModule);
hModule = NULL;
}
return hr;
}
附加之後初始化程式碼剖析工具
從 .NET Framework 4 開始,ICorProfilerCallback3::InitializeForAttach 方法已提供成為 ICorProfilerCallback::Initialize 方法的附加對應項目。 CLR 會呼叫 InitializeForAttach,讓程式碼剖析工具能夠在附加之後初始化其狀態。 在這個回呼中,程式碼剖析工具會呼叫 ICorProfilerInfo::SetEventMask 方法來要求一個或多個事件。 與 ICorProfilerCallback::Initialize 方法期間所進行的呼叫不同,從 InitializeForAttach 所進行的 SetEventMask 方法呼叫只能設定位於 COR_PRF_ALLOWABLE_AFTER_ATTACH 位元遮罩中的位元 (請參閱附加限制)。
一旦從 InitializeForAttach 收到錯誤碼之後,CLR 就會將失敗記錄至 Windows 應用程式事件記錄檔、釋放程式碼剖析工具回呼介面,然後卸載程式碼剖析工具。 AttachProfiler 與 InitializeForAttach 會傳回相同的錯誤碼。
完成程式碼剖析工具附加
從 .NET Framework 4 開始,ICorProfilerCallback3::ProfilerAttachComplete 回呼是在 InitializeForAttach 方法之後發出的。 ProfilerAttachComplete 表示 InitializeForAttach 方法中之程式碼剖析工具所要求的回呼已經啟動,而且程式碼剖析工具現在可以針對相關聯的 ID 執行更新,而不需要擔心遺漏告知。
例如,假設程式碼剖析工具已經在其 InitializeForAttach 回呼期間設定了 COR_PRF_MONITOR_MODULE_LOADS。 當程式碼剖析工具從 InitializeForAttach 傳回時,CLR 就會啟用 ModuleLoad 回呼,然後發出 ProfilerAttachComplete 回呼。 接著,程式碼剖析工具可以使用 ICorProfilerInfo3::EnumModules 方法,在其 ProfilerAttachComplete 回呼期間列舉目前已載入之所有模組的 ModuleID。 此外,系統會針對在列舉期間載入的任何新模組發出 ModuleLoad 事件。 程式碼剖析工具必須正確處理它們遇到的任何重複項目。 例如,正好在程式碼剖析工具附加時載入的模組可能會導致 ModuleID 出現兩次:在 ICorProfilerInfo3::EnumModules 所傳回的列舉中,以及在 ModuleLoadFinished 回呼中。
中斷連結程式碼剖析工具
附加要求必須由觸發處理序以跨處理序方式起始。 不過,當程式碼剖析工具 DLL 呼叫 ICorProfilerInfo3::RequestProfilerDetach 方法時,它就會以同處理序方式起始中斷連結要求。 如果程式碼剖析工具開發人員想要透過跨處理序 (例如,從自訂 UI) 起始中斷連結要求,開發人員就必須建立處理序間通訊機制來通知程式碼剖析工具 DLL (與目標應用程式一起執行) 呼叫 RequestProfilerDetach。
RequestProfilerDetach 會先以同步方式執行其部分工作 (包括設定其內部狀態,防止將事件傳送至程式碼剖析工具 DLL),然後再傳回。 在 RequestProfilerDetach 傳回之後,便以非同步方式完成其餘工作。 這些其餘工作會針對個別的執行緒 (DetachThread) 執行,而且包括輪詢和確定已經從所有應用程式執行緒的堆疊中取出所有程式碼剖析工具程式碼。 當 RequestProfilerDetach 完成時,程式碼剖析工具會先接收單一最終回呼 (ICorProfilerCallback3::ProfilerDetachSucceeded),然後 CLR 再釋放程式碼剖析工具的介面和程式碼堆積,並且卸載程式碼剖析工具的 DLL。
中斷連結期間的執行緒考量
執行控制權可能會以許多方式傳輸至程式碼剖析工具。 不過,控制權不得在卸載程式碼剖析工具之後傳遞給程式碼剖析工具,而且程式碼剖析工具和執行階段必須一起負責確定不會發生這個行為:
執行階段不了解程式碼剖析工具所建立或挾持而且包含 (或可能很快包含) 堆疊上之程式碼剖析工具程式碼的執行緒。 因此,程式碼剖析工具必須確定它會結束已建立的所有執行緒,而且必須先停止所有取樣或挾持,然後再呼叫 ICorProfilerInfo3::RequestProfilerDetach。 這項規則有一個例外:程式碼剖析工具可以使用它所建立的執行緒來呼叫 ICorProfilerInfo3::RequestProfilerDetach。 不過,這個執行緒必須透過 LoadLibrary (英文) 和 FreeLibraryAndExitThread (英文) 函式 (如需詳細資料,請參閱下一節),保留它對程式碼剖析工具 DLL 的參考。
在程式碼剖析工具呼叫 RequestProfilerDetach 之後,當執行階段嘗試完全卸載程式碼剖析工具時,執行階段必須確定 ICorProfilerCallback 方法不會導致程式碼剖析工具程式碼保留在任何執行緒的堆疊上。
逐步中斷連結
下列步驟會在程式碼剖析工具中斷連結時進行:
程式碼剖析工具會結束它已建立的所有執行緒,並且會先停止所有取樣和挾持,然後再呼叫 ICorProfilerInfo3::RequestProfilerDetach,不過有下列例外:
程式碼剖析工具可能會使用其中一個自己的執行緒來呼叫 ICorProfilerInfo3::RequestProfilerDetach 方法 (而非使用 CLR 建立的執行緒),藉以實作中斷連結。 如果程式碼剖析工具實作這個行為,在呼叫 RequestProfilerDetach 方法時,讓這個程式碼剖析工具執行緒存在是可接受的作法 (因為這是呼叫此方法的執行緒)。 不過,如果程式碼剖析工具選擇這個實作,它就必須確定下列事項:
針對呼叫 RequestProfilerDetach 所保留的執行緒必須保存它對程式碼剖析工具 DLL 的參考 (自行呼叫 LoadLibrary 函式)。
在執行緒呼叫 RequestProfilerDetach 之後,它必須立即呼叫 FreeLibraryAndExitThread 函式來釋放它所持有的程式碼剖析工具 DLL 並結束。
RequestProfilerDetach 會設定執行階段的內部狀態,以便將程式碼剖析工具視為停用。 這樣做可防止未來透過回呼方法呼叫程式碼剖析工具。
RequestProfilerDetach 會通知 DetachThread 開始檢查所有執行緒是否已經從堆疊中取出任何其餘回呼方法。
RequestProfilerDetach 會傳回狀態碼,指出中斷連結是否成功啟動。
此時,CLR 不允許程式碼剖析工具透過 ICorProfilerInfo, ICorProfilerInfo2 和 ICorProfilerInfo3 介面方法進一步呼叫 CLR。 任何這類呼叫都會立即失敗並傳回 CORPROF_E_PROFILER_DETACHING HRESULT。
程式碼剖析工具會傳回或結束執行緒。 如果程式碼剖析工具使用其中一個自己的執行緒來呼叫 RequestProfilerDetach,它就必須立即從這個執行緒呼叫 FreeLibraryAndExitThread。 如果程式碼剖析工具使用 CLR 執行緒 (亦即,在回呼內部) 來呼叫 RequestProfilerDetach,則程式碼剖析工具就會將控制權傳回至 CLR。
同時,DetachThread 會繼續檢查所有執行緒是否已經從堆疊中取出任何其餘回呼方法。
當 DetachThread 已經判斷出沒有回呼保留在任何執行緒的堆疊上時,它就會呼叫 ICorProfilerCallback3::ProfilerDetachSucceeded 方法。 程式碼剖析工具應該在 ProfilerDetachSucceeded 方法中進行最少的工作,並且快速傳回。
DetachThread 會針對程式碼剖析工具的 ICorProfilerCallback 介面執行最終 Release()。
DetachThread 會針對程式碼剖析工具的 DLL 呼叫 FreeLibrary 函式。
中斷連結限制
下列案例不支援中斷連結:
當程式碼剖析工具設定不可變的事件旗標時。
當程式碼剖析工具使用 enter/leave/tailcall (ELT) 探查時。
當程式碼剖析工具使用 Microsoft Intermediate Language (MSIL) 檢測 (例如,呼叫 SetILFunctionBody 方法) 時。
如果在這些案例中嘗試進行程式碼剖析工具中斷連結,RequestProfilerDetach 就會傳回錯誤 HRESULT。
偵錯
程式碼剖析工具附加和中斷連結功能不會防止應用程式進行偵錯。 在偵錯工作階段期間,應用程式可以隨時進行程式碼剖析工具附加和中斷連結。 相反地,進行程式碼剖析工具附加 (之後中斷連結) 的應用程式也可以隨時進行偵錯工具附加和中斷連結。 不過,偵錯工具所暫停的應用程式無法進行程式碼剖析,因為它無法回應程式碼剖析工具附加要求。