スタックトレースを表示する

出典: encom wiki

daisuke 2007年8月25日 (土) 16:10


スタックトレースを表示するには、以下の手順を行います。

  1. 現在のアドレスを元に、StackWalk64 という関数を呼び、呼び出し元アドレスを取得。
  2. 呼び出し元アドレスから、その関数名、ファイル名、行数を取得。
  3. 呼び出し元アドレスを現在のアドレスに置き換える。
  4. 呼び出し元が無くなるまで 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 を使って各関数がサポートされているか調べる必要があります。

リンク