例外のポータブルC ++スタックトレース
-
03-07-2019 - |
質問
ポータブルにしたいライブラリを書いています。したがって、glibcやMicrosoftの拡張機能、または標準に含まれていないその他のものに依存するべきではありません。ロジックと入力のエラーを処理するために使用するstd :: exceptionから派生したクラスの素晴らしい階層があります。特定の種類の例外が特定のファイルおよび行番号でスローされたことを知ることは有用ですが、実行がどのように行われたかを知ることは潜在的にはるかに価値があるため、スタックトレースを取得する方法を検討しました。
execinfo.hの関数を使用してglibcに対してビルドする場合、このデータが利用可能であることを認識しています(質問76822 )およびMicrosoftのC ++実装のStackWalkインターフェイス(質問126450 )ですが、移植性のないものは避けたいと思います。
この形式でこの機能を自分で実装することを考えていました:
class myException : public std::exception
{
public:
...
void AddCall( std::string s )
{ m_vCallStack.push_back( s ); }
std::string ToStr() const
{
std::string l_sRet = "";
...
l_sRet += "Call stack:\n";
for( int i = 0; i < m_vCallStack.size(); i++ )
l_sRet += " " + m_vCallStack[i] + "\n";
...
return l_sRet;
}
private:
...
std::vector< std::string > m_vCallStack;
};
ret_type some_function( param_1, param_2, param_3 )
{
try
{
...
}
catch( myException e )
{
e.AddCall( "some_function( " + param_1 + ", " + param_2 + ", " + param_3 + " )" );
throw e;
}
}
int main( int argc, char * argv[] )
{
try
{
...
}
catch ( myException e )
{
std::cerr << "Caught exception: \n" << e.ToStr();
return 1;
}
return 0;
}
これはひどい考えですか?すべての関数にtry / catchブロックを追加することは多くの作業を意味しますが、私はそれに耐えることができます。例外の原因がメモリの破損またはメモリの不足である場合は機能しませんが、その時点ではとにかくほとんど混乱しています。スタック内の一部の関数が例外をキャッチせず、リストに追加し、再スローしない場合、誤解を招く情報を提供する可能性がありますが、少なくともすべてのライブラリ関数がそうすることを保証できます。 「本物」とは異なり、スタックトレース関数の呼び出しで行番号を取得しませんが、少なくとも何かがあるでしょう。
私の最大の懸念は、実際に例外がスローされない場合でも、これによりスローダウンが発生する可能性があることです。これらのtry / catchブロックはすべて、各関数呼び出しで追加のセットアップとティアダウンを必要としますか、それともコンパイル時に何らかの方法で処理されますか?または、考慮していない他の問題がありますか?
解決
これは本当に悪い考えだと思います。
ポータビリティは非常に価値のある目標ですが、邪魔にならず、パフォーマンスが低下し、実装が劣るソリューションになる場合はそうではありません。
すべてのプラットフォーム(Windows / Linux / PS2 / iPhone /など)で作業したことがあるのは、例外が発生したときにスタックをたどり、アドレスを関数名に一致させる方法を提供することです。はい、これらはいずれも移植性がありませんが、レポートフレームワークは移植可能であり、通常、プラットフォーム固有のバージョンのスタックウォーキングコードを記述するのに1〜2日もかかりません。
クロスプラットフォームソリューションの作成/保守にかかる時間よりも短いだけでなく、結果がはるかに優れています。
- 関数を変更する必要はありません
- 標準またはサードパーティのライブラリでトラップがクラッシュする
- すべての機能でtry / catchが不要(低速でメモリ集約型)
他のヒント
ネストされた診断コンテキスト
を1回検索します。ここにちょっとしたヒントがあります:
class NDC {
public:
static NDC* getContextForCurrentThread();
int addEntry(char const* file, unsigned lineNo);
void removeEntry(int key);
void dump(std::ostream& os);
void clear();
};
class Scope {
public:
Scope(char const *file, unsigned lineNo) {
NDC *ctx = NDC::getContextForCurrentThread();
myKey = ctx->addEntry(file,lineNo);
}
~Scope() {
if (!std::uncaught_exception()) {
NDC *ctx = NDC::getContextForCurrentThread();
ctx->removeEntry(myKey);
}
}
private:
int myKey;
};
#define DECLARE_NDC() Scope s__(__FILE__,__LINE__)
void f() {
DECLARE_NDC(); // always declare the scope
// only use try/catch when you want to handle an exception
// and dump the stack
try {
// do stuff in here
} catch (...) {
NDC* ctx = NDC::getContextForCurrentThread();
ctx->dump(std::cerr);
ctx->clear();
}
}
オーバーヘッドは、NDCの実装にあります。私は遅延評価バージョンと同様に一定数のエントリのみを保持したバージョンで遊んでいました。重要な点は、コンストラクタとデストラクタを使用してスタックを処理する場合、これらの厄介な try
/ catch
ブロックと明示的な操作をすべて必要としないようにすることです。
プラットフォーム固有の唯一の頭痛の種は、 getContextForCurrentThread()
メソッドです。スレッドローカルストレージを使用するプラットフォーム固有の実装を使用して、すべてではないにしてもほとんどの場合にジョブを処理できます。
パフォーマンスを重視し、ログファイルの世界に住んでいる場合は、スコープを変更して、ファイル名と行番号へのポインターを保持し、NDCを完全に省略します。
class Scope {
public:
Scope(char const* f, unsigned l): fileName(f), lineNo(l) {}
~Scope() {
if (std::uncaught_exception()) {
log_error("%s(%u): stack unwind due to exception\n",
fileName, lineNo);
}
}
private:
char const* fileName;
unsigned lineNo;
};
これにより、例外がスローされたときにログファイルに素敵なスタックトレースが表示されます。実際のスタックウォーキングは必要ありません。例外がスローされたときの小さなログメッセージだけです;)
「プラットフォームに依存しない」とは思わないこれを行う方法-結局のところ、もしあったとしても、StackWalkやあなたが言及した特別なgccスタックトレース機能は必要ありません。
少々面倒ですが、これを実装する方法は、スタックトレースにアクセスするための一貫したインターフェイスを提供するクラスを作成し、適切なプラットフォーム固有のメソッドを使用する#ifdefsを実装に含めることです実際にスタックトレースをまとめます。
その方法は、クラスの使用法がプラットフォームに依存しないため、他のプラットフォームをターゲットにしたい場合はそのクラスだけを変更する必要があります。
デバッガー内:
例外がスローされた場所のスタックトレースを取得するには、std :: exceptionコンストラクターでブレークポイントをstcikします。
したがって、例外が作成されると、デバッガーが停止し、その時点でスタックトレースを確認できます。完璧ではありませんが、ほとんどの場合に機能します。
スタック管理は、非常に迅速に複雑になる単純なものの1つです。特別なライブラリに残しておく方が良いでしょう。 libunwindを試しましたか? Windowsで試したことはありませんが、うまく機能し、見たところポータブルです。
これは遅くなりますが、動作するはずです。
高速でポータブルなスタックトレースを作成する際の問題を理解したことから、スタックの実装はOSとCPUの両方に固有であるため、暗黙的にプラットフォーム固有の問題です。別の方法として、MS / glibc関数を使用し、#ifdefと適切なプリプロセッサ定義(_WIN32など)を使用して、異なるビルドでプラットフォーム固有のソリューションを実装します。
スタックの使用はプラットフォームと実装に大きく依存するため、完全に移植可能な直接的な方法はありません。ただし、プラットフォームおよびコンパイラ固有の実装への移植可能なインターフェイスを構築して、問題を可能な限り特定することができます。私見、これがあなたの最善のアプローチでしょう。
トレーサの実装は、利用可能なプラットフォーム固有のヘルパーライブラリにリンクします。例外が発生した場合にのみ動作し、catchブロックから呼び出した場合にのみ動作します。最小限のAPIは、トレース全体を含む文字列を返すだけです。
コールチェーンにキャッチおよび再スロー処理を挿入するようにコーダーを要求すると、一部のプラットフォームで実行時のコストが大きくなり、将来のメンテナンスコストが大きくなります。
それは、キャッチ/スローメカニズムを使用することを選択した場合、C ++でもCプリプロセッサが利用可能であり、マクロ __ FILE __
および __ LINE __ <であることを忘れないでください/ code>が定義されています。これらを使用して、トレース情報にソースファイル名と行番号を含めることができます。