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
にてテスト済み。
#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など)を返します。
そのため返り値だけではエラーの原因を特定できません。
以下のコードはエラーコードとその説明を取得できます
上記と同じ動作環境にてテスト済み。
#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