LoginSignup
29
26

More than 5 years have passed since last update.

C++で実行時エラーを追跡と特定します

Last updated at Posted at 2014-10-31

Pythonを使った頃はバックトレース機能がとても強いためエラーの追跡に悩んだことはあまりありませんでした。

>>> def hogehoge(): print traceback.print_stack()
... 
>>> def hoge(): hogehoge()
... 
>>> hoge()
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in hoge
  File "<stdin>", line 1, in hogehoge
None

しかしC++ではそんな便利なことはなかなかできません。
自分が模索してきたエラーの追跡と特定手法をここに書いておきます。
デバッグツールとMSVCを使ってる人には向けません。

スタックトレース

コールツリーを調べるときに使えます。

以下のコードは
Windows 7 mingw-w64 i686 gcc 4.9.1
Centos 6.5 Clang 3.5
MacOSX 10.9 Clang 3.4
にてテスト済み。

traceback.cpp
#include <iostream>
#include <sstream>
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
    #define __posix__
    #include <execinfo.h>
#elif defined(__MINGW32__)
    #include <windows.h>
#endif

/// バックトレース情報を取得します
std::string get_backtrace_info() {
    std::string result;
#if defined(__posix__)
    const static ssize_t trace_frames_max = 100;
    void* trace_frames[trace_frames_max] = {};
    int size = ::backtrace(trace_frames, trace_frames_max);
    if (size < 0 || size >= trace_frames_max)
        return std::string("get_backtrace_info error: backtrace failed");
    char** trace_symbols = ::backtrace_symbols(trace_frames, trace_frames_max);
    if (!trace_symbols)
        return std::string("get_backtrace_info error: get symbol failed");
    for (ssize_t i = 1; i < size; ++i) { // 呼び出し先のアドレスから書き込みます
        result.append(trace_symbols[i]);
        result.append("\n");
    }
    free(trace_symbols); // [pointer, pointer, char*, char*, ...]
    trace_symbols = nullptr;
#elif defined(__MINGW32__)
    const static ssize_t trace_frames_max = 50;
    void* trace_frames[trace_frames_max] = {};
    // trace_frames_maxが63以下でない場合は0しか返しません
    int size = ::CaptureStackBackTrace(0, trace_frames_max, trace_frames, nullptr);
    if (size < 0 || size >= trace_frames_max)
        return std::string("get_backtrace_info error: backtrace failed");
    // mingwはpdbを生成できないので関数の名前は取得できません
    // ここはアドレスのみを出力します
    std::ostringstream address_str;
    for (ssize_t i = 1; i < size; ++i) {
        int64_t address = reinterpret_cast<int64_t>(trace_frames[i]);
        address_str << i << ": 0x" << std::hex << address << "\n";
    }
    result.assign(address_str.str());
#else
    return std::string("get_backtrace_info error: unknow platform");
#endif
    return result;
}

void hogehoge() {
    std::cout << get_backtrace_info() << std::endl;
}

void hoge() {
    hogehoge();
}

int main() {
    hoge();
    return 0;
}

実行結果(Linux/OSX)

./a.out(_Z8hogehogev+0x16) [0x8049526]
./a.out(_Z4hogev+0xb) [0x80495ab]
./a.out(main+0x12) [0x80495c2]
/lib/libc.so.6(__libc_start_main+0xe6) [0xa46d36]
./a.out() [0x8049111]

実行結果(Windows)

1: 0x4017ff
2: 0x401855
3: 0x401867
4: 0x4013e2
5: 0x7d4e7d2a

実はこの情報を基づけばソースコードの行まで特定できます。
まずはコンパイルの時-gをつけること、そして以下のコマンドを実行して対応するソースコードを含めるアセンブリコードを出力します。
objdump -C -S -M intel a.out > a.out.s

.sファイルを取得したあとはこのコマンドでa.outに含まれているデバッグ情報(ソースコード)を消すことができます。
stripの結果はfileコマンドで確認できます。

$ strip a.out
$ file a.out
a.out ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked (uses shared libs), for GNU/Linux 2.6.18, stripped

