●C++でスタックトレースを取得する意義
プログラムの開発においてデバッグは避けて通れない道のりです。
特にC++のようなパワフルな言語では、ちょっとしたミスがアプリケーションのクラッシュにつながることも珍しくありません。
そんな時、スタックトレースは強力な武器になります。
○プログラムのクラッシュ原因特定に効果的
スタックトレースとは、プログラムがクラッシュした時点でのスタックの状態を記録したものです。
関数の呼び出し履歴や、各関数の実行位置などの情報が含まれており、クラッシュの原因となったコードを特定するのに非常に役立ちます。
実際に私がC++のプロジェクトで経験した例を紹介しましょう。
あるデータ処理モジュールを開発していた時のことです。
テストを行ったところ、特定の入力データでプログラムがクラッシュしてしまいました。
そこでスタックトレースを取得し、詳しく調べてみると、あるメンバ関数内でヌルポインタ参照が発生していることがわかりました。
そのメンバ関数の引数に渡されるポインタが、ある条件下でNULLになっていたのです。
スタックトレースのおかげで、原因箇所をすぐに特定でき、修正することができました。
○デバッグ効率の大幅な改善が可能
スタックトレースを活用すれば、こんな風にデバッグの効率を大幅に上げることができます。
prinftデバッグなどに頼らず、手がかりを得られるので、問題箇所に早く辿り着けるのです。
特に大規模なプロジェクトでは、デバッグに費やす時間の削減は重要な課題です。
スタックトレースをうまく使いこなせば、開発のスピードアップに繋がるでしょう。
○サンプルコード1:基本的なスタックトレース取得
では、実際にC++でスタックトレースを取得するサンプルコードを見てみましょう。
このコードでは、func1
関数内で例外を投げています。
main
関数では、try-catch
ブロックを使って例外をキャッチし、boost::stacktrace::stacktrace()
でスタックトレースを取得、出力しています。
実行結果
スタックトレースから、例外がfunc1
で発生し、func2
、main
と呼び出し元を辿れることがわかります。
これを手がかりに、func1
のコードを詳しく調べていけば、例外の原因を特定できるというわけです。
●C++標準ライブラリを使ったスタックトレース取得
C++標準ライブラリにもスタックトレースを取得するための機能が用意されています。
例外処理の機能と組み合わせることで、例外発生時のスタックトレースを簡単に取得できます。
ここでは、その方法を詳しく見ていきましょう。
○ヘッダを使用する方法
スタックトレースを取得するには、<exception>
ヘッダをインクルードする必要があります。
このヘッダには、std::exception
クラスやstd::current_exception
、std::rethrow_if
などの例外処理に関連する機能が含まれています。
○サンプルコード2:std::current_exceptionを使用
std::current_exception
は、現在処理中の例外オブジェクトへのポインタを取得するための関数です。
例外がキャッチされた時点で、この関数を呼び出すことでスタックトレースを取得できます。
std::current_exception
で例外オブジェクトのポインタを取得し、std::rethrow_exception
で再スローしています。
これにより、キャッチした例外をそのまま再スローできるので、スタックトレースを取得しつつ、例外処理の流れを変えずに済みます。
実行結果
スタックトレースから、例外がfunc1
で発生したことがわかります。
ただし、標準ライブラリの機能だけでは、詳細なスタックトレース情報は取得できません。
より詳細なスタックトレースが必要な場合は、後述するBoostライブラリなどを使う必要があります。
○サンプルコード3:std::rethrow_ifを使用
std::rethrow_if
は、条件付きで例外を再スローするための関数です。
例えば、特定の型の例外が発生した場合にのみスタックトレースを取得したいといったケースで使用します。
この例では、func1
でstd::runtime_error
またはstd::invalid_argument
の例外が発生する可能性があります。
main
関数のcatch
ブロックでは、std::rethrow_if
を使ってstd::runtime_error
の場合にのみ例外を再スローしています。
実行結果
std::runtime_error
が発生したため、std::rethrow_if
によって例外が再スローされ、スタックトレースが表示されています。
○サンプルコード4:try-catch構文の活用
try-catch
構文を使えば、例外発生時に柔軟な処理を記述できます。
例えば、スタックトレースをログに出力したり、例外の種類に応じて異なる処理を行ったりといったことが可能です。
この例では、std::runtime_error
とstd::invalid_argument
を個別にキャッチし、それぞれ異なるエラーメッセージを出力しています。
また、std::exception
をキャッチすることで、その他の例外についても汎用的な処理を行っています。
実行結果
try-catch
構文を使えば、このようにスタックトレースを取得するだけでなく、例外の種類に応じた柔軟な処理を記述できます。
アプリケーションの要件に合わせて、適切な例外処理を行いましょう。
●Boost/stacktraceライブラリの活用
C++標準ライブラリのスタックトレース取得機能は便利ですが、取得できる情報は限定的でした。
より詳細なスタックトレースを取得したい場合は、Boostライブラリのstacktrace
モジュールを使うのがおすすめです。
Boostは、C++の標準ライブラリを補完する高品質なライブラリ群です。
幅広い機能を提供しており、多くのC++プロジェクトで活用されています。
stacktrace
モジュールは、クロスプラットフォームでスタックトレースを取得するための機能を提供します。
○Boostライブラリの導入方法
Boostライブラリを使うには、まずBoostをインストールする必要があります。
Linuxであれば、パッケージマネージャを使ってインストールできます。
例えばUbuntuなら、下記のコマンドでインストールできます。
Windowsの場合は、Boostの公式サイトからインストーラをダウンロードし、実行します。
インストール先のディレクトリを指定し、必要なライブラリをビルドします。
インストールが完了したら、C++のソースコードからstacktrace
ヘッダをインクルードします。
これで、stacktrace
モジュールの機能が使えるようになります。
○サンプルコード5:basic_stacktrace クラスの使用
stacktrace
モジュールの中心となるのは、boost::stacktrace::basic_stacktrace
クラスです。
このクラスを使って、現在の実行点でのスタックトレースを取得できます。
func1
関数内でbasic_stacktrace
オブジェクトを生成し、std::cout
で出力しています。
実行結果
関数名や行番号を含む詳細なスタックトレースが表示されます。
C++標準ライブラリに比べて、格段に詳細な情報が取得できていることがわかります。
○サンプルコード6:スタックトレース出力のカスタマイズ
stacktrace
モジュールでは、スタックトレースの出力方法をカスタマイズできます。
下記の例では、各行の先頭に任意の文字列を付加しています。
basic_stacktrace::print
メンバ関数の第2引数に、行頭に付加する文字列を指定します。
実行結果
各行の先頭に " > "
が付加されています。
このように、出力形式を自由にカスタマイズできるのもBoostライブラリの利点です。
○サンプルコード7:マルチスレッド対応のスタックトレース
stacktrace
モジュールは、マルチスレッド環境でも安全にスタックトレースを取得できます。
下記の例では、複数のスレッドからスタックトレースを取得しています。
thread_func
関数内でbasic_stacktrace
オブジェクトを生成し、スレッドIDとともに出力しています。
main
関数では、2つのスレッドを作成し、それぞれthread_func
を実行しています。
実行結果
各スレッドから取得したスタックトレースが出力されています。
このように、stacktrace
モジュールを使えば、マルチスレッド環境でも簡単にスタックトレースを取得できます。
Boost/stacktraceライブラリを活用することで、C++標準ライブラリよりも詳細で柔軟なスタックトレース取得が可能になります。
導入に多少の手間はかかりますが、一度使い方を覚えてしまえば、デバッグ効率の大幅な改善が期待できるでしょう。
ただし、Boostライブラリはプラットフォームに依存する部分があるため、クロスプラットフォームな開発では注意が必要です。
●Visual Studioでのスタックトレース取得
WindowsでC++の開発といえば、多くの人がVisual Studioを使っているのではないでしょうか。
Visual Studioには、強力なデバッガが内蔵されており、スタックトレースの取得もとてもわかりやすい仕組みになっています。
VisualStudioでのデバッグ作業に慣れている方なら、直感的にスタックトレースを活用できるはずです。
しかし、初めて触れる方にとっては、設定の方法や使い方が少しわかりにくいかもしれません。
そこで、ここではVisual Studioでのスタックトレース取得方法を、丁寧に解説していきます。
○Visual Studioデバッガの例外設定
Visual Studioでスタックトレースを取得するには、デバッガの例外設定を適切に行う必要があります。
デバッグメニューから「例外設定」を選択し、例外設定ウィンドウを開きましょう。
ここで、C++の例外である「C++ Exceptions」にチェックを入れます。
これにより、C++の例外が発生した時点でデバッガが自動的に割り込み、スタックトレースを表示してくれるようになります。
初期設定では、「ユーザーハンドルなし」にチェックが入っていると思います。
これは、プログラムがキャッチしない例外が発生した場合にのみ、デバッガが割り込むという設定です。
「ユーザーハンドルあり」にチェックを入れると、プログラムがキャッチする例外に対してもデバッガが割り込むようになります。
状況に応じて適切な設定を選びましょう。
例えば、プログラムがキャッチしている例外についてもスタックトレースを確認したい場合は、「ユーザーハンドルあり」を選ぶと良いでしょう。
○サンプルコード8:__try-__exceptによる構造化例外処理
Visual Studioには、独自の構造化例外処理の仕組みがあります。
__try
と__except
を使って、例外処理を記述できます。
この仕組みを使えば、C++標準の例外処理とは別の方法でスタックトレースを取得できます。
このコードでは、func1
関数内でNULLポインタ参照による例外が発生します。
main
関数では、__try
ブロック内でfunc2
を呼び出し、例外が発生した場合は__except
ブロックで処理を行います。
__except
の第1引数には、例外が発生した際に呼び出される関数を指定します。
ここでは、printStackTrace
関数を指定しています。
この関数は、EXCEPTION_POINTERS
構造体へのポインタを受け取り、スタックトレースを出力します。
printStackTrace
関数の中身を見てみましょう。
まず、SymInitialize
関数でデバッグシンボルを初期化します。
次に、STACKFRAME64
構造体を初期化し、例外発生時のレジスタの状態を設定します。
そして、StackWalk64
関数を使ってスタックフレームを順番に取得していきます。
取得したスタックフレームの情報から、SymFromAddr
関数を使って関数名を取得し、出力しています。
最後に、SymCleanup
関数でデバッグシンボルの後処理を行います。
実行結果
スタックトレースから、例外がfunc1
で発生し、func2
、main
の順に呼び出されたことがわかります。
○サンプルコード9:CaptureStackBackTraceを使用
Visual Studio 2005以降では、CaptureStackBackTrace
関数を使ってスタックトレースを取得できます。
この関数は、<windows.h>
ヘッダに定義されています。
func1
関数内で、CaptureStackBackTrace
関数を使ってスタックフレームを取得しています。
第1引数と第2引数で、取得するスタックフレームの範囲を指定します。
ここでは、最大256個のスタックフレームを取得するようにしています。
取得したスタックフレームは、frames
配列に格納されます。
numFrames
変数には、実際に取得されたスタックフレームの数が格納されます。
取得したスタックフレームの情報から、関数名を取得して出力する処理は、サンプルコード8と同様です。
実行結果
func1
、func2
、main
関数のスタックフレームが出力されています。
●Linux環境でのスタックトレース生成
ここまで、C++標準ライブラリやBoost、Visual Studioでのスタックトレース取得方法を見てきました。
しかし、C++の開発環境は多岐に渡ります。
特に、サーバサイドの開発ではLinuxを使うことも珍しくありません。
Linuxでは、デバッグツールとしてgdbが広く使われています。
gdbを使えば、プログラムのクラッシュ時にバックトレース(スタックトレースのこと)を取得できます。
また、バックトレースを独自に生成するためのAPIも用意されています。
ここでは、Linuxでのスタックトレース生成方法を2つ紹介します。1つはgdbを使った方法、もう1つはbacktrace
関数を使った方法です。
どちらもユーザーコードからスタックトレースを取得できるので、状況に応じて使い分けましょう。
○gdbを使ったバックトレース取得
まずは、gdbを使ったバックトレースの取得方法を見ていきましょう。
gdbは、Linuxのデバッグツールとして非常に有名で、多くのディストリビューションにデフォルトでインストールされています。
gdbを使ってバックトレースを取得するには、次の手順を踏みます。
- プログラムをビルドする際に、デバッグ情報を含めるオプション(
-g
)を付けてコンパイルする。 - プログラムをgdbから起動する。クラッシュ時にコアダンプを生成するオプション(
-c
)を付ける。 - プログラムを実行し、クラッシュさせる。
- gdbのプロンプトで
bt
コマンドを実行し、バックトレースを表示する。
例えば、次のようなコードをデバッグしてみましょう。
このコードをgdbで実行すると、次のようになります。
bt
コマンドを実行することで、バックトレースが表示されました。
func1
でクラッシュが発生し、func2
、main
の順に呼び出されたことがわかります。
gdbを使えば、このようにコマンドラインからバックトレースを取得できます。
デバッグビルドした実行ファイルとコアダンプがあれば、クラッシュ時の状況を詳しく調査できるでしょう。
○サンプルコード10:backtrace関数の使用
gdbを使う方法では、プログラムの外からバックトレースを取得しました。
一方、backtrace
関数を使えば、ユーザーコードの中からバックトレースを生成できます。
backtrace
関数は、<execinfo.h>
ヘッダに定義されています。
この関数を使って、現在の関数呼び出し履歴を取得できます。
下記のサンプルコードを見てみましょう。
printBacktrace
関数の中で、backtrace
関数を呼び出しています。
第1引数には、バックトレースを格納する配列を指定します。
第2引数には、配列の最大サイズを指定します。
戻り値として、実際に取得されたバックトレースのフレーム数が返されます。
取得したバックトレースのフレーム情報は、backtrace_symbols
関数を使って文字列に変換できます。
この関数は、フレーム情報を表す文字列の配列を動的に割り当てます。
使い終わったら、free
関数で解放する必要があります。
上記のコードをコンパイル・実行すると、次のような出力が得られます。
printBacktrace
関数が呼び出された位置から、main
関数までのバックトレースが出力されています。関数名と、その関数のアドレスが表示されています。
●スタックトレース取得時の注意点
スタックトレースは、プログラムのデバッグに欠かせないツールです。
しかし、使い方を誤ると、かえって開発効率を下げてしまうこともあります。
ここでは、スタックトレースを取得する際の注意点を3つ挙げておきましょう。
○パフォーマンスへの影響を最小限に
スタックトレースの取得には、一定のオーバーヘッドがつきものです。
特に、例外が頻繁に発生するようなコードでは、パフォーマンスへの影響が無視できなくなります。
例えば、次のようなコードを考えてみましょう。
このコードでは、vec
の要素を順番にfunc
関数に渡しています。
func
関数は、引数が負の値だった場合に例外を投げます。
main
関数では、try-catch
ブロックを使って例外をキャッチし、スタックトレースを出力しています。
しかし、このコードでは、例外が発生するたびにスタックトレースが取得されるため、パフォーマンスが大きく低下します。
実行結果
このように、例外が発生するたびにスタックトレースが取得・出力されてしまいます。
パフォーマンスへの影響を最小限に抑えるには、次のような工夫が必要です。
・例外を投げる代わりに、エラーコードを返すようにする
・スタックトレースの取得は、必要な時だけ行う
・assert
マクロを使って、デバッグビルドでのみスタックトレースを取得する
状況に応じて、適切な方法を選びましょう。
○必要以上に詳細な情報は控える
スタックトレースには、関数名や行番号など、デバッグに必要な情報が含まれています。
しかし、中にはファイルパスなど、公開すべきでない情報も含まれている場合があります。
例えば、次のようなスタックトレースを考えてみましょう。
このスタックトレースには、ソースコードのファイルパスが含まれています。
この情報は、デバッグには役立ちますが、一般のユーザに公開すべきではありません。
必要以上に詳細な情報を含むスタックトレースを出力しないようにするには、次のような工夫が必要です。
・デバッグビルドとリリースビルドを分ける
・リリースビルドでは、ファイルパスなどの詳細情報を除去する
・スタックトレースをログファイルに出力する際は、適切にフィルタリングする
セキュリティにも配慮しつつ、必要な情報だけを出力するようにしましょう。
○シンボル情報の確認と最適化
スタックトレースから有用な情報を得るには、シンボル情報が必要不可欠です。
シンボル情報とは、関数名や行番号などのデバッグ情報のことです。
コンパイル時に最適化オプションを指定すると、シンボル情報が削除されてしまうことがあります。
その場合、スタックトレースには関数名などが表示されず、デバッグが困難になります。
例えば、次のようなスタックトレースを考えてみましょう。
このスタックトレースには、関数名が表示されていません。
代わりに、関数のアドレスが表示されています。
このようなスタックトレースでは、どの関数でクラッシュが発生したのかを特定するのが難しくなります。
シンボル情報を確認し、最適化オプションを適切に設定するには、次のような工夫が必要です。
・-g
オプションを指定して、デバッグ情報を含めてコンパイルする
・-O0
オプションを指定して、最適化を無効にする
・リリースビルドでは、-g
オプションを外し、適切な最適化オプションを指定する
・strip
コマンドを使って、実行ファイルからシンボル情報を削除する
デバッグビルドとリリースビルドを適切に使い分け、シンボル情報を管理しましょう。
スタックトレース取得時の注意点を3つ挙げました。
パフォーマンスへの影響を最小限に抑え、必要以上に詳細な情報は控え、シンボル情報を確認・最適化することが大切です。
●スタックトレースとC++の例外処理
スタックトレースと例外処理は、C++プログラミングにおいて切っても切れない関係にあります。
例外が発生した時、スタックトレースを使えば、その原因を素早く特定できます。
逆に、例外処理の仕組みを理解していれば、スタックトレースをより効果的に活用できるでしょう。
ここでは、C++の例外処理とスタックトレースの関係について、詳しく見ていきます。
try-catch
構文との連携や、std::exception
派生クラスの活用、カスタム例外クラスでのスタックトレース埋め込みなど、実践的な内容を取り上げていきますので、ぜひ最後までお付き合いください。
○try-catch構文との連携
C++の例外処理は、try-catch
構文を使って行います。
try
ブロック内で例外が発生した場合、処理はただちに中断され、対応するcatch
ブロックに移ります。
スタックトレースを取得するタイミングは、通常、このcatch
ブロック内になります。
下記のサンプルコードを見てみましょう。
main
関数のtry
ブロック内でfunc2
を呼び出しています。
func2
はfunc1
を呼び出し、func1
内で例外が発生します。
この例外は、main
関数のcatch
ブロックでキャッチされます。
catch
ブロック内では、例外オブジェクトe
を使ってエラーメッセージを出力し、boost::stacktrace::stacktrace()
でスタックトレースを取得・出力しています。
実行結果
このように、try-catch
構文とスタックトレースを組み合わせることで、例外発生時の状況を詳しく把握できます。
デバッグ作業の効率化につながるでしょう。
○std::exception派生クラスの活用
C++標準ライブラリには、std::exception
から派生した様々な例外クラスが用意されています。
これらの例外クラスを活用することで、例外の種類に応じた適切な処理を行えます。
下記のサンプルコードを見てみましょう。
func
関数では、引数の値に応じて異なる種類の例外を投げています。
main
関数では、それぞれの例外クラスに対応するcatch
ブロックを用意しています。
例えば、std::out_of_range
例外が発生した場合は、最初のcatch
ブロックで処理されます。
std::invalid_argument
例外なら2番目のcatch
ブロック、それ以外のstd::exception
派生例外なら3番目のcatch
ブロックで処理されます。
実行結果
std::out_of_range
例外がキャッチされ、対応するエラーメッセージとスタックトレースが出力されています。
このように、std::exception
派生クラスを活用することで、例外の種類に応じた柔軟な処理が可能になります。
状況に合わせて適切な例外クラスを選択し、スタックトレースと組み合わせて使いましょう。
○カスタム例外クラスでのスタックトレース埋め込み
標準ライブラリの例外クラスだけでは不十分な場合、自分でカスタム例外クラスを定義することもできます。
その際、スタックトレースを例外オブジェクトに埋め込んでおくと便利です。
下記のサンプルコードを見てみましょう。
MyException
クラスは、std::exception
から派生したカスタム例外クラスです。
コンストラクタでエラーメッセージを受け取り、boost::stacktrace::stacktrace
オブジェクトを生成して保持します。
what()
メンバ関数は、std::exception
からの仮想関数をオーバーライドしたものです。
保持しているエラーメッセージを返します。
stacktrace()
メンバ関数は、保持しているスタックトレースオブジェクトへの参照を返します。
func1
関数内でMyException
をスローし、それをmain
関数のcatch
ブロックでキャッチしています。
what()
で例外メッセージを、stacktrace()
でスタックトレースを取得し、出力しています。
実行結果
MyException
がキャッチされ、例外メッセージとスタックトレースが出力されています。
スタックトレースには、MyException
のコンストラクタ呼び出しも含まれていることに注目してください。
このように、カスタム例外クラスにスタックトレースを埋め込んでおけば、例外が発生した状況を詳しく知ることができます。
必要に応じて、この手法を活用してみてください。
スタックトレースとC++の例外処理について、try-catch
構文との連携、std::exception
派生クラスの活用、カスタム例外クラスでのスタックトレース埋め込みの3点を取り上げました。
例外処理とスタックトレースは、C++プログラミングに欠かせない要素です。
両者を適切に組み合わせることで、デバッグ作業の効率化や、コードの質の向上につなげられるでしょう。
次のようなことを心がけると良いでしょう。
- 例外が発生しそうな箇所は、
try
ブロックで囲む - 例外をキャッチしたら、スタックトレースを取得・出力する
- 例外の種類に応じて、適切な例外クラスを使い分ける
- カスタム例外クラスを定義する際は、スタックトレースを埋め込むことを検討する
これらを実践していけば、C++のデバッグ作業が格段にスムーズになるはずです。
本記事で紹介した内容を、ぜひ実際のプロジェクトで活用してみてください。
まとめ
C++でスタックトレースを取得する方法について、詳しく解説してきました。
スタックトレースは、プログラムのクラッシュ時に原因を特定するための強力なツールです。
C++標準ライブラリやBoost、Visual Studio、Linuxなど、様々な環境でスタックトレースを取得する方法を紹介しました。
本記事で紹介した内容は、一朝一夕には身につかないかもしれません。
しかし、地道に実践を重ねていけば、必ずや自分のものにできるはずです。