C ++またはサイクリングのスタックトレース、クイックコードレベル

免責事項


この記事はコミックですが、いくつかの真実があります(ただし、プログラミング)。 この記事には、視力に致命的な損害を与える可能性のあるコードも含まれています。 ご自身の責任で読んでください。

エントリー


こんにちは。 多くの人が、プログラムでクラッシュする最も重大なエラーの情報不足に直面したと思います。 プログラムのクラッシュを引き起こす可能性のある状況をリストしてみましょう。

例外

例外-これは、プログラムで発生する例外的な状況を処理するための非常に強力なシステムです。 ただし、例外が処理されなかった場合は、std :: terminateを介してプログラムをドロップします。 したがって、適切に作成されたプログラムでは、処理されていない例外は多くの場合、プログラムのバグを修正する必要があることを意味します。

プログラムのクラッシュ時にwhat()例外メソッドがstderrに自動的に表示されるため、このタイプのエラーは最も有益です。

アサート

関数の正しい使用を確認するための切り替え可能な方法。 無効化は、ナノ秒ごとにカウントされる機能の生産性を向上させるツールとして提供されます。 プログラムがアサートに陥った場合、プログラマはモジュールのインターフェースを使用するときにどこかで台無しになります。 しかし、彼が単に重要な値を予測しなかった可能性が完全にあり、このため、アサート条件を終了しました。

このタイプのエラーは最も有益なものではありませんが、エラーが発生すると、違反した状態が表示されます。

シグセグフ

あなたは、あなたの分野の専門家として、nullポインターを逆参照し、喜びを持ってその中に値を書き込みました。 プログラムは特に落ち込みに抵抗していません。

このような低下はメッセージを伴わず、おそらく最も情報価値がなく、提示されますが、いずれにしても除外することはできません。

情報の内容に関係なく、あらゆる種類のエラーは、実際にどのような理由で発生したかを判断するのに役立ちません。 この記事の一部として、エラーをキャッチしながら少なくともなんらかのスタックトレースを取得できたことを示します。

見回す


まず、一般的な関数呼び出しを追跡する方法を理解する必要があります。 グーグルは非常に残念な結果をもたらしました。 明らかに、クロスプラットフォームのソリューションはありません。 LinuxおよびMac OSの場合、呼び出しスタックのリンクリストを取得できるexecinfo.hヘッダーファイルがあります。 Windowsには、WinAPI CaptureStackBackTrace関数があり、スタックに沿って歩き、フレームから呼び出しを受信できます。 ただし、C ++を使用します。 プラットフォーム依存の関数は使用しません。

データは通常のスタックに保存されます。 関数をプッシュおよびプッシュするには、関数呼び出し中に作成されるオブジェクトを使用します。 このアプローチの利点は、例外がスローされても、このオブジェクトが削除されることです。

そして、どのような特定のデータが必要ですか? もちろん、美しさのために、ファイル、文字列、関数名を用意しておくといいでしょう。 また、この関数に引数を持たせて、オーバーロード時に呼び出される関数を指定できるようにすると便利です。

しかし、どのインターフェイスを使用するのでしょうか? 多かれ少なかれ美しいコードを書くと同時に必要な機能を取得する方法。
私が見つけた唯一の解決策はマクロでした(おそらくテンプレートを介して何らかの方法で実装することも可能ですが、テンプレートには非常に表面的に精通しているため、できる限り行います)。

実装


最初に、スタックを操作するために使用されるシングルトンを実装します。 ユーザーインターフェイスとして、スタックトレースの文字列表現を取得するメソッドのみを実装します。

class StackTracer { friend class CallHolder; public: static StackTracer& i() { static StackTracer s; return s; } std::string getStackTrace() const { std::stringstream ss; for (auto iterator = m_data.begin(), end = m_data.end(); iterator != end; ++iterator) ss << iterator->file << ':' << iterator->line << " -> " << iterator->name << std::endl; return ss.str(); } private: void push(const std::string &name, const char *file, int line) { m_data.push_front({name, file, line}); } void pop() { m_data.pop_front(); } struct CallData { std::string name; const char *file; int line; }; StackTracer() : m_data() {} std::list<CallData> m_data; }; 

