スタックトレースを表示する
出典: encom wiki
daisuke 2007年8月25日 (土) 16:10
スタックトレースを表示するには、以下の手順を行います。
- 現在のアドレスを元に、StackWalk64 という関数を呼び、呼び出し元アドレスを取得。
- 呼び出し元アドレスから、その関数名、ファイル名、行数を取得。
- 呼び出し元アドレスを現在のアドレスに置き換える。
- 呼び出し元が無くなるまで 1 から繰り返す。
簡単そうなのですが、最初の「現在のアドレスを取得する」ところと、関数名などの情報を取得するための「前準備」などで結構な作業があります。
スタックトレースを表示するための主要な関数は次のとおりです。
- StackWalk64
- アドレスを取得する
- SymFromAddr
- シンボル(関数名)を取得する
- SymGetLineFromAddr64
- ファイル名、行数を取得する
目次 |
[編集] StackWalk64
StackWalk64 の定義は、
BOOL StackWalk64( DWORD MachineType, HANDLE hProcess, HANDLE hThread, LPSTACKFRAME64 StackFrame, PVOID ContextRecord, PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine, PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine, PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine, PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress );
ですので、これらに正しい引数を与えれば、呼び出し元のアドレスが取得できます。
[編集] MachineType
MachineType はアーキテクチャによって変わります。
IMAGE_FILE_MACHINE_I386, IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_IA64 の 3 つの値があり、ビルド時に決定します。
#if defined(_M_IX86)
DWORD machineType = IMAGE_FILE_MACHINE_I386;
#elif defined(_M_AMD64)
DWORD machineType = IMAGE_FILE_MACHINE_AMD64;
#elif defined(_M_IA64)
DWORD machineType = IMAGE_FILE_MACHINE_IA64;
#else
#error "Not Supported."
#endif
[編集] hProcess, hThread
hProcess, hThread はそれぞれ GetCurrentProcess(), GetCurrentThread() の戻り値になります。
[編集] StackFrame
StackFrame は、現在のアドレスで初期化する必要があります。初期値は次の ContextRecord で決まりますが、ContextRecord がアーキテクチャによって変わるため、MachineType と同様にビルド時に切り替えます。また Intel x64 系では、設定するメンバも違います(AddrStack のかわりに AddrBStore)。
STACKFRAME64 stackFrame;
memset(&stackFrame, 0, sizeof(stackFrame));
#if defined(_M_IX86)
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;
#elif defined(_M_AMD64)
stackFrame.AddrPC.Offset = context.Rip;
stackFrame.AddrPC.Mode = AddrModeFlat;
stackFrame.AddrFrame.Offset = context.Rbp;
stackFrame.AddrFrame.Mode = AddrModeFlat;
stackFrame.AddrStack.Offset = context.Rsp;
stackFrame.AddrStack.Mode = AddrModeFlat;
#elif defined(_M_IA64)
stackFrame.AddrPC.Offset = context.StIIP;
stackFrame.AddrPC.Mode = AddrModeFlat;
stackFrame.AddrStack.Offset = context.IntSp;
stackFrame.AddrStack.Mode = AddrModeFlat;
stackFrame.AddrBStore.Offset = context.RsBSP;
stackFrame.AddrBStore.Mode = AddrModeFlat;
#else
#error "Not Supported."
#endif
上記の context は、下記の ContextRecord 値です。
[編集] ContextRecord
ContextRecord の実体は、CONTEXT 構造体です。CONTEXT 構造体はアーキテクチャによってかわります。ただし、StackWalk64 で利用する場合、StackFrame に値を設定するとき以外は意識する必要はありません。
CONTEXT 値の取得は、以下のように 32 ビット系と 64 ビット系とでわかれます。
CONTEXT context;
memset(&context, 0, sizeof(context));
context.ContextFlags = CONTEXT_FULL;
#if defined(_M_IX86)
__asm call(x);
__asm x: pop eax;
__asm mov context.Eip, eax;
__asm mov context.Ebp, ebp;
__asm mov context.Esp, esp;
#else
RtlCaptureContext(&context);
#endif
64 ビット系には RtlCaptureContext 関数があり、簡単に取得できるのですが、32 ビット系はレジスタから取得する必要があります。
- アセンブラブロックの補足
- call した場合、戻り先(call の次のアドレス)が push されます。そのため、call 先で pop すると戻り先が取得できます。これを現在のアドレスとしています。
[編集] その他
その他の引数は NULL になります。
[編集] シンボル情報の読み込み(SymLoadModule64)
シンボル情報の読み込みには SymLoadModule64 関数を実行します。読み込むモジュール情報の取得には Toolhelp32 の Module32First と Module32Next を利用します。
[編集] サンプルソース
StackTrace.h
#include <windows.h>
//
// スタックトレースの出力を提供します。
//
class StackTrace
{
private:
bool LoadModules(HANDLE hProcess, DWORD dwProcessId);
public:
StackTrace(void);
~StackTrace(void);
void Show();
};
StackTrace.cpp
#include "StackTrace.h"
#include <dbghelp.h>
#include <tlhelp32.h>
#pragma comment(lib, "Dbghelp.lib")
//
// デフォルトコンストラクタ
//
StackTrace::StackTrace(void)
{
}
//
// デストラクタ
//
StackTrace::~StackTrace(void)
{
}
//
// モジュールを読み込みます。
//
// 引数:[i] hProcess ... プロセスハンドル
// dwProcessId ... プロセスID
//
// 戻り値:結果
//
bool StackTrace::LoadModules(HANDLE hProcess, DWORD dwProcessId)
{
HANDLE hSnap;
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
if (hSnap == (HANDLE)(-1))
{
return false;
}
try
{
BOOL loop;
MODULEENTRY32 entry;
entry.dwSize = sizeof(entry);
// モジュールを取得
loop = Module32First(hSnap, &entry);
while (loop)
{
TCHAR* image;
TCHAR* module;
image = _tcsdup(entry.szExePath);
module = _tcsdup(entry.szModule);
SymLoadModule64(hProcess, NULL, (PSTR)(image), (PSTR)(module),
(DWORD64)(entry.modBaseAddr),
static_cast<DWORD>(entry.modBaseSize));
loop = Module32Next(hSnap, &entry);
}
CloseHandle(hSnap);
}
catch (...)
{
CloseHandle(hSnap);
}
return true;
}
//
// スタックトレースを表示します。
//
void StackTrace::Show()
{
HANDLE hProcess = GetCurrentProcess();
DWORD dwProcessId = GetCurrentProcessId();
// 初期化
if (!SymInitialize(hProcess, NULL, FALSE))
{
throw;
}
// モジュール読み込み
this->LoadModules(hProcess, dwProcessId);
// Context 取得
HANDLE hThread = GetCurrentThread();
CONTEXT context;
memset(&context, 0, sizeof(context));
context.ContextFlags = CONTEXT_FULL;
#if defined(_M_IX86)
__asm call(x);
__asm x: pop eax;
__asm mov context.Eip, eax;
__asm mov context.Ebp, ebp;
__asm mov context.Esp, esp;
#else
// for Windows 64-bit editions
RtlCaptureContext(&context);
#endif
STACKFRAME64 stackFrame;
memset(&stackFrame, 0, sizeof(stackFrame));
#if defined(_M_IX86)
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;
DWORD machineType = IMAGE_FILE_MACHINE_I386;
#elif defined(_M_AMD64)
stackFrame.AddrPC.Offset = context.Rip;
stackFrame.AddrPC.Mode = AddrModeFlat;
stackFrame.AddrFrame.Offset = context.Rbp;
stackFrame.AddrFrame.Mode = AddrModeFlat;
stackFrame.AddrStack.Offset = context.Rsp;
stackFrame.AddrStack.Mode = AddrModeFlat;
DWORD machineType = IMAGE_FILE_MACHINE_AMD64;
#elif defined(_M_IA64)
stackFrame.AddrPC.Offset = context.StIIP;
stackFrame.AddrPC.Mode = AddrModeFlat;
stackFrame.AddrStack.Offset = context.IntSp;
stackFrame.AddrStack.Mode = AddrModeFlat;
stackFrame.AddrBStore.Offset = context.RsBSP;
stackFrame.AddrBStore.Mode = AddrModeFlat;
DWORD machineType = IMAGE_FILE_MACHINE_IA64;
#else
#error "Not Supported."
#endif
for ( ; ; )
{
// スタックフレームを取得
if (!StackWalk64(machineType, hProcess, hThread, &stackFrame,
&context, NULL, NULL, NULL, NULL))
break;
// スタックフレームを検証
if (stackFrame.AddrPC.Offset == stackFrame.AddrReturn.Offset)
{
// エンドレスになるので終わり
break;
}
if (stackFrame.AddrPC.Offset == 0)
{
// 不正なスタックフレーム
break;
}
if (stackFrame.AddrReturn.Offset == 0)
{
// 最後のスタックフレーム
break;
}
// ファイル名、行数、メソッド名を取得
DWORD64 displacement64;
ULONG64 buffer[(sizeof(SYMBOL_INFO) +
MAX_SYM_NAME * sizeof(TCHAR) +
sizeof(ULONG64) - 1) /
sizeof(ULONG64)];
PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer;
pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
pSymbol->MaxNameLen = MAX_SYM_NAME;
if (SymFromAddr(hProcess, stackFrame.AddrReturn.Offset,
&displacement64, pSymbol))
{
DWORD displacement;
IMAGEHLP_LINE64 Line;
memset(&Line, 0, sizeof(Line));
Line.SizeOfStruct = sizeof(Line);
if (SymGetLineFromAddr64(hProcess, stackFrame.AddrReturn.Offset,
&displacement, &Line))
{
printf("%s (%lu): %s\n", Line.FileName, Line.LineNumber,
pSymbol->Name);
}
else
{
printf("??? (---): %s\n", pSymbol->Name);
}
}
else
{
printf("Failed to retrieve symbol information: 0x%08x\n",
stackFrame.AddrReturn.Offset);
}
}
}
StackWalk64 は dbghelp.dll で定義されていますが、dbghelp.dll のバージョンによってサポートされている関数や動作が若干違うようです。
実際に利用する場合は、LoadModule を使って各関数がサポートされているか調べる必要があります。
