探查器附加和分离
在 .NET Framework 4 版之前的 .NET Framework 版本中,必须同时加载应用程序及其探查器。 当启动某个应用程序时,运行时会确定是否应通过检查环境变量和注册表设置来对该应用程序进行分析,然后加载所需的探查器。 探查器随后将保留在进程空间内,直至该应用程序退出。
从 .NET Framework 4 开始,您可以将探查器附加到一个已启动的应用程序,对该应用程序进行分析,然后在该应用程序结束之前分离探查器。 在分离探查器之后,该应用程序将继续像附加探查器之前那样运行。 换言之,探查器的临时存在不会在进程空间中留下任何痕迹。
另外,从 .NET Framework 4 开始,利用分析附加和分离方法及相关的分析 API 增强功能,可以将基于探查器的工具作为现成的实时诊断工具使用。
以下几节对这些增强功能进行了探讨:
附加探查器
附加详细信息
分步附加
定位 AttachProfiler 方法
在加载运行时之前附加到目标应用程序
附加限制
探查器附加示例
附加操作后初始化探查器
完成探查器附加
分离探查器
分离期间的线程注意事项
分步分离
分离限制
调试
附加探查器
将探查器附加到运行中的应用程序需要两个独立的进程:触发器进程和目标进程。
触发器进程是使探查器 DLL 被加载到运行中应用程序的进程空间的可执行程序。 触发器进程使用 ICLRProfiling::AttachProfiler 方法来请求公共语言运行时 (CLR) 加载探查器 DLL。 在探查器 DLL 已加载到目标进程中之后,触发器进程可以保留也可以退出,具体依探查器开发人员的判断而定。
目标进程包含要分析的应用程序和 CLR。 在调用 AttachProfiler 方法之后,探查器 DLL 将随目标应用程序一起被加载到此进程空间中。
附加详细信息
AttachProfiler 将指定的探查器附加到指定的进程,同时选择将一些数据传递到该探查器。 它等待附加完成,直至指定的超时时间。
在目标进程中,用于在附加时加载探查器的过程与用于在启动时加载探查器的过程类似:CLR 对给定的 CLSID 执行 CoCreates 操作,查询 ICorProfilerCallback3 接口并调用 ICorProfilerCallback3::InitializeForAttach 方法。 如果这些操作在分配的超时时间内成功执行,则 AttachProfiler 返回 S_OK HRESULT。 因此,触发器进程应选择一个足以使探查器完成初始化的超时。
注意 |
---|
发生超时的原因是,目标进程中的终结器的运行时间超过了超时值,导致返回 HRESULT_FROM_WIN32 (ERROR_TIMEOUT)。如果您收到此错误,则可以重试附加操作。 |
如果在附加完成之前超过超时,则 AttachProfiler 将返回 HRESULT 错误;但是,附加有可能已经成功。 在这种情况下,可以通过替代方式确定成功与否。 探查器开发人员通常会在其触发器进程和目标应用程序之间建立一个自定义通信通道。 此类通信通道可用于检测成功的附加。 触发器进程也可以通过再次调用 AttachProfiler 并接收 CORPROF_E_PROFILER_ALREADY_ACTIVE 来检测成功。
此外,目标进程还必须具有足够的访问权限,然后才能加载探查器的 DLL。 如果要附加到的服务(例如 W3wp.exe 进程)需要在具有受限制访问权限的帐户(如 NETWORK SERVICE)下运行,则这可能是个问题。 在这种情况下,文件系统上的某些目录可能会具有阻止目标进程进行访问的安全限制。 因此,探查器开发人员应负责将探查器 DLL 安装在允许进行适当访问的位置。
失败情况记录在应用程序事件日志中。
分步附加
触发器进程将调用 AttachProfiler,标识要附加的目标进程和探查器,并选择传递数据以分配给附加后的探查器。
在目标进程中,CLR 将收到附加请求。
在目标进程中,CLR 将创建探查器对象并从中获取 ICorProfilerCallback3 接口。
然后,CLR 将调用 InitializeForAttach 方法,并传递触发器进程包含在附加请求中的数据。
探查器将初始化自身,启用其需要的回调,并成功返回。
在触发器进程中,AttachProfiler 将返回 S_OK HRESULT,以指示已成功附加探查器。 触发器进程不再相关。
从这以后,接下来的分析过程与以前的所有版本中的过程一样。
定位 AttachProfiler 方法
触发器进程可以按照以下步骤操作来找到 AttachProfiler 方法:
调用 LoadLibrary 方法以加载 mscoree.dll 并找到 CLRCreateInstance 方法。
通过 CLSID_CLRMetaHost 和 IID_ICLRMetaHost 参数调用 CLRCreateInstance 方法,这将返回 ICLRMetaHost 接口。
调用 ICLRMetaHost::EnumerateLoadedRuntimes方法,以便为进程中的每个 CLR 实例检索一个 ICLRRuntimeInfo 接口。
循环访问 ICLRRuntimeInfo 接口,直至找到要将探查器附加到的所需接口。
通过 IID_ICLRProfiling 的参数调用 ICLRRuntimeInfo::GetInterface 方法,以获取提供 AttachProfiler 方法的 ICLRProfiling 接口。
在加载运行时之前附加到目标应用程序
不支持此功能。 如果触发器进程尝试通过指定一个尚未加载运行时的进程来调用 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 回调。 然后,探查器可以在其 ProfilerAttachComplete 回调期间,使用 ICorProfilerInfo3::EnumModules 方法来枚举当前加载的所有模块的 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 之前停止所有采样和截获操作,但以下是个例外:
探查器可以通过使用自己的某个线程(而不是使用 CLR 创建的线程)调用 ICorProfilerInfo3::RequestProfilerDetach 方法来实现分离。 如果探查器实现了此行为,则可以接受在调用 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 中间语言 (MSIL) 检测(例如,通过调用 SetILFunctionBody 方法)。
如果尝试在这些情形下执行探查器分离,则 RequestProfilerDetach 将返回错误 HRESULT。
调试
探查器附加和分离功能不会阻止对应用程序进行调试。 在运行调试会话的过程中,应用程序可以随时执行探查器附加和分离操作。 反之,已附加(并在以后分离)探查器的应用程序也可以随时附加和分离调试器。 但是,无法分析调试器挂起的应用程序,因为该应用程序无法响应探查器附加请求。