そのあとa.out.sをテキストエディタで開いて8049526を検索してみると以下の断片が見れます。

    std::cout << get_backtrace_info() << std::endl;
 8049513:   56                      push   esi
 8049514:   83 ec 34                sub    esp,0x34
 8049517:   89 e0                   mov    eax,esp
 8049519:   8d 4d e8                lea    ecx,[ebp-0x18]
 804951c:   89 08                   mov    DWORD PTR [eax],ecx
 804951e:   89 4d dc                mov    DWORD PTR [ebp-0x24],ecx
 8049521:   e8 8a fc ff ff          call   80491b0 <get_backtrace_info()>
 8049526:   83 ec 04                sub    esp,0x4

さらに80495abを検索すると

    hogehoge();
 80495a3:   83 ec 08                sub    esp,0x8
 80495a6:   e8 65 ff ff ff          call   8049510 <hogehoge()>
}
 80495ab:   83 c4 08                add    esp,0x8

スタックを知ってる人なら既に分かると思います、[]の中の数値は関数の戻し先のアドレスになります。
これを調べるとget_backtrace_infoを呼び出す時に辿ったコードをすべて特定できます。

エラーコードとその説明の取得

CとC++では多くの関数は失敗が発生した時にエラーコードをどこかに保存したあと失敗を示す返り値(-1など)を返します。
そのため返り値だけではエラーの原因を特定できません。
以下のコードはエラーコードとその説明を取得できます
上記と同じ動作環境にてテスト済み。

last_error.cpp
#include <iostream>
#include <sstream>
#include <unistd.h>
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
    #define __posix__
    #include <execinfo.h>
#elif defined(__MINGW32__)
    #include <windows.h>
#endif

/// クロスプラットフォームエラー処理関数
#if defined(__posix__)
    int my_errno() { return errno; }
    void my_errno(int errnum) { errno = errnum; }
#elif defined(__MINGW32__)
    int my_errno() { return ::GetLastError(); }
    void my_errno(int errnum) { ::SetLastError(errnum); }
#endif // defined(__posix__)

/// エラーコードの説明文字列を取得します
std::string get_errno_explanation(int errnum) {
    const static size_t buffer_size = 1024;
    char buffer[buffer_size + 1] = {};
    const char* message = nullptr;
#if defined(__posix__) && defined(__linux__)
    message = strerror_r(errnum, buffer, buffer_size);
#elif defined(__posix__)
    message = (strerror_r(errnum, buffer, buffer_size) == 0) ? buffer : nullptr;
#elif defined(__MINGW32__)
    message = (::FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr,
        errnum, 0, buffer, buffer_size, nullptr) > 0) ? buffer : nullptr;
#else
    message = "get_errno_explanation error: unknow platform";
#endif
    std::string result;
    result.reserve(buffer_size + 20);
    if (message)
        result.append(message);
    result.append(" [errno ");
    result.append(std::to_string(errnum));
    result.append("]");
    return result;
}

int main() {
    if (::chdir("(´・ω・`)") != 0) {
        int errnum = my_errno();
        std::cout << get_errno_explanation(errnum) << std::endl;
    }
    return 0;
}

実行結果(Linux/OSX)
No such file or directory [errno 2]

実行結果(Windows)

The system cannot find the path specified.
 [errno 3]

ちなみに、Windowsでもerrnoを使えますが一部の関数(例えばWSA...)はerrnoを設定してくれません、
そのためできればGetLastErrorを使うべきです。
またWSAGetLastErrorという関数もありますが、GetLastErrorとは同じ関数です。

使われた関数のmanページ一覧

http://linux.die.net/man/3/backtrace
http://linux.die.net/man/3/backtrace_symbols
http://linux.die.net/man/3/errno
http://linux.die.net/man/3/strerror_r
https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/intro.2.html
https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/strerror_r.3.html
(CaptureStackBackTrace) http://msdn.microsoft.com/en-us/library/windows/desktop/bb204633(v=vs.85).aspx
(GetLastError) http://msdn.microsoft.com/en-us/library/windows/desktop/ms679360(v=vs.85).aspx
(SetLastError) http://msdn.microsoft.com/en-us/library/windows/desktop/ms680627(v=vs.85).aspx
(FormatMessage) http://msdn.microsoft.com/en-us/library/windows/desktop/ms679351(v=vs.85).aspx

29
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
26