プログラミング

C++/WindowsでPDBを使ってスタックバックトレースを表示する

プログラムで構造化例外が出た時に、スタックバックトレースを見たくなったのですが、その時に作った簡単な動作検証用コードを載せておきます。

構造化例外が出るコード

まずはわざと構造化例外が発生するプログラムを用意します。

一番下のmain関数から始まって、test2関数で構造化例外が発生します。

#include "pch.h"
#include <iostream>
#include "StackBackTrace.h"

void test2()
{
    char *a = nullptr;
    memcpy(a, "abc", 4);
}

void test1()
{
    test2();
}

void se_translator(unsigned int u, _EXCEPTION_POINTERS *e)
{
    printf_s("In se_translator. cEip = 0x%x\n", e->ContextRecord->Eip);
    StackBackTrace stackBackTrace;
    printf_s(stackBackTrace.build().c_str());
}

int main()
{
    _set_se_translator(se_translator);
    try
    {
        test1();
    }
    catch (...)
    {}
    return 0;
}

例外が発生したら、se_translator関数でStackBackTraceクラスを使ってスタックバックトレースを表示するようにしています。

なお、このプログラムを実行するためにはコンパイラの設定で「C++の例外を有効にする」を「はい - SEHの例外あり(/EHa)」にしておく必要があります。

StackBackTraceクラス

スタックバックトレースを表示するクラスでは、CaptureStackBackTrace関数で取得したアドレスを使ってSymFromAddr関数でシンボル情報を取得しています。

本筋とは関係ありませんが、newするのが嫌だったので、あらかじめ必要なサイズ分確保したsymbol変数を取りまわす方法を取っています。

#include "StackBackTrace.h"
#include <dbghelp.h>

StackBackTrace::StackBackTrace()
    : _hProcess(GetCurrentProcess())
    , _symInitialized(SymInitialize(_hProcess, NULL, TRUE))
{}

StackBackTrace::~StackBackTrace()
{
    if (_symInitialized)
    {
        SymCleanup(_hProcess);
    }
}

std::string StackBackTrace::build()
{
    void *symbol[sizeof(SYMBOL_INFO) + MAX_SYMBOL_NAME_LEN];
    reinterpret_cast<SYMBOL_INFO *>(symbol)->SizeOfStruct
        = sizeof(SYMBOL_INFO);
    reinterpret_cast<SYMBOL_INFO *>(symbol)->MaxNameLen
        = MAX_SYMBOL_NAME_LEN;

    std::string result;
    void *stack[MAX_FRAMES_TO_CAPTURE];
    const WORD frames = CaptureStackBackTrace(
        0
        , MAX_FRAMES_TO_CAPTURE
        , stack
        , NULL
    );
 
    for (WORD i = 0; i < frames; i++)
    {
        SymFromAddr(
            _hProcess
            , reinterpret_cast<DWORD64>(stack[i])
            , 0
            , reinterpret_cast<SYMBOL_INFO *>(symbol)
        );

        char buf[1024];
        sprintf_s(
            buf
            , "%05d: %s - 0x%llx\n"
            , frames - i - 1
            , reinterpret_cast<SYMBOL_INFO *>(symbol)->Name
            , reinterpret_cast<SYMBOL_INFO *>(symbol)->Address
        );
        result += buf;
    }

    return result;
}

なお、取得したアドレスをシンボル情報と関連付ける際は、該当プログラムをビルドするときに同時にできるPDBファイル(Program Databaseファイル)が必要になります。

PDBファイルは、該当プログラムのカレントディレクトリか、_NT_SYMBOL_PATH環境変数、または_NT_ALTERNATE_SYMBOL_PATH環境変数が指すディレクトリに置いておく必要がありますが、以下いずれかの方法によりそのパスを指定することもできます。

  • SymSetSearchPath関数でパスを指定する
  • SymInitialize関数の第二引数にパスを指定する

それぞれ以下のような形になります。

SymSetSearchPath(_hProcess, "C:\hogehoge\;C:\fugafuga\");

SymInitialize(_hProcess, "C:\hogehoge\;C:\fugafuga\", TRUE);

参考:

https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symsetsearchpath

https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-syminitialize

実行結果

サンプルプログラムの実行結果です。下から追っていくことで、実行経路を追うことができます。

サンプルコードのダウンロード

本記事で紹介したサンプルコード全体を以下にアップロードしました。

ご自身の責任の下で、自由にご利用ください。

GitHub - fuket/StackBackTraceSample: C++/WindowsでPDBを使ってスタックバックトレースを表示するサンプル
GitHub - fuket/StackBackTraceSample: C++/WindowsでPDBを使ってスタックバックトレースを表示するサンプル

C++/WindowsでPDBを使ってスタックバックトレースを表示するサンプル. Contribute to fuket/StackBackTraceSample development by cre ...

続きを見る

 

-プログラミング