Click here to Skip to main content
Click here to Skip to main content

A Mixed-Mode Stackwalk with the IDebugClient Interface

, 22 Apr 2012 Ms-PL
Rate this:
Please Sign up or sign in to vote.
A native stackwalk funtion like Stackwalk64 cannot handle mixed-mode stacks, since managed code does not use the stack in the same way as native code does. There is an API called IDebugClient, that does walk a mixed-mode stack correctly, which we will explore.

Introduction

Do you need to traverse the callstack of a mixed-mode (unmananged/managed) application or are curious how it can be done? In this article, I show how the IDebugClient interface can be used to walk a mixed-mode callstack, and how to use the IXCLRDataProcess interface to find the symbol names of managed methods.

Although it gives a full native callstack, it is not able to completely resolve all managed method names. But if you are curious about the IDebugClient interface and want to know more about how to interact with the CLR runtime, I think it can be interesting to continue reading.

Background

It all began with a wish to improve the performance of an application at work. The application is partly written in C++, partly in C#. Part of the C++ framework had already been optimized. This was done by simply inserting a call to StackWalk64 (dbghelp.dll), at places that we called often, and writing the callstack to disk. Yes, it is a poor-man's profiler, but the truth is that it is effective and is very easy to use. The weakness is that it does not handle managed code, so I only got a partial stacktrace. This led my to look at alternative stackwalk APIs. Each with its own pros and cons.

Alternative APIs

Below is an example of a mixed-mode callstack.

We start from the bottom, where a native code calls into managed code. This managed code calls into native code, and we end up with an interleaved stack.

System.Diagnostics.StackTrace

In .NET, you can quite easily walk the stack with the System.Diagnostics.StackTrace class. Below is a sample written in C++/CLI (usable from both managed and unmanaged code).

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());
}

Calling it in my mixed-mode application (C++/CLI), it gives me the following output:

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)

It correctly unwinds the callstack down to the last function call MixedABCDEF.

StackWalk64

Stackwalk64 lets us see native stack frames on the stack, but not the managed frames. One reason for this is that managed frames does not use the stack in the same way as native code. Below is how you more or less use the StackWalk64 function. In order to map the Instruction Pointers to symbol names, one must call SymInitialize once, and call SymGetSymFromAddr64 for each found EIP.

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);
    }	
}

Using the same mixed-mode application, I get the following output:

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

There are symbols that cannot be found. They actually belong to kernel32 and msvcrt. They should have been resolved, with a little troubleshooting they can probably be resolved. Remember that SymInitialize is asynchronous, it returns but Symbol files are loaded in the background. If you try to resolve before the symbol file has been loaded, you will get an error.

What I wanted to show was that the managed frames are not displayed. They are displayed as function calls within the CLR runtime, which isn't very helpful.

WinDbg

Let's see how WinDbg handles a mixed mode callstack.

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

The stack looks very similar to mine. A bit better, because it correctly resolves functions from kernel32 and msvcrt. But look closely. But there are addresses that cannot be resolved. Apparently, a normal stackwalk gets confused "WARNING: Frame IP not in any known module. Following frames may be wrong." Normally DLLs get loaded into a memory space, and code is located within that memory range. Assemblies are also loaded, but don't contain any executable code. The JIT compiler takes the IL code and generates machine code which it puts on the heap. A native stackwalker only sees the generated code, which is not in any loaded module (correct). The stackwalker doesn't know anything about the IL code, nor can it use the PDB files correctly, because it maps to the IL-code and not the machine dependent code.

WinDbg with SOS extension

SOS is a WinDbg extension to debug managed applications. It is capable of walking mixed stackframes with the !clrstack command. Let's see how well it performs.

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]

Pretty neat. This is what we want to obtain.

Discarding System.Diagnostics.StackTrace

The StackTrace class works great, but it has disadvantages.

Firstly, it uses Reflection and is really slow. Secondly, the old way of manually instrumenting code is not good any more. The stacktrace calls can not be left in the source code, and adding and removing them each time would be time consuming. Thirdly, I didn't really know where we had performance problems and where the stack should be traced. What I now need is to take stacktrace samples and based on the frequency be pointed in a general direction where the problem was. This is also know as Sample Profiling. An external application hooks up to a target app, and at regular intervals, e.g., every 20 ms, it takes a stacktrace sample.

Where to find information about possible solutions

  • One possibility is to try to reverse engineer the SOS extension.
  • Try to find some CLR API, maybe look at mscoree.h and related includes.
  • Look at the Mono source code.
  • Look at Microsoft's Shared Source CLI (Rotor V2.0). source code.
  • Google Google Google

If you want to know more about the CLR runtime and how to interact with it through mscoree, and the CLR hosting interfaces, I can recommend reading Customizing the Microsoft® .NET Framework Common Language Runtime. The CLR APIs might not be enough. But a wonderful source of inspiration is the Rotor source code, it is the implementation of an unoptimized CLR runtime written by Microsoft for standardization purposes.

The implementation

We will explore two interfaces. IDebugClient that is said to give the full stacktrace and IXCLRDATAProcess (mscordacwks.dll) to translate managed addresses into readable method names.

IDebugClient

Microsoft has been kind enough to provide an API that can walk mixed-frames, IDebugClient which is exposed by dbgeng.dll.

It is fairly straightforward to use the API. There are several samples on the internet. You create an object through a special function called DebugCreate and feed it the GUID of the IDebugClient interface.

DebugClient* debugClient = nullptr;
if ((hr = DebugCreate(__uuidof(IDebugClient), (void **)&debugClient)) != S_OK)
{
  return false;
}
m_debugClient = debugClient;

Attaching to a process is straightforward:

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;

I actually got into problems with the attach. It sometimes worked, sometimes not. It worked when I debugged it. It even worked by adding a sleep after the attach. I found out what it was. It takes some time for the attach to complete, it is only initiated, so the object isn't really ready yet. What we can do is, to set the execution status of the target app to "go". The process is already running (it was never suspended), so the call will hopefully return immediately. But here is the clever thing. It returns when the debugger is properly attached.

hr = m_debugControl->SetExecutionStatus(DEBUG_STATUS_GO);

if ((hr = m_debugControl->WaitForEvent(DEBUG_WAIT_DEFAULT, INFINITE)) != S_OK)
{
    return false;
}

Then using the IDebugClient object, you create the other COM objects that you might need.

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))

I made one big mistake, which took some time to fix. Since I was just interested in the function IDebugControl4::GetStackTrace, and I wasn't going to use it for stepping, setting breakpoints, etc., I didn't bother to implement the callback functions for printing to screen, and the thing kept crashing on me when trying to get the interfaces. Come on! Couldn't someone have inserted an extra test to see whether users of the API were interested in the events or the output? I implemented these debug output callback classes too. Well, I left the function bodies totally empty. I wasn't interested in it anyway.

HRESULT hr1 = m_debugClient->SetOutputCallbacks(&g_DebugOutputCallback);
HRESULT hr2 = m_debugClient->SetEventCallbacks(&g_DebugEventCallbacks);

CLRDataCreateInstance

CLRDataCreateInstance (defined in clrdata.idl CorGuids.lib) can return a COM object with the interface IXCLRDataProcess. This object can enumerate tasks, appdomains, methods, etc. It also contains functions for mapping addresses or internal CLR IDs to methods, classes, assemblies, etc. Quite neat. This might be what we need.

HRESULT CLRDataCreateInstance (
    [in]  REFIID           iid, 
    [in]  ICLRDataTarget  *target, 
    [out] void           **iface
);

A small problem is that the IXCLRDataProcess interface doesn't have a header file, but you can generate one from xclrdata.idl which is part of the Rotor source code. According to the license, it is allowed to use the source code for non-commercial purposes.

Some of the Rotor source code can be non-trivial to understand. I want to give credit to Steve's Blog, which gives some useful instructions, but unfortunately, Steve supplies no source code Frown | :( So, there was actually quite a lot of implementation and debugging work left to do.

Implementing ICLRDataTarget

In order to create an IXCLRDataProcess, the function CLRCreateInstance expects an ICLRDataTarget object that interacts with the managed application. It is an interface that needs to be implemented by the user. I have no idea why a default implementation of this interface doesn't already exist. It does only basic stuff such as reading and writing to raw memory, returning pointer size, etc.

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        
};

I cut some corners doing the implementation. I only support the x86 architecture. Managed apps, compiled for the "any" platform, can run in both x86/x64 mode depending on the OS hosting it, but in Visual Studio 2010, it actually defaults to the x86 architecture. Apart from that, I wanted the x86 to work first, before I tried x64. Always do the easy case first. When it works, we extend.

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 
};

The Stackwalker class

Initialization

In order to attach to a running process, some basic initialization is needed:

bool Stackwalker::Initialize(int pId)
{
  m_pId = pId;
  m_isClr4 = IsClr4Process(pId);
  m_isManaged = IsDotNetProcess(pId);
  bool result = m_debugNative->Initialize(pId);
  return result;
}

How do you know if a process is a .NET process? The PE file header, present in all executables, contains that information. A simpler way is to look if certain CLR modules have been loaded like clr, clrjit, mscorlib_ni, mscoree, etc.

How do you know if it is a CLR v4.0? Look for clr.dll. Mscorwks.dll was renamed from v2.0 to v4.0. Might give false positives, if someone names their modules clr.dll, but isn't this article about mixed-mode/managed apps anyway?

Obtaining the IXCLRDataprocess object

To create the IXCLDataProcess object we need to call CLRDataCreateInstance located in the data access DLL named mscordacwks.dll. Remember that the DLL is CLR version dependent, so it must be loaded from the correct file location, that is why we check the CLR version. Then we have to manually load the library into memory, then call GetProcessAddress to get the address of CLRDataCreateInstance. Finally we call the function, giving it an instance of our 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;
}

I am sorry about all the TCHARs, char, std::string, and std::wstring you might find in my code. A TCHAR is a wchar_t when compiled for Unicode, and a char when compiled in multibyte. Regardless of how it is compiled. StackWalk64 always uses chars, but most Win32 APIs adapts themselves. It can be messy sometimes when you have to convert back and forth.

Putting it all together

HMODULE accessDLL;
void* iface = NULL;
HRESULT hr = LoadDataAccessDLL(m_isClr4, m_clrDataTarget, &accessDLL, &iface);
m_clrDataProcess = static_cast<IXCLRDataProcess*>(iface);

Now we are ready to use the object with the instruction pointers we get from the stackwalk.

HRESULT hr = m_clrDataProcess->GetRuntimeNameByAddress(
               clrAddr /*EIP*/, 0, maxSize - 1 , &nameLen, nameBuffer, &displacement);

It will return a symbolname for the managed Instruction Pointer (IP).

Resolving Managed Method names using IXCLRDataProcess::Request

In Steve's blog, you can read about something called DacpMethodDescData and IXCLRDataProcess::Request. It is a generic interface that takes an enum value describing what type of data you want, a pointer to the input parameter, and a pointer to the output parameter.

return dac->Request(DACPRIV_REQUEST_METHODDESC_NAME,
                    sizeof(addrMethodDesc), (PBYTE)&addrMethodDesc,
                    sizeof(WCHAR)*iNameChars, (PBYTE) pwszName);

A powerful interface, if you know what enum values to send in, otherwise you are doomed. It gives the same info as GetRuntimeNameByAddress. Below is a code snippet, you can also find it in the attached source code.

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);
}

Sample apps

There are three applications present in the demo folder. It looks in the current folder for pdb files. It also looks in C:\symbols. It also tries to download PDB files from Microsoft and store them in C:\symbols. Without these symbols, Stackwalk64 might get lost very quickly, since it doesn't know about the calling convention, omitted frame-pointers, and other optimizations.

Start CppCliApp.exe first, it prints the process ID, and outputs a Stackwalk64 and a System.Diagnostics.StackTrace callstack. Use the pId when you use the other apps.

c:\Demo>CppCliApp.exe

Current Process Id #4404
---- StackTrace ----
....
---- StackWalk64 ----
....

C:\Demo>DiagApp.exe 4404
C:\Demo>StackWalk64App 4404

This is the result I get from DiagApp.exe (using the IClientDebug Interface):

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:

We managed to get a full stacktrace from a managed app. It even resolved the addresses that WinDbg failed on, thanks to mscordacwks.dll. But my own managed classes still don't appear. This is unfortunate. The calls to clr!xxx makes absolute sense if we think about it. IL code cannot run, it must be JITted to machine code, but the CLR probably has a native function that executes JITted code. It is this function that we see.

On a purely managed app, I actually get a much better stacktrace. Many CLR functions show up in readable code, but my own managed method names are still hiding.

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
...

I have another mixed mode app, that actually confuses IDebugClient. The stackwalk gets lost trying to find return addresses.

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

The IDebugClient interface is supposed to be able to walk the callstack. I don't know why it fails. The difference is that I have a Managed C# app at the bottom that calls into mixed-mode/libraries. The other app was a C++/CLI app that called into mixed-mode/managed libraries. The libraries are the same.

After all it was not the result I expected. Even if I got the full stacktrace, using the IDebugClient and the IXCLRDataProcess is not enough.

The solution

The solution is to use the ICorProfiler interface instead. It allows you to create an in-process profiler that interacts with the CLR. It contains code for walking mixed mode apps. Inprocess means that it is a DLL that loads into the process space of the target process. This means also that we can say goodbye to the IDebugClient interface, since it is not possible to attach a debugger to the same process we make the attach from. I have done a small sampler profiler too, but it will be another article.

Points of interest

There are some great sources on the internet Profiler stack walking: Basics and beyond and Building a mixed mode stack walker The last link made me realise that what I really needed was the ICorProfiler interface. But at that point I had already done 95% of what I have just shown you. So I decided to finish it anyway. I made a brave attempt, and I learned a great deal along the way. I hope some of this information can be useful for you too.

For people analyzing memory dumps of .NET apps using WinDbg and SOS, mscordacwks.dll is a must to know about. A memory dump of a .NET app from one machine cannot simply be copied to other machines for analyzing. The SOS extension must load the correct version of mscordacwks.dll in order to understand the memory dump. But if the machine where the memory dump was saved didn't use exactly the same .NET Framework version, the SOS cannot understand the data. To overcome this problem, mscordacwks.dll should be copied along with the dump file.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Mattias Högström
Architect Visma Software AB
Sweden Sweden
Mattias works at Visma, a leading Nordic ERP solution provider. He has good knowledge in C++/.Net development, test tool development, and debugging. His great passion is memory dump analysis. He likes giving talks and courses.
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 Pinmember Randor 22-Apr-12 11:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.141022.2 | Last Updated 22 Apr 2012
Article Copyright 2012 by Mattias Högström
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid