解决托管和本机堆栈跟踪 - 使用哪个 API?

Resolve managed and native stack trace - which API to use?

这是我之前问题的延续 - 可以说是第 2 阶段。

第一个问题在这里:

现在我已经解析了大量的堆栈跟踪,现在想知道如何解析托管堆栈帧的符号信息。

对于本机 C++ 端,它相对简单 -

首先指定从哪个进程获取符号:

HANDLE g_hProcess = GetCurrentProcess();

您可以在 运行 时间内使用如下代码片段替换流程:

g_hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, g_processId);

b = (g_hProcess != NULL );

if( !b )
    errInfo.AppendFormat(_T("Process id '%08X' is not running anymore."), g_processId );
else
    InitSymbolLoad();

并初始化符号加载:

void InitSymbolLoad()
{
    SymInitialize(g_hProcess, NULL, TRUE);
    DWORD dwFlags = SymGetOptions();
    SymSetOptions(SymGetOptions() | SYMOPT_DEFERRED_LOADS | SYMOPT_NO_IMAGE_SEARCH);
}

然后解析原生符号,就像这样:

extern HANDLE g_hProcess;

void StackFrame::Resolve()
{
    struct {
        union
        {
            SYMBOL_INFO symbol;
            char buf[sizeof(SYMBOL_INFO) + 1024];
        }u;
    }ImageSymbol = { 0 };

    HANDLE hProcess = g_hProcess;
    DWORD64 offsetFromSymbol = 0;

    ImageSymbol.u.symbol.SizeOfStruct = sizeof(SYMBOL_INFO);
    ImageSymbol.u.symbol.Name[0] = 0;
    ImageSymbol.u.symbol.MaxNameLen = sizeof(ImageSymbol) - sizeof(SYMBOL_INFO);
    SYMBOL_INFO* pSymInfo = &ImageSymbol.u.symbol;

    // Get file / line of source code.
    IMAGEHLP_LINE64 lineStr = { 0 };
    lineStr.SizeOfStruct = sizeof(IMAGEHLP_LINE64);

    function.clear();


    if( SymGetLineFromAddr64(hProcess, (DWORD64)ip, (DWORD*)&offsetFromSymbol, &lineStr) )
    {
        function = lineStr.FileName;
        function += "(";
        function += std::to_string((_ULonglong) lineStr.LineNumber).c_str();
        function += "): ";
    }

    // Successor of SymGetSymFromAddr64.
    if( SymFromAddr(hProcess, (DWORD64)ip, &offsetFromSymbol, pSymInfo) )
        function += ImageSymbol.u.symbol.Name;

}

这看起来像工作。

但现在还管理堆栈帧。

我找到了两个接口:

  1. IDebugClient / GetNameByOffset

提及于:

使用者:

实现似乎在这里:

在 (*) 文章末尾提到。

方法 1 似乎很老套,文章 (*) 也提到了一些围绕它的问题。

方法 3 可能需要深入分析分析 API。 我还发现了关于这些 API 的提及 - 在这里:

https://naughter.wordpress.com/2015/05/24/changes-in-the-windows-10-sdk-compared-to-windows-8-1-part-two/

· cor.h, cordebug.h/idl, CorError.h, CorHdr.h, corhlpr.h, corprof.h/idl, corpub.h/idl & corsym.h/idl: All of these header files have been removed. They are all the native mode COM interface to .NET.

这句话我不是很懂。这些接口是死了还是被替换了,或者它们发生了什么?

因此,根据我的简要分析,我猜想方法 2 只是好的/有效的 API 界面,值得使用?您是否遇到过与 api 相关的任何问题。

这是 Jan Kotas 对此的回答:

From: Jan Kotas <jkotas@microsoft.com>
To: Tarmo Pikaro <tapika@yahoo.com> 
Sent: Tuesday, January 12, 2016 5:09 AM
Subject: RE: Fast capture stack trace on windows 64 bit / mixed mode...

Your solution based on IXCLRDATAProcess sounds good to me.

PerfView (https://www.microsoft.com/en-us/download/details.aspx?id=28567) – 
that does what you are trying to build as well as a lot of other stuff – is 
using IXCLRDATA* as well. You may be interested in 
https://github.com/Microsoft/clrmd . It is set of managed wrappers for 
IXCLRDATA* that are easier to use than the COM interfaces.

我简单尝试过的 - 这需要 Visual Studio 2015 / C# 6.0。

而且这个技术是不能用的。就像 .net StackTrace / StackFrame 正在立即解析调用堆栈和符号信息 - 我需要在之后解析符号信息(在堆栈跟踪捕获之后)。

备选方案 1 / IDebugClient / GetNameByOffset 不可用于托管堆栈跟踪,它只能用于本机代码 - 至于本机调用堆栈,我上面已经有演示代码片段。不确定 IDebugClient 是否提供了比 SymGetLineFromAddr64 更多的东西/SymFromAddr 没有提供 - 不确定。

在浏览了大量的代码示例和界面之后,我了解到没有任何简单易用的 API 界面。为本机 C++ 开发的代码和 API 仅适用于本机 C++,而为托管代码开发的代码和 API 仅适用于托管代码。

还有一个问题是之后解析堆栈跟踪可能无法正常工作。你看 - 开发人员可以使用 Jit 引擎/IL 生成器动态生成代码,并处理它 - 所以在你有 "void*" / 指令地址之后 - 你应该立即解析符号信息,而不是之后。但我暂时不谈这个,假设开发人员不是太花哨的编码员并且不会一直生成和处理新代码,并且不会在没有需要的情况下调用 FreeLibrary。 (如果我挂接 FreeLibrary / Jit 组件,我可以稍后解决这个问题。)

解析函数名称非常简单,通过 IXCLRDataProcess 加上一点魔法和运气 - 我能够获得函数名称,但是 - 我想更深入地扩展它 - 到确切的源代码路径和代码所在的源代码行正在执行,这变成了相当复杂的功能。

最后我找到了执行此类操作的源代码 - 并在此处完成:

https://github.com/dotnet/coreclr/blob/master/src/ToolBox/SOS/Strike/util.cpp

GetLineByOffset 是该文件中的函数名称。

我已经分析、重新调整并根据该源代码制作了我自己的解决方案,我现在将其附加在这里:

可以从这里找到更新的代码: https://sourceforge.net/projects/diagnostic/

但这里只是同一代码在某个时间点的快照:

ResolveStackM.h:

#pragma once
#include <afx.h>
#pragma warning (disable: 4091)     //dbghelp.h(1544): warning C4091: 'typedef ': ignored on left of '' when no variable is declared
#include <cor.h>                    //xclrdata.h requires this
#include "xclrdata.h"               //IXCLRDataProcess
#include <atlbase.h>                //CComPtr
#include <afxstr.h>                 //CString
#include <crosscomp.h>              //TCONTEXT
#include <Dbgeng.h>                 //IDebugClient
#pragma warning (default: 4091)

class ResoveStackM
{
public:
    ResoveStackM();
    ~ResoveStackM();
    void Close(void);

    bool InitSymbolResolver(HANDLE hProcess, CString& lastError);
    bool GetMethodName(void* ip, CStringA& methodName);
    bool GetManagedFileLineInfo(void* ip, CStringA& lineInfo);

    HMODULE mscordacwks_dll;
    CComPtr<IXCLRDataProcess> clrDataProcess;
    CComPtr<ICLRDataTarget> target;

    CComPtr<IDebugClient>       debugClient;
    CComQIPtr<IDebugControl>    debugControl;
    CComQIPtr<IDebugSymbols>    debugSymbols;
    CComQIPtr<IDebugSymbols3>   debugSymbols3;
};

//
// Typically applications don't need more than one instance of this. If you do, use your own copies.
//
extern ResoveStackM g_managedStackResolver;

ResolveStackM.cpp:

#include "ResolveStackM.h"
#include <Psapi.h>                      //EnumProcessModules
#include <string>                       //to_string
#pragma comment( lib, "dbgeng.lib" )


class CLRDataTarget : public ICLRDataTarget
{
public:
    ULONG refCount;
    bool bIsWow64;
    HANDLE hProcess;

    CLRDataTarget( HANDLE _hProcess, bool _bIsWow64 ) :
        refCount(1), 
        bIsWow64(_bIsWow64),
        hProcess(_hProcess)
    {
    }

    HRESULT STDMETHODCALLTYPE QueryInterface( REFIID riid, PVOID* ppvObject)
    {
        if ( IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, __uuidof(ICLRDataTarget)) )
        {
            AddRef();
            *ppvObject = this;
            return S_OK;
        }

        *ppvObject = NULL;
        return E_NOINTERFACE;
    }

    ULONG STDMETHODCALLTYPE AddRef( void)
    {
        return ++refCount;
    }

    ULONG STDMETHODCALLTYPE Release( void)
    {
        refCount--;

        if( refCount == 0 )
            delete this;

        return refCount;
    }

    virtual HRESULT STDMETHODCALLTYPE GetMachineType( ULONG32 *machineType )
    {
        #ifdef _WIN64
            if (!bIsWow64)
                *machineType = IMAGE_FILE_MACHINE_AMD64;
            else
                *machineType = IMAGE_FILE_MACHINE_I386;
        #else
            *machineType = IMAGE_FILE_MACHINE_I386;
        #endif

        return S_OK;
    }

    virtual HRESULT STDMETHODCALLTYPE GetPointerSize( ULONG32* pointerSize )
    {
#ifdef _WIN64
    if (!bIsWow64)
#endif
        *pointerSize = sizeof(PVOID);
#ifdef _WIN64
    else
        *pointerSize = sizeof(ULONG);
#endif
        return S_OK;
    }

    virtual HRESULT STDMETHODCALLTYPE GetImageBase( LPCWSTR imagePath, CLRDATA_ADDRESS *baseAddress )
    {
        HMODULE dlls[1024] = { 0 };
        DWORD nItems = 0;
        wchar_t path[ MAX_PATH ];
        DWORD whatToList = LIST_MODULES_ALL;

        if( bIsWow64 )
            whatToList = LIST_MODULES_32BIT;

        if( !EnumProcessModulesEx( hProcess, dlls, sizeof(dlls), &nItems, whatToList ) )
        {
            DWORD err = GetLastError();
            return HRESULT_FROM_WIN32(err);
        }

        nItems /= sizeof(HMODULE);
        for( unsigned int i = 0; i < nItems; i++ )
        {
            path[0] = 0;
            if( GetModuleFileNameEx(hProcess, dlls[i], path, sizeof(path) / sizeof(path[0])) )
            {
                wchar_t* pDll = wcsrchr( path, L'\');
                if (pDll) pDll++;

                if (_wcsicmp(imagePath, path) == 0 || _wcsicmp(imagePath, pDll) == 0)
                {
                    *baseAddress = (CLRDATA_ADDRESS) dlls[i];
                    return S_OK;
                }
            }
        }
        return E_FAIL;
    }

    virtual HRESULT STDMETHODCALLTYPE ReadVirtual( CLRDATA_ADDRESS address, BYTE *buffer, ULONG32 bytesRequested, ULONG32 *bytesRead )
    {
        SIZE_T readed;

        if( !ReadProcessMemory(hProcess, (void*)address, buffer, bytesRequested, &readed) )
            return HRESULT_FROM_WIN32( GetLastError() );

        *bytesRead = (ULONG32) readed;
        return S_OK;
    }

    virtual HRESULT STDMETHODCALLTYPE WriteVirtual( CLRDATA_ADDRESS address, BYTE *buffer, ULONG32 bytesRequested, ULONG32 *bytesWritten )
    {
        return E_NOTIMPL;
    }

    virtual HRESULT STDMETHODCALLTYPE GetTLSValue( ULONG32 threadID, ULONG32 index, CLRDATA_ADDRESS *value )
    {
        return E_NOTIMPL;
    }

    virtual HRESULT STDMETHODCALLTYPE SetTLSValue( ULONG32 threadID, ULONG32 index, CLRDATA_ADDRESS value )
    {
        return E_NOTIMPL;
    }

    virtual HRESULT STDMETHODCALLTYPE GetCurrentThreadID( ULONG32 *threadID )
    {
        return E_NOTIMPL;
    }

    virtual HRESULT STDMETHODCALLTYPE GetThreadContext( ULONG32 threadID, ULONG32 contextFlags, ULONG32 contextSize, BYTE *context )
    {
        return E_NOTIMPL;
    }

    virtual HRESULT STDMETHODCALLTYPE SetThreadContext( ULONG32 threadID, ULONG32 contextSize, BYTE *context)
    {
        return E_NOTIMPL;
    }

    virtual HRESULT STDMETHODCALLTYPE Request( ULONG32 reqCode, ULONG32 inBufferSize, BYTE *inBuffer, ULONG32 outBufferSize, BYTE *outBuffer)
    {
        return E_NOTIMPL;
    }
}; //CLRDataTarget





ResoveStackM::ResoveStackM() :
    mscordacwks_dll(0)
{

}

ResoveStackM::~ResoveStackM()
{
    Close();
}

void ResoveStackM::Close( void )
{
    clrDataProcess.Release();
    target.Release();
    debugClient.Release();

    if( mscordacwks_dll != 0 )
    {
        FreeLibrary(mscordacwks_dll);
        mscordacwks_dll = 0;
    }
}

bool ResoveStackM::InitSymbolResolver(HANDLE hProcess, CString& lastError)
{
    wchar_t path[ MAX_PATH ] = { 0 };

    // According to process hacker - mscoree.dll must be loaded before loading mscordacwks.dll.
    // It's enough if base application is managed.

    if( GetWindowsDirectoryW(path, sizeof(path)/sizeof(wchar_t) ) == 0 )
        return false;   //Unlikely to fail.

#ifdef _WIN64
    wcscat(path, L"\Microsoft.NET\Framework64\v4.0.30319\mscordacwks.dll");
#else
    wcscat(path, L"\Microsoft.NET\Framework\v4.0.30319\mscordacwks.dll");
#endif

    mscordacwks_dll = LoadLibraryW(path);
    PFN_CLRDataCreateInstance pCLRCreateInstance = 0;

    if( mscordacwks_dll != 0 )
        pCLRCreateInstance = (PFN_CLRDataCreateInstance) GetProcAddress(mscordacwks_dll, "CLRDataCreateInstance");

    if( mscordacwks_dll == 0 || pCLRCreateInstance == 0)
    {
        lastError.Format(L"Required dll mscordacwks.dll from .NET4 installation was not found (%s)", path);
        Close();
        return false;
    }

    BOOL isWow64 = FALSE;
    IsWow64Process(hProcess, &isWow64);
    target.Attach( new CLRDataTarget(hProcess, isWow64 != FALSE) );

    HRESULT hr = pCLRCreateInstance(__uuidof(IXCLRDataProcess), target, (void**)&clrDataProcess );

    if( FAILED(hr) )
    {
        lastError.Format(L"Failed to initialize mscordacwks.dll for symbol resolving (%08X)", hr);
        Close();
        return false;
    }

    hr = DebugCreate(__uuidof(IDebugClient), (void**)&debugClient);
    if (FAILED(hr))
    {
        lastError.Format(_T("Could retrieve symbolic debug information using dbgeng.dll (Error code: 0x%08X)"), hr);
        return false;
    }

    DWORD processId = GetProcessId(hProcess);
    const ULONG64 LOCAL_SERVER = 0;
    int flags = DEBUG_ATTACH_NONINVASIVE | DEBUG_ATTACH_NONINVASIVE_NO_SUSPEND;

    hr = debugClient->AttachProcess(LOCAL_SERVER, processId, flags);
    if (hr != S_OK)
    {
        lastError.Format(_T("Could attach to process 0x%X (Error code: 0x%08X)"), processId, hr);
        Close();
        return false;
    }

    debugControl = debugClient;

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

    debugSymbols3 = debugClient;
    debugSymbols  = debugClient;
    // if debugSymbols3 == NULL - GetManagedFileLineInfo will not work
    return true;
} //Init

struct ImageInfo
{
    ULONG64 modBase;
};

// Based on a native offset, passed in the first argument this function
// identifies the corresponding source file name and line number.
bool ResoveStackM::GetManagedFileLineInfo( void* ip, CStringA& lineInfo )
{
    ULONG lineN = 0;
    char path[MAX_PATH];
    ULONG64 dispacement = 0;

    CComPtr<IXCLRDataMethodInstance> method;
    if (!debugSymbols || !debugSymbols3)
        return false;

    // Get managed method by address
    CLRDATA_ENUM methEnum;
    HRESULT hr = clrDataProcess->StartEnumMethodInstancesByAddress((ULONG64)ip, NULL, &methEnum);
    if( hr == S_OK )
    {
        hr = clrDataProcess->EnumMethodInstanceByAddress(&methEnum, &method);
        clrDataProcess->EndEnumMethodInstancesByAddress(methEnum);
    }

    if (!method)
        goto lDefaultFallback;

    ULONG32 ilOffsets = 0;
    hr = method->GetILOffsetsByAddress((CLRDATA_ADDRESS)ip, 1, NULL, &ilOffsets);

    switch( (long)ilOffsets )
    {
        case CLRDATA_IL_OFFSET_NO_MAPPING:
            goto lDefaultFallback;

        case CLRDATA_IL_OFFSET_PROLOG:
            // Treat all of the prologue as part of the first source line.
            ilOffsets = 0;
            break;

        case CLRDATA_IL_OFFSET_EPILOG:
        {
            // Back up until we find the last real IL offset.
            CLRDATA_IL_ADDRESS_MAP mapLocal[16];
            CLRDATA_IL_ADDRESS_MAP* map = mapLocal;
            ULONG32 count = _countof(mapLocal);
            ULONG32 needed = 0;

            for( ; ; )
            {
                hr = method->GetILAddressMap(count, &needed, map);

                if ( needed <= count || map != mapLocal)
                    break;

                map = new CLRDATA_IL_ADDRESS_MAP[ needed ];
            }

            ULONG32 highestOffset = 0;
            for (unsigned i = 0; i < needed; i++)
            {
                long l = (long) map[i].ilOffset;

                if (l == CLRDATA_IL_OFFSET_NO_MAPPING || l == CLRDATA_IL_OFFSET_PROLOG || l == CLRDATA_IL_OFFSET_EPILOG )
                    continue;

                if (map[i].ilOffset > highestOffset )
                    highestOffset = map[i].ilOffset;
            } //for

            if( map != mapLocal )
                delete[] map;

            ilOffsets = highestOffset;
        }
        break;
    } //switch

    mdMethodDef methodToken;
    void* moduleBase = 0;
    {
        CComPtr<IXCLRDataModule> module;

        hr = method->GetTokenAndScope(&methodToken, &module);
        if( !module )
            goto lDefaultFallback;

        //
        // Retrieve ImageInfo associated with the IXCLRDataModule instance passed in. First look for NGENed module, second for IL modules.
        //
        for (int extentType = CLRDATA_MODULE_PREJIT_FILE; extentType >= CLRDATA_MODULE_PE_FILE; extentType--)
        {
            CLRDATA_ENUM enumExtents;
            if (module->StartEnumExtents(&enumExtents) != S_OK )
                continue;

            CLRDATA_MODULE_EXTENT extent;
            while (module->EnumExtent(&enumExtents, &extent) == S_OK)
            {
                if (extentType != extent.type )
                    continue;

                ULONG startIndex = 0;
                ULONG64 modBase = 0;

                hr = debugSymbols->GetModuleByOffset((ULONG64) extent.base, 0, &startIndex, &modBase);
                if( FAILED(hr) )
                    continue;

                moduleBase = (void*)modBase;

                if (moduleBase )
                    break;
            }
            module->EndEnumExtents(enumExtents);

            if( moduleBase != 0 )
                break;
        } //for
    } //module scope

    DEBUG_MODULE_AND_ID id;
    DEBUG_SYMBOL_ENTRY symInfo;
    hr = debugSymbols3->GetSymbolEntryByToken((ULONG64)moduleBase, methodToken, &id);
    if( FAILED(hr) )
        goto lDefaultFallback;

    hr = debugSymbols3->GetSymbolEntryInformation(&id, &symInfo);
    if (FAILED(hr))
        goto lDefaultFallback;

    char* IlOffset = (char*)symInfo.Offset + ilOffsets;

    //
    // Source maps for managed code can end up with special 0xFEEFEE markers that
    // indicate don't-stop points.  Try and filter those out.
    //
    for (ULONG SkipCount = 64; SkipCount > 0; SkipCount--)
    {
        hr = debugSymbols3->GetLineByOffset((ULONG64)IlOffset, &lineN, path, sizeof(path), NULL, &dispacement );
        if( FAILED( hr ) )
            break;

        if (lineN == 0xfeefee)
            IlOffset++;
        else
            goto lCollectInfoAndReturn;
    }

    if( !FAILED(hr) )
        // Fall into the regular translation as a last-ditch effort.
        ip = IlOffset;

lDefaultFallback:
    hr = debugSymbols3->GetLineByOffset((ULONG64) ip, &lineN, path, sizeof(path), NULL, &dispacement);

    if( FAILED(hr) )
        return false;

lCollectInfoAndReturn:
    lineInfo += path;
    lineInfo += "(";
    lineInfo += std::to_string((_ULonglong) lineN).c_str();
    lineInfo += "): ";
    return true;
}


bool ResoveStackM::GetMethodName(void* ip, CStringA& symbol)
{
    symbol.Empty();

    GetManagedFileLineInfo(ip, symbol);

    USES_CONVERSION;
    CLRDATA_ADDRESS displacement = 0;
    ULONG32 len = 0;
    wchar_t name[1024];
    if (!clrDataProcess )
        return false;

    HRESULT hr = clrDataProcess->GetRuntimeNameByAddress( (CLRDATA_ADDRESS)ip, 0, sizeof(name) / sizeof(name[0]), &len, name, &displacement );

    if( FAILED( hr ) )
        return false;

    name[ len ] = 0;
    symbol += W2A(name);
    return true;
} //GetMethodName



ResoveStackM g_managedStackResolver;

到目前为止只测试了一些较小的代码,只有 64 位(怀疑 32 位是否有效 - 我还没有确定调用堆栈)。

此代码可能包含错误,但我会尽力解决并修复它们。

我收获了很多代码,请将此答案标记为有用。 :-)