出力用のすべての要素を取得するには、コンテナ全体をコピーする必要があるため、std :: stackを使用する方法はありません。

このクラスの問題の1つは、完全なストリーミングの不安定性です。 ただし、これについては後で対処しますが、現在はPoCです。

ここで、関数呼び出しを登録および削除するクラスを実装します。

 class CallHolder { public: CallHolder(const std::string &name, const char *file, int line) { StackTracer::i().push(name, file, line); } ~CallHolder() { StackTracer::i().pop(); } }; 

かなり重要なコードですよね? 繰り返しますが、この「レジストラ」はマルチスレッドを考慮していません。

次に、このようなフランケンシュタインの操作性をテストするための小さな例を試してみましょう。

 void func1(); void func2() { CallHolder __f("func2()", __FILE__, __LINE__); func1(); } void func1() { CallHolder __f("func1()", __FILE__, __LINE__); static int i = 1; if (i-- == 1) func2(); else std::cout << StackTracer::i().getStackTrace() << std::endl; } int main() { func1(); return 0; } 

結果:


図3.1-「生きている!!」

いいね! しかし、何らかの方法でCallHolder呼び出しをパッケージ化する必要があります。そうしないと、ハンドルを呼び出してメソッド名を2回登録することが判明します。

関数とメソッドの実装では、このマクロは次のようになりました。

 #define MEM_IMPL(func_name, args)\ func_name args\ {\ CallHolder __f("" #func_name #args "", __FILE__, __LINE__); 

これでフランケンシュタインを修正して、このようなものを取得できます。 もう「通常の」コードに似ています:

 void func1(); void MEM_IMPL(func2, ()) func1(); } void MEM_IMPL(func1, ()) static int i = 1; if (i-- == 1) func2(); else std::cout << StackTracer::i().getStackTrace() << std::endl; } int main() { func1(); return 0; } 

実行結果は以前とまったく同じです。 しかし、このアプローチには明らかな問題があります。 マクロが隠す開始中括弧は失われます。 これにより、コードが読みにくくなります。 タイトルバーに開き括弧があるイデオロギーに固執する人は、これを強いマイナスとは見なしません。 マイナスの強みは、私が使用している開発環境では、このような危険なケースの処理方法がわからず、マクロ以外の波括弧のみを考慮していることです。

しかし、私たちはバッカナリアから気を取られました。 クラスがある場合はどうなりますか? 実装がクラス外にある場合、何もありません。 例:

 void func1(); void MEM_IMPL(func2, ()) func1(); } void MEM_IMPL(func1, ()) static int i = 1; if (i-- == 1) func2(); else std::cout << StackTracer::i().getStackTrace() << std::endl; } class EpicClass { public: void someFunc(); }; void MEM_IMPL(EpicClass::someFunc, ()) func1(); } int main() { EpicClass a; a.someFunc(); return 0; } 

結果:


図3.2-クラスの結論

しかし、クラス宣言に実装を直接記述するとどうなりますか? 次に、別のマクロが必要です。

 #define CLASS_IMPL(class_name, func_name, args)\ func_name args\ {\ CallHolder __f("" #class_name "::" #func_name "", __FILE__, __LINE__); 

しかし、このアプローチには問題があります。 クラス名を個別に示す必要がありますが、あまり良くありません。 これは、C ++ 11を使用する場合にジャンプできます。 スタックオーバーフローで見つかったソリューションを使用しています。 これはtype_name <decltype(i)>()です。 type_nameは

 #include <type_traits> #include <typeinfo> #ifndef _MSC_VER # include <cxxabi.h> #endif #include <memory> #include <string> #include <cstdlib> template <class T> std::string type_name() { typedef typename std::remove_reference<T>::type TR; std::unique_ptr<char, void(*)(void*)> own ( #ifndef _MSC_VER abi::__cxa_demangle(typeid(TR).name(), nullptr, nullptr, nullptr), #else nullptr, #endif std::free ); std::string r = own != nullptr ? own.get() : typeid(TR).name(); // if (std::is_const<TR>::value) // r += " const"; // if (std::is_volatile<TR>::value) // r += " volatile"; // if (std::is_lvalue_reference<T>::value) // r += "&"; // else if (std::is_rvalue_reference<T>::value) // r += "&&"; return r; } 

修飾子のある部分は、処理結果(* this)の最後にリンク記号(アンパサンド(&))があるため、コメント化されています。

cなマクロは次のようになります。

 #define CLASS_IMPL(func_name, args)\ func_name args\ {\ CallHolder __f(type_name<decltype(*this)>() + "::" + #func_name + #args, __FILE__, __LINE__); 

フランを編集して結果を確認します。

 void func1(); void MEM_IMPL(func2, ()) func1(); } void MEM_IMPL(func1, ()) static int i = 1; if (i-- == 1) func2(); else std::cout << StackTracer::i().getStackTrace() << std::endl; } class EpicClass { public: void someFunc(); void CLASS_IMPL(insideFunc, ()) func1(); } }; void MEM_IMPL(EpicClass::someFunc, ()) func1(); } int main() { EpicClass a; // a.someFunc(); a.insideFunc(); return 0; } 

結果:


図3.3-内部で宣言されたクラスメソッド

さて、しかし、情報の内容はどうですか? 秋に少なくともいくつかの有用な情報を取得するにはどうすればよいですか。 結局、今、同じセグフォールトが発生すると、すべてが単純に落ちます。 まず、エラーをキャッチするint mainを実装します。 ヘッダーで、次を宣言します。

 int safe_main(int argc, char *argv[]); 

cppでは、safe_mainを既に呼び出す「安全な」メインを実装します。

 void signal_handler(int signum) { std::cerr << "Death signal has been taken. Stack trace:" << std::endl << StackTracer::i().getStackTrace() << std::endl; signal(signum, SIG_DFL); exit(3); } int MEM_IMPL(main, (int argc, char * argv[])) signal(SIGSEGV, signal_handler); signal(SIGTERM, signal_handler); signal(SIGABRT, signal_handler); return safe_main(argc, argv); } 

説明する価値があると思います。 シグナル関数を使用して、SIGSEGV、SIGTERM、およびSIGABRTシグナルが表示されたときに呼び出されるハンドラーをセットアップします。 これは、stderrスタックトレースに既に表示されます。 (アサートには後者が必要です)。

SIGSEGVプログラムを壊してみましょう。 「テストベンチ」を再度変更しましょう。

 void func1(); void MEM_IMPL(func2, ()) func1(); } void MEM_IMPL(func1, ()) static int i = 1; if (i-- == 1) func2(); else { int *i = nullptr; (*i) = 12; } } class EpicClass { public: void someFunc(); void CLASS_IMPL(insideFunc, ()) func1(); } }; void MEM_IMPL(EpicClass::someFunc, ()) func1(); } int MEM_IMPL(safe_main, (int argc, char *argv[])) EpicClass a; // a.someFunc(); a.insideFunc(); return 0; } 

結果:


図3.4-作業安全メイン

しかし、例外はどうですか? 実際、例外がスローされた場合、利用可能なすべてのCallHolderが破棄されるだけで、スタックトレースでは徹底的な情報は得られません。 これを行うには、例外がスローされたときにスタックトレースを受け取る独自のTHROWマクロを作成します。

 #define THROW(exception, explanation)\ throw exception(explanation + std::string("\n\rStack trace:\n\r") + StackTracer::i().getStackTrace()); 

「テストベンチ」も少し変更します。

 void func1(); void MEM_IMPL(func2, ()) func1(); } void MEM_IMPL(func1, ()) static int i = 1; if (i-- == 1) func2(); else { // int *i = nullptr; // (*i) = 12; THROW(std::runtime_error, "Some cool error"); } } class EpicClass { public: void someFunc(); void CLASS_IMPL(insideFunc, ()) func1(); } }; void MEM_IMPL(EpicClass::someFunc, ()) func1(); } int MEM_IMPL(safe_main, (int argc, char *argv[])) EpicClass a; // a.someFunc(); a.insideFunc(); return 0; } 

そして結果が得られます:


図3.5-スローは許しません

いいね 完全な基本機能を実現しましたが、マルチスレッドについてはどうですか? 彼女と一緒に何かをしますか?
さて、少なくとも試してみましょう!

まず、StackTracerを編集して、異なるスレッドでの作業を開始します。

 class StackTracer { friend class CallHolder; public: static StackTracer& i() { static StackTracer s; return s; } std::string getStackTrace() const { std::stringstream ss; std::lock_guard<std::mutex> guard(m_readMutex); for (auto mapIterator = m_data.begin(), mapEnd = m_data.end(); mapIterator != mapEnd; ++mapIterator) { ss << "Thread: 0x" << std::hex << mapIterator->first << std::dec << std::endl; for (auto listIterator = mapIterator->second.begin(), listEnd = mapIterator->second.end(); listIterator != listEnd; ++listIterator) ss << listIterator->file << ':' << listIterator->line << " -> " << listIterator->name << std::endl; ss << std::endl; } return ss.str(); } private: void push(const std::string &name, const char *file, int line, std::thread::id thread_id) { m_data[thread_id].push_front({name, file, line}); } void pop(std::thread::id thread_id) { m_data[thread_id].pop_front(); } struct CallData { std::string name; const char *file; int line; }; StackTracer() : m_data() {} mutable std::mutex m_readMutex; std::map<std::thread::id, std::list<CallData> > m_data; }; 

同様に、thread_idが渡されるようにCallHolderを変更します。

 class CallHolder { public: CallHolder(const std::string &name, const char *file, int line, std::thread::id thread_id) { StackTracer::i().push(name, file, line, thread_id); m_id = thread_id; } ~CallHolder() { StackTracer::i().pop(m_id); } private: std::thread::id m_id; }; 

さて、マクロを少し変更しましょう。

 #define CLASS_IMPL(func_name, args)\ func_name args\ {\ CallHolder __f(type_name<decltype(*this)>() + "::" + #func_name + #args, __FILE__, __LINE__, std::this_thread::get_id()); #define MEM_IMPL(func_name, args)\ func_name args\ {\ CallHolder __f("" #func_name #args "", __FILE__, __LINE__, std::this_thread::get_id()); 


テスト中です。 そのような「スタンド」を準備しましょう。
 void MEM_IMPL(sleepy, ()) std::this_thread::sleep_for(std::chrono::seconds(3)); THROW(std::runtime_error, "Thread exception"); } void MEM_IMPL(thread_func, ()) sleepy(); } int MEM_IMPL(safe_main, (int argc, char *argv[])) std::thread th(&thread_func); th.detach(); std::this_thread::sleep_for(std::chrono::seconds(20)); return 0; } 

そして、実行してみてください:


図3.6-モスクワ時間1:10に死亡

そこで、マルチスレッドスタックトレースを取得しました。 実験は終わり、被験者は死んでいます。 この実装の明らかな問題のうち

おわりに


残念ながら、コンパイラを真剣にサポートしないと、デバッグスタックトレースを実装することは非常に難しく、松葉杖に頼らなければなりません。 とにかく、この記事を読んでくれてありがとう。

Source: https://habr.com/ru/post/J302170/


All Articles