使用 IDebugClient 接口进行混合模式堆栈遍历






4.90/5 (14投票s)
像 Stackwalk64 这样的原生堆栈遍历函数无法处理混合模式堆栈,因为托管代码使用的堆栈方式与原生代码不同。有一个名为 IDebugClient 的 API,它可以正确地遍历混合模式堆栈,我们将对其进行探讨。
引言
您是否需要遍历混合模式(非托管/托管)应用程序的调用堆栈,或者对如何实现感到好奇?在本文中,我将展示如何使用 IDebugClient
接口来遍历混合模式调用堆栈,以及如何使用 IXCLRDataProcess
接口来查找托管方法的符号名称。
尽管它提供了完整的原生调用堆栈,但无法完全解析所有托管方法名。但是,如果您对 IDebugClient
接口感到好奇,并想了解更多关于如何与 CLR 运行时交互的信息,我认为继续阅读会很有趣。
背景
这一切都始于我希望提高公司应用程序性能的愿望。该应用程序部分是用 C++ 编写的,部分是用 C# 编写的。C++ 框架的一部分已经过优化。这只是通过在经常调用的地方插入 StackWalk64
(dbghelp.dll)调用并将调用堆栈写入磁盘来完成的。是的,这是一种简陋的性能分析器,但事实证明它很有效且易于使用。其弱点在于它无法处理托管代码,因此我只得到了一个部分堆栈跟踪。这促使我研究其他的堆栈遍历 API。每种 API 都有其优缺点。
替代 API
下面是一个混合模式调用堆栈的示例。
我们从底部开始,原生代码调用托管代码。该托管代码调用原生代码,最终形成一个交错的堆栈。
System.Diagnostics.StackTrace
在 .NET 中,您可以使用 System.Diagnostics.StackTrace
类非常轻松地遍历堆栈。下面是用 C++/CLI 编写的示例(可供托管和非托管代码使用)。
static void DumpStackTrace()
{
auto sb = gcnew System::Text::StringBuilder();
auto stackTrace = gcnew System::Diagnostics::StackTrace();
auto frames = stackTrace->GetFrames();
for each(System::Diagnostics::StackFrame^ frame in frames)
{
auto methodBase = frame->GetMethod();
sb->Append(methodBase->Name);
auto parameters = methodBase->GetParameters();
sb->Append("(");
for (int i = 0; i < parameters->Length; i++)
{
auto parInfo = parameters[i];
if (i > 0)
sb->Append(", ");
sb->AppendFormat("{0} {1}", parInfo->ParameterType->Name, parInfo->Name);
}
sb->Append(")");
sb->AppendLine();
}
System::Console::WriteLine(sb->ToString());
}
在我的混合模式应用程序(C++/CLI)中调用它,会得到以下输出:
ManagedA(Int32 a)
MixedAB(Int32 a, Int32 b)
NativeABC(Int32 , Int32 , Int32 )
MixedABCD(Int32 a, Int32 b, Int32 c, Int32 d)
ManagedABCDE(Int32 a, Int32 b, Int32 c, Int32 d, Int32 e)
MixedABCDEF(Int32 a, Int32 b, Int32 c, Int32 d, Int32 e, Int32 f)
它正确地展开调用堆栈直到最后一个函数调用 MixedABCDEF
。
StackWalk64
Stackwalk64
允许我们查看堆栈上的原生堆栈帧,但无法看到托管帧。原因之一是托管帧的堆栈使用方式与原生代码不同。下面是您大致使用 StackWalk64
函数的方式。为了将指令指针映射到符号名称,必须调用一次 SymInitialize
,然后为找到的每个 EIP 调用 SymGetSymFromAddr64
。
void DumpStackTraceEx(HANDLE processHandle, HANDLE threadHandle)
{
STACKFRAME64 stackFrame = { 0 };
CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_FULL;
//Use this one the current process / current thread
//RtlCaptureContext(&context);
//Use this one, for other processes.
GetThreadContext(threadHandle, &context)
stackFrame.AddrPC.Offset = context.Eip;
stackFrame.AddrPC.Mode = AddrModeFlat;
stackFrame.AddrFrame.Offset = context.Ebp;
stackFrame.AddrFrame.Mode = AddrModeFlat;
stackFrame.AddrStack.Offset = context.Esp;
stackFrame.AddrStack.Mode = AddrModeFlat;
while(
StackWalk64(
IMAGE_FILE_MACHINE_I386,
processHandle,
threadHandle,
&stackFrame,
(PVOID)&context,
NULL,
SymFunctionTableAccess64,
SymGetModuleBase64,
NULL))
{
// EIP
DWORD64 addr64 = stackFrame.AddrPC.Offset
// BOOL bRetSymFromAddr = SymGetSymFromAddr64(
// currentProcess,
// addr64,
// &displacement,
// SymbolInfo );
printf("EIP = 0x%08I64X\n", addr64);
}
}
使用相同的混合模式应用程序,我得到以下输出:
Error: SymGetSymFromAddr64() failed. Module = UnknownModule Addr64 = 0x00287E72 BaseAddr = 0x00000000
Error: SymGetSymFromAddr64() failed. Module = UnknownModule Addr64 = 0x0028583F BaseAddr = 0x00000000
Error: SymGetSymFromAddr64() failed. Module = UnknownModule Addr64 = 0x00285527 BaseAddr = 0x00000000
MixedLib1!MixedLib1::MixedLib1_Func_Add_ABCDEF(25) : 0x65F810AC
MixedLib1!MixedLib1::MixedLib1_Func_Add_ABCDEFG(30) : 0x65F810E0
Error: SymGetSymFromAddr64() failed. Module = UnknownModule Addr64 = 0x00283A82 BaseAddr = 0x00000000
clr!DecCantStopCount(UnknownLine) : 0x6F271E1F
clr!CallDescrWorker(UnknownLine) : 0x6F2721BB
clr!CallDescrWorker(UnknownLine) : 0x6F2721BB
clr!CallDescrWorkerWithHandler(UnknownLine) : 0x6F294C02
clr!MethodDesc::CallDescr(UnknownLine) : 0x6F294DA4
clr!MethodDesc::CallTargetWorker(UnknownLine) : 0x6F294DD9
clr!MethodDescCallSite::Call_RetArgSlot(UnknownLine) : 0x6F294DF9
clr!ClassLoader::RunMain(UnknownLine) : 0x6F3E9643
clr!Assembly::ExecuteMainMethod(UnknownLine) : 0x6F41CEC8
clr!SystemDomain::ExecuteMainMethod(UnknownLine) : 0x6F41CCDC
clr!ExecuteEXE(UnknownLine) : 0x6F41D0D5
clr!_CorExeMainInternal(UnknownLine) : 0x6F41CFD5
clr!_CorExeMain(UnknownLine) : 0x6F40E258
mscoreei!_CorExeMain(UnknownLine) : 0x71C555AB
MSCOREE!ShellShim__CorExeMain(UnknownLine) : 0x71D37F16
MSCOREE!_CorExeMain_Exported(UnknownLine) : 0x71D34DE3
KERNEL32!BaseThreadInitThunk(UnknownLine) : 0x75C133CA
ntdll!__RtlUserThreadStart(UnknownLine) : 0x76EF9ED2
ntdll!_RtlUserThreadStart(UnknownLine) : 0x76EF9EA5
有一些符号无法找到。它们实际上属于 kernel32 和 msvcrt。应该可以解析它们,通过一些故障排除可能可以解决。请记住,SymInitialize
是异步的,它会返回,但符号文件会在后台加载。如果您在符号文件加载之前尝试解析,将会收到错误。
我想展示的是,托管帧没有被显示出来。它们被显示为 CLR 运行时内的函数调用,这并没有什么帮助。
WinDbg
让我们看看 WinDbg 如何处理混合模式调用堆栈。
0:000> k
ChildEBP RetAddr
002ce5e8 75cb7361 KERNEL32!ReadConsoleInternal+0x15
002ce670 75c3f1c6 KERNEL32!ReadConsoleA+0x40
002ce6b8 74dcc3b3 KERNEL32!ReadFileImplementation+0x75
002ce700 74dcc2bc msvcrt!_read_nolock+0x183
002ce744 74dcc472 msvcrt!_read+0x9f
002ce760 74dcee5d msvcrt!_filbuf+0x7d
002ce768 74dcede4 msvcrt!_ftbuf+0x72
002ce774 74dceb62 msvcrt!_ftbuf+0x89
002ce954 74e26866 msvcrt!_input_l+0x36c
002ce998 74e268d9 msvcrt!vwscanf+0x55
002ce9b0 0031435c msvcrt!scanf+0x18
WARNING: Frame IP not in any known module. Following frames may be wrong.
002ceaac 0031405a 0x31435c
002cebdc 65e210ac 0x31405a
002cebf8 65e210e0 MixedLib1!MixedLib1::MixedLib1_Func_Add_ABCDEF+0x1c
002cec18 00313a82 MixedLib1!MixedLib1::MixedLib1_Func_Add_ABCDEFG+0x20
002ced88 6f271e1f 0x313a82
002cedc4 6f2721bb clr!DecCantStopCount+0x13
002cede4 6f2721bb clr!CallDescrWorker+0x33
002cedf4 6f294c02 clr!CallDescrWorker+0x33
002cee70 6f294da4 clr!CallDescrWorkerWithHandler+0x8e
002cefb0 6f294dd9 clr!MethodDesc::CallDescr+0x194
002cefcc 6f294df9 clr!MethodDesc::CallTargetWorker+0x21
002cefe4 6f3e9643 clr!MethodDescCallSite::Call_RetArgSlot+0x1c
002cf148 6f41cec8 clr!ClassLoader::RunMain+0x238
002cf3b0 6f41ccdc clr!Assembly::ExecuteMainMethod+0xc1
002cf894 6f41d0d5 clr!SystemDomain::ExecuteMainMethod+0x4ec
002cf8e8 6f41cfd5 clr!ExecuteEXE+0x58
002cf934 6f40e258 clr!_CorExeMainInternal+0x19f
002cf96c 71c555ab clr!_CorExeMain+0x4e
002cf978 71d37f16 mscoreei!_CorExeMain+0x38
002cf988 71d34de3 MSCOREE!ShellShim__CorExeMain+0x99
002cf990 75c133ca MSCOREE!_CorExeMain_Exported+0x8
002cf99c 76ef9ed2 KERNEL32!BaseThreadInitThunk+0xe
002cf9dc 76ef9ea5 ntdll!__RtlUserThreadStart+0x70
002cf9f4 00000000 ntdll!_RtlUserThreadStart+0x1b
堆栈看起来与我的非常相似。好一点,因为它正确解析了 kernel32 和 msvcrt 中的函数。但仔细看看。但有些地址无法解析。显然,普通的堆栈遍历会感到困惑“警告:帧 IP 不在任何已知模块中。后面的帧可能不正确。”通常,DLL 被加载到内存空间中,代码位于该内存范围内。程序集也被加载,但不包含任何可执行代码。JIT 编译器获取 IL 代码并生成机器码,然后将其放在堆上。原生堆栈遍历器只看到生成的代码,它不在任何已加载的模块中(正确)。堆栈遍历器对 IL 代码一无所知,也无法正确使用 PDB 文件,因为它映射到 IL 代码而不是机器相关的代码。
带 SOS 扩展的 WinDbg
SOS 是一个用于调试托管应用程序的 WinDbg 扩展。它能够通过 !clrstack
命令遍历混合堆栈帧。让我们看看它的表现如何。
0:000> .loadby sos clr
0:000> !clrstack
OS Thread Id: 0xbf0 (0)
Child SP IP Call Site
002ce9dc 75cb76f8 [InlinedCallFrame: 002ce9dc]
002ce9b8 0031435c DomainBoundILStubClass.IL_STUB_PInvoke(System.String, System.Text.StringBuilder, ...)
002ce9dc 0031405a [InlinedCallFrame: 002ce9dc] ManagedLib0.Win32Imports.scanf(System.String, ...)
002ceab4 0031405a ManagedLib0.Win32Imports.ReadLine()
002ceae8 00313d86 ManagedLib0.A.Add_A(Int32)
002ceb28 00313cbf ManagedLib0.AB.Add_AB(Int32, Int32)
002ceb44 00313c61 MixedLib1.ABC.Add_ABC(Int32, Int32, Int32)
002ceb68 00313bbd <Module>.MixedLib1.MixedLib1_Func_Add_ABCD(Int32, Int32, Int32, Int32)
002ceb94 00313b37 <Module>.MixedLib1.MixedLib1_Func_Add_ABCDE(Int32, Int32, Int32, Int32, Int32)
002cec40 00990b1b [InlinedCallFrame: 002cec40]
002cec20 00313a82 DomainBoundILStubClass.IL_STUB_PInvoke(Int32, Int32, Int32, Int32, Int32, Int32, Int32)
002cec40 0031391f [InlinedCallFrame: 002cec40] <Module>.MixedLib1.MixedLib1_Func_Add_ABCDEFG(Int32, Int32, Int32, Int32, Int32, Int32, Int32)
002cecf4 0031391f MixedLib1.MixedLib1Funcs.MixedLib1_Func_Add_ABCDEFGH(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32)
002ced2c 0031386c <Module>.CppCliApp.MixedLib2_Func_Add_ABCDEFGHI(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32)
002ced6c 003122d4 <Module>.main(System.String[])
002ced84 00311e21 <Module>.mainCRTStartupStrArray(System.String[])
002cf018 6f2721bb [GCFrame: 002cf018]
非常整洁。这就是我们想要达到的目标。
弃用 System.Diagnostics.StackTrace
StackTrace
类效果很好,但也有缺点。
首先,它使用反射,速度非常慢。其次,手动插桩代码的旧方法已经不再适用。源代码中不能保留堆栈跟踪调用,每次添加和删除它们都会非常耗时。第三,我并不知道我们在哪里存在性能问题,以及应该在哪里跟踪堆栈。我现在需要的是获取堆栈跟踪样本,并根据频率指出问题的方向。这也被称为采样性能分析。一个外部应用程序连接到一个目标应用程序,并以固定的间隔(例如,每 20 毫秒)获取一个堆栈跟踪样本。
在哪里可以找到有关可能解决方案的信息
- 一种可能性是尝试逆向工程 SOS 扩展。
- 尝试查找一些 CLR API,也许可以看看 mscoree.h 和相关的包含文件。
- 查看 Mono 源代码。
- 查看 Microsoft 的共享源 CLI(Rotor V2.0)。源代码。
- 谷歌 谷歌 谷歌
如果您想了解更多关于 CLR 运行时以及如何通过 mscoree 和 CLR 托管接口与其交互的信息,我推荐阅读《Customizing the Microsoft® .NET Framework Common Language Runtime》。CLR API 可能不够。但是,一个绝佳的灵感来源是 Rotor 源代码,它是 Microsoft 为了标准化目的而编写的一个未优化的 CLR 运行时实现。
实现
我们将探讨两个接口。IDebugClient
,据说可以提供完整的堆栈跟踪,以及 IXCLRDataProcess
(mscordacwks.dll),用于将托管地址转换为可读的方法名称。
IDebugClient
Microsoft 很友好地提供了一个可以遍历混合帧的 API,IDebugClient
,它由 dbgeng.dll 公开。
使用该 API 非常直接。网上有很多示例。您通过一个名为 DebugCreate
的特殊函数创建一个对象,然后将 IDebugClient
接口的 GUID 传递给它。
DebugClient* debugClient = nullptr;
if ((hr = DebugCreate(__uuidof(IDebugClient), (void **)&debugClient)) != S_OK)
{
return false;
}
m_debugClient = debugClient;
附加到进程非常直接:
const ULONG64 LOCAL_SERVER = 0;
int flags = DEBUG_ATTACH_NONINVASIVE | DEBUG_ATTACH_NONINVASIVE_NO_SUSPEND;
hr = debugClient->AttachProcess(LOCAL_SERVER, pId, flags);
if (hr != S_OK)
return false;
if ((hr = debugClient->QueryInterface(__uuidof(IDebugControl), (void **)&debugControl)) != S_OK)
{
debugClient->Release();
return false;
}
m_debugControl = debugControl;
我实际上遇到了附加问题。有时它有效,有时无效。调试时它有效。添加延迟后它甚至有效。我发现了原因。附加需要一些时间才能完成,它只是被启动,所以对象还没有真正准备好。我们可以做的是将目标应用程序的执行状态设置为“go”。进程已经在运行(从未暂停过),所以调用应该会立即返回。但这里有一个巧妙之处。当调试器正确附加时,它会返回。
hr = m_debugControl->SetExecutionStatus(DEBUG_STATUS_GO);
if ((hr = m_debugControl->WaitForEvent(DEBUG_WAIT_DEFAULT, INFINITE)) != S_OK)
{
return false;
}
然后,使用 IDebugClient
对象,您可以创建其他可能需要的 COM 对象。
m_debugClient->QueryInterface(__uuidof(IDebugAdvanced), (void **)&m_ExtAdvanced))
m_debugClient->QueryInterface(__uuidof(IDebugAdvanced2), (void **)&m_ExtAdvanced2))
m_debugClient->QueryInterface(__uuidof(IDebugControl2), (void **)&m_ExtControl))
m_debugClient->QueryInterface(__uuidof(IDebugControl4), (void **)&m_ExtControl4))
m_debugClient->QueryInterface(__uuidof(IDebugDataSpaces), (void **)&m_ExtData))
m_debugClient->QueryInterface(__uuidof(IDebugDataSpaces2), (void **)&m_ExtData2))
m_debugClient->QueryInterface(__uuidof(IDebugRegisters), (void **)&m_ExtRegisters))
m_debugClient->QueryInterface(__uuidof(IDebugSymbols), (void **)&m_ExtSymbols))
m_debugClient->QueryInterface(__uuidof(IDebugSymbols2), (void **)&m_ExtSymbols2))
m_debugClient->QueryInterface(__uuidof(IDebugSymbols3), (void **)&m_ExtSymbols3))
m_debugClient->QueryInterface(__uuidof(IDebugSystemObjects), (void **)&m_ExtSystem))
我犯了一个大错误,花了很长时间才修复。由于我只对 IDebugControl4::GetStackTrace
函数感兴趣,而且我不会用它来单步执行、设置断点等,所以我没有实现打印到屏幕的回调函数,然后当我尝试获取接口时,它一直在崩溃。拜托!难道没有人能插入一个额外的测试来查看 API 的用户是否对事件或输出感兴趣吗?我也实现了这些调试输出回调类。好吧,我将函数体留空了。反正我对此不感兴趣。
HRESULT hr1 = m_debugClient->SetOutputCallbacks(&g_DebugOutputCallback);
HRESULT hr2 = m_debugClient->SetEventCallbacks(&g_DebugEventCallbacks);
CLRDataCreateInstance
CLRDataCreateInstance
(定义在 clrdata.idl CorGuids.lib 中)可以返回一个具有 IXCLRDataProcess
接口的 COM 对象。该对象可以枚举任务、应用程序域、方法等。它还包含将地址或内部 CLR ID 映射到方法、类、程序集等的函数。非常方便。这可能就是我们需要的。
HRESULT CLRDataCreateInstance (
[in] REFIID iid,
[in] ICLRDataTarget *target,
[out] void **iface
);
一个小问题是 IXCLRDataProcess
接口没有头文件,但您可以从xclrdata.idl 生成一个,它包含在 Rotor 源代码中。根据许可,允许将源代码用于非商业目的。
Rotor 的一些源代码可能不容易理解。我想感谢 Steve's Blog,它提供了一些有用的说明,但不幸的是,Steve 没有提供任何源代码:(。所以,实际上还剩下相当多的实现和调试工作要做。
实现 ICLRDataTarget
为了创建 IXCLRDataProcess
,CLRCreateInstance
函数需要一个 ICLRDataTarget
对象,该对象与托管应用程序交互。这是用户需要实现的一个接口。我不知道为什么这个接口没有默认实现。它只做基本的事情,如读写原始内存、返回指针大小等。
interface ICLRDataTarget : IUnknown {
HRESULT GetCurrentThreadID
HRESULT GetImageBase
HRESULT GetMachineType
HRESULT GetPointerSize
HRESULT GetThreadContext
HRESULT GetTLSValue
HRESULT ReadVirtual
HRESULT Request
HRESULT SetThreadContext
HRESULT SetTLSValue
HRESULT WriteVirtual
};
我在实现过程中省略了一些细节。我只支持 x86 架构。为“any”平台编译的托管应用程序可以根据托管它的操作系统在 x86/x64 模式下运行,但在 Visual Studio 2010 中,它实际上默认使用 x86 架构。除此之外,我想先让 x86 工作,然后再尝试 x64。总是先处理简单的情况。当它成功后,我们再扩展。
public class DiagCLRDataTarget : public ICLRDataTarget
{
public:
virtual HRESULT STDMETHODCALLTYPE GetMachineType(
/* [out] */ ULONG32 *machineType)
{
// Other possibilities are
// IMAGE_FILE_MACHINE_IA64
// IMAGE_FILE_MACHINE_AMD64
*machineType = IMAGE_FILE_MACHINE_I386;
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE GetPointerSize(
/* [out] */ ULONG32 *pointerSize)
{
*pointerSize = sizeof(PVOID);
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE GetImageBase(
/* [string][in] */ LPCWSTR imagePath,
/* [out] */ CLRDATA_ADDRESS *baseAddress)
{
// This method should return the address to mscorwks module
// mscorwks was renamed to clr in v4.0 of the CLR
ULONG index = 0;
ULONG64 baseAddr = 0;
std::basic_string<WCHAR> img = std::basic_string<WCHAR>(imagePath);
std::basic_string<WCHAR> moduleName;
if (img == L"mscorwks.dll")
moduleName = L"mscorwks";
else if (img == L"clr.dll")
moduleName = L"clr";
else
moduleName = img;
HRESULT hr = this->m_debugNative->
m_ExtSymbols3->
GetModuleByModuleNameWide
(moduleName.c_str(), 0, &index, &baseAddr);
*baseAddress = baseAddr;
return hr;
}
virtual HRESULT STDMETHODCALLTYPE ReadVirtual(
/* [in] */ CLRDATA_ADDRESS address,
/* [length_is][size_is][out] */ BYTE *buffer,
/* [in] */ ULONG32 bytesRequested,
/* [out] */ ULONG32 *bytesRead)
{
PULONG bRead = reinterpret_cast<PULONG>(bytesRead);
return this->m_debugNative->
m_ExtData2->
ReadVirtual
(address, buffer, bytesRequested, bRead);
}
// The methods below are not used by IXCLRDataProcess
// In my implementation I just throw an NOT_IMPLEMENTED_EXCEPTION
virtual HRESULT STDMETHODCALLTYPE WriteVirtual
virtual HRESULT STDMETHODCALLTYPE GetTLSValue
virtual HRESULT STDMETHODCALLTYPE SetTLSValue
virtual HRESULT STDMETHODCALLTYPE GetCurrentThreadID
virtual HRESULT STDMETHODCALLTYPE GetThreadContext
virtual HRESULT STDMETHODCALLTYPE SetThreadContext
virtual HRESULT STDMETHODCALLTYPE Request
};
Stackwalker 类
初始化
为了附加到正在运行的进程,需要一些基本的初始化:
bool Stackwalker::Initialize(int pId)
{
m_pId = pId;
m_isClr4 = IsClr4Process(pId);
m_isManaged = IsDotNetProcess(pId);
bool result = m_debugNative->Initialize(pId);
return result;
}
您如何知道一个进程是否是 .NET 进程?PE 文件头(存在于所有可执行文件中)包含该信息。更简单的方法是查看是否加载了某些 CLR 模块,如 clr、clrjit、mscorlib_ni、mscoree 等。
您如何知道它是 CLR v4.0?查找 clr.dll。Mscorwks.dll 从 v2.0 重命名为 v4.0。如果有人将他们的模块命名为 clr.dll,可能会产生误报,但本文不是关于混合模式/托管应用程序吗?
获取 IXCLRDataprocess 对象
要创建 IXCLDataProcess
对象,我们需要调用位于名为 mscordacwks.dll 的数据访问 DLL 中的 CLRDataCreateInstance
。请记住,该 DLL 依赖于 CLR 版本,因此必须从正确的文件位置加载它,这就是我们检查 CLR 版本的原因。然后我们必须手动将库加载到内存中,然后调用 GetProcessAddress
来获取 CLRDataCreateInstance
的地址。最后,我们调用该函数,传递我们 ICLRDataTarget
的实例。
HRESULT LoadDataAccessDLL(bool IsClrV4, ICLRDataTarget* target, HMODULE* dllHandle, void** iface)
{
std::basic_string<TCHAR> systemRootString;
std::basic_string<TCHAR> mscordacwksPathString;
std::basic_string<TCHAR> mscordacwksFileName;
const int size = 500;
TCHAR windir[size];
HRESULT hr = GetWindowsDirectory(windir, size);
systemRootString = std::basic_string<TCHAR>(windir);
if (IsClrV4)
{
mscordacwksPathString =
std::basic_string<TCHAR>("\\Microsoft.NET\\Framework\\v4.0.30319\\mscordacwks.dll");
}
else
{
mscordacwksPathString =
std::basic_string<TCHAR>("\\Microsoft.NET\\Framework\\v2.0.50727\\mscordacwks.dll");
}
mscordacwksFileName = systemRootString + mscordacwksPathString;
HMODULE accessDll = LoadLibrary(mscordacwksFileName.c_str());
PFN_CLRDataCreateInstance entry =
(PFN_CLRDataCreateInstance) GetProcAddress(accessDll, "CLRDataCreateInstance");
RESULT status;
void* ifacePtr = NULL;
if (!entry)
{
status = GetLastError();
FreeLibrary(accessDll);
}
else if ((status = entry(__uuidof(IXCLRDataProcess), target, &ifacePtr)) != S_OK)
{
FreeLibrary(accessDll);
}
else
{
*dllHandle = accessDll;
*iface = ifacePtr;
}
return status;
}
很抱歉,我的代码中可能包含了很多 TCHAR
、char
、std::string
和 std::wstring
。当为 Unicode 编译时,TCHAR
是 wchar_t
,而在多字节编译时是 char。无论如何编译。StackWalk64
始终使用 char,但大多数 Win32 API 会自行调整。当您必须来回转换时,有时可能会很麻烦。
整合所有内容
HMODULE accessDLL;
void* iface = NULL;
HRESULT hr = LoadDataAccessDLL(m_isClr4, m_clrDataTarget, &accessDLL, &iface);
m_clrDataProcess = static_cast<IXCLRDataProcess*>(iface);
现在我们可以使用该对象以及从堆栈遍历中获得的指令指针了。
HRESULT hr = m_clrDataProcess->GetRuntimeNameByAddress(
clrAddr /*EIP*/, 0, maxSize - 1 , &nameLen, nameBuffer, &displacement);
它将返回托管指令指针 (IP) 的符号名称。
使用 IXCLRDataProcess::Request 解析托管方法名
在 Steve's blog 上,您可以阅读关于 DacpMethodDescData
和 IXCLRDataProcess::Request
的内容。它是一个通用的接口,接受一个描述您想要的数据类型的枚举值、一个指向输入参数的指针以及一个指向输出参数的指针。
return dac->Request(DACPRIV_REQUEST_METHODDESC_NAME,
sizeof(addrMethodDesc), (PBYTE)&addrMethodDesc,
sizeof(WCHAR)*iNameChars, (PBYTE) pwszName);
一个强大的接口,如果您知道要发送哪些枚举值,否则您就完了。它提供的信息与 GetRuntimeNameByAddress
相同。下面是一个代码片段,您也可以在附带的源代码中找到它。
WCHAR buffer[255];
struct DacpMethodDescData DacpData;
ZeroMemory(&DacpData, sizeof(DacpData));
CLRDATA_ADDRESS managedIP = static_cast<CLRDATA_ADDRESS>(ip);
HRESULT hr1 = DacpData.RequestFromIP(m_clrData, managedIP);
ULONG32 nameChars = sizeof(buffer)/sizeof(WCHAR) - 1;
if (SUCCEEDED(hr))
{
buffer[0] = 0;
HRESULT hr2 = DacpData.GetMethodName(m_clrData /* IXCLRDataProcess */,
DacpData.MethodDescPtr /* CLRDATA_ADDRESS */,
nameChars /* Max chars of buffer */,
buffer);
if (SUCCEEDED(hr2))
result = std::basic_string<WCHAR>(buffer);
}
示例应用程序
演示文件夹中有三个应用程序。它在当前文件夹中查找 pdb 文件。它还在 C:\symbols 中查找。它还尝试从 Microsoft 下载 PDB 文件并将其存储在 C:\symbols 中。没有这些符号,Stackwalk64
可能会很快迷失方向,因为它不知道调用约定、省略的帧指针和其他优化。
首先启动 CppCliApp.exe,它会打印进程 ID,并输出 Stackwalk64
和 System.Diagnostics.StackTrace
调用堆栈。使用其他应用程序时,请使用 pId
。
c:\Demo>CppCliApp.exe
Current Process Id #4404
---- StackTrace ----
....
---- StackWalk64 ----
....
C:\Demo>DiagApp.exe 4404
C:\Demo>StackWalk64App 4404
这是我从 DiagApp.exe(使用 IClientDebug
接口)获得的结果:
Process is managed
Process is Clr 4
Stack Trace:
KERNEL32!ReadConsoleInternal+0x00000015
KERNEL32!ReadConsoleA+0x00000040
KERNEL32!ReadFileImplementation+0x00000075
msvcrt!_read_nolock+0x00000183
msvcrt!_read+0x0000009F
msvcrt!_filbuf+0x0000007D
msvcrt!_ftbuf+0x00000072
msvcrt!_ftbuf+0x00000089
msvcrt!_input_l+0x0000036C
msvcrt!vwscanf+0x00000055
msvcrt!scanf+0x00000018
DomainBoundILStubClass.IL_STUB_PInvoke(System.String, System.Text.StringBuilder, ...)
ManagedLib0.Win32Imports.ReadLine()
MixedLib1!MixedLib1::MixedLib1_Func_Add_ABCDEF+0x0000001C
MixedLib1!MixedLib1::MixedLib1_Func_Add_ABCDEFG+0x00000020
DomainBoundILStubClass.IL_STUB_PInvoke(Int32, Int32, Int32, Int32, Int32, Int32, Int32)
clr!DecCantStopCount+0x00000013
clr!CallDescrWorker+0x00000033
clr!CallDescrWorker+0x00000033
clr!CallDescrWorkerWithHandler+0x0000008E
clr!MethodDesc::CallDescr+0x00000194
clr!MethodDesc::CallTargetWorker+0x00000021
clr!MethodDescCallSite::Call_RetArgSlot+0x0000001C
clr!ClassLoader::RunMain+0x00000238
clr!Assembly::ExecuteMainMethod+0x000000C1
clr!SystemDomain::ExecuteMainMethod+0x000004EC
clr!ExecuteEXE+0x00000058
clr!_CorExeMainInternal+0x0000019F
clr!_CorExeMain+0x0000004E
mscoreei!_CorExeMain+0x00000038
MSCOREE!ShellShim__CorExeMain+0x00000099
MSCOREE!_CorExeMain_Exported+0x00000008
KERNEL32!BaseThreadInitThunk+0x0000000E
ntdll!__RtlUserThreadStart+0x00000070
ntdll!_RtlUserThreadStart+0x0000001B
Stack Trace:
我们成功地从托管应用程序获取了完整的堆栈跟踪。由于 mscordacwks.dll 的存在,它甚至解析了 WinDbg 失败的地址。但我的自己的托管类仍然没有出现。这很不幸。考虑到这一点,调用 clr!xxx
是完全有意义的。IL 代码无法直接运行,必须将其 JIT 编译为机器码,但 CLR 可能有一个原生函数来执行 JIT 编译的代码。这就是我们看到的函数。
在纯托管应用程序上,我实际上得到了一个更好的堆栈跟踪。许多 CLR 函数以可读代码的形式显示,但我的自己的托管方法名仍然隐藏着。
KERNEL32!ReadConsoleInternal+0x00000015
KERNEL32!ReadConsoleA+0x00000040
KERNEL32!ReadFileImplementation+0x00000075
DomainNeutralILStubClass.IL_STUB_PInvoke
System.IO.__ConsoleStream.ReadFileNative
System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
System.IO.StreamReader.ReadBuffer()
System.IO.StreamReader.ReadLine()
System.IO.TextReader+SyncTextReader.ReadLine()
System.Console.ReadLine()
clr!CallDescrWorker+0x00000033
clr!CallDescrWorker+0x00000033
clr!CallDescrWorkerWithHandler+0x0000008E
...
我还有另一个混合模式应用程序,它实际上会混淆 IDebugClient
。堆栈遍历在尝试查找返回地址时会迷失方向。
kernel32!GetConsoleInput+0x00000015
kernel32!ReadConsoleInputW+0x0000001A
DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr, InputRecord ByRef, Int32, Int32 ByRef)
System.Console.ReadKey(Boolean)
System.Console.ReadKey()
NativeLib!NativeABC+0x0000002E
0xFFFFFFFFCCCCCCCC
IDebugClient
接口应该能够遍历调用堆栈。我不知道它为什么会失败。区别在于,我的底部有一个托管 C# 应用程序调用到混合模式/库。另一个应用程序是一个 C++/CLI 应用程序,它调用到混合模式/托管库。库是相同的。
毕竟,结果并非我所期望的。即使我获得了完整的堆栈跟踪,使用 IDebugClient
和 IXCLRDataProcess
也不够。
解决方案
解决方案是使用 ICorProfiler
接口。它允许您创建一个与 CLR 交互的进程内性能分析器。它包含用于遍历混合模式应用程序的代码。进程内意味着它是一个加载到目标进程空间中的 DLL。这意味着我们也可以告别 IDebugClient
接口,因为无法将调试器附加到我们从中附加的同一个进程。我也制作了一个小型采样器性能分析器,但这将是另一篇文章。
关注点
互联网上有一些很棒的资源:Profiler stack walking: Basics and beyond 和 Building a mixed mode stack walker。最后一个链接让我意识到,我真正需要的是 ICorProfiler
接口。但那时我已经完成了我向您展示的大部分工作。所以我决定还是完成它。我做了一个勇敢的尝试,并且在此过程中学到了很多东西。我希望其中一些信息对您也有用。
对于使用 WinDbg 和 SOS 分析 .NET 应用程序内存转储的人来说,了解 mscordacwks.dll 至关重要。一个机器上的 .NET 应用程序的内存转储不能简单地复制到其他机器上进行分析。SOS 扩展必须加载正确版本的 mscordacwks.dll 才能理解内存转储。但是,如果保存内存转储的机器没有使用完全相同的 .NET Framework 版本,SOS 无法理解数据。为了克服这个问题,mscordacwks.dll 应该与转储文件一起复制。