/ /マネージスタックトレースとネイティブスタックトレースを解決します-どのAPIを使用しますか? -ウィンドウズ、スタックトレース、混合モード

マネージドスタックトレースとネイティブスタックトレースを解決する-使用するAPI -ウィンドウ、スタックトレース、混合モード

これは、前の質問の続きです。いわばフェーズ2です。

最初の質問はここにありました: Windows / 64ビット/混在モードでのスタックトレースの高速キャプチャ

大量のスタックトレースを解決し、マネージドスタックフレームのシンボル情報を解決する方法を知りました。

ネイティブ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;

}

これは動作しているように見えます。

しかし、現在はスタックフレームも管理しています。

私が見つけた2つのインターフェイスがあります。

  1. IDebugClient / GetNameByOffset

言及:

によって使われた:

  • https://github.com/okigan/CrashInsight (コードは4年間変更されません)
  • 混合モードのスタックウォークの記事が良い例です。

    1. IXCLRDATAProcess / GetRuntimeNameByAddress
  • 上記の2つのリンクにも記載されています。

  • プロセスハッカーが使用(GPLライセンス、Cスタイル)

実装はここにあるようです:

(*)記事の最後に記載されています。

アプローチ1は非常に古い形式のようであり、記事(*)はそれに関するいくつかの問題を述べています。

アプローチ3では、プロファイリングAPIの詳細な分析が必要になるでしょう。 これらのAPIについて私が見つけた1つの言及もあります-ここに:

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:これらすべてのヘッダーファイル 削除されました。これらはすべて.NETへのネイティブモードCOMインターフェイスです。

この文は完全に理解していません。これらのインターフェースは無効になっているのか、置き換えられているのか、それともどうなったのですか?

だから私は私の簡単な分析アプローチに基づいて2は良い/生きているAPIインターフェイスだけを使用する価値があると思いますか?これらのAPIに関連する問題に遭遇しましたか?

回答:

回答№1は5

膨大な量のコードサンプルを調べた後とインターフェースについては、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ビットはまったく機能しないので、コールスタックの決定はまだありません)。

このコードにはバグが含まれている可能性がありますが、私はそれらを見つけ出して修正しようと試みます。

この回答を有用とマークするほど多くのコードを収集しました。 :-)


回答№2の場合は0

代替1 / IDebugClient / GetNameByOffsetはマネージドスタックトレースには使用できません。ネイティブコードにのみ使用できます。ネイティブコールスタックについては、すでに上記のデモコードスニペットを使用しています。 IDebugClientがSymGetLineFromAddr64 / SymFromAddrが提供する以上のものを提供するかどうかは不明-不明。


回答№3の場合は0

これに関する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は、呼び出しスタックとシンボル情報をすぐに解決しています-そして、後で(スタックトレースのキャプチャ後に)シンボル情報を解決する必要があります。