* nixのクラッシュレポート:バックトレース、SEGFAULT(およびreinterpret_cast)

こんにちは、habrayuzer様!



すべてのソフトウェア開発者は遅かれ早かれ、ユーザーがプログラムをクラッシュさせるという問題に直面します。 しかし、何かがうまくいかない特定のコンピューターに誰もがアクセスできるわけではなく、そこでgdbを実行してクラッシュを繰り返します。 また、ユーザーから情報を取得することさえ非常に困難な場合があります。「プログラムがクラッシュします。どうすればよいですか?」というメッセージがバグトラッカー(またはテクニカルサポート)に届きますが、ユーザーは開発者にとって非常に重要な技術情報をメッセージに添付しません。 はい、誰もがそれについて書くわけではありません! プログラムの使用をやめるだけです-それだけです。



一部のオペレーティングシステムでは、クラッシュレポートを開発者に送信できます。 しかし! OS開発者、あなたではなく、つまり、本当にそれを必要としている人たちではありません! そして、あなた自身のクラッシュレポートが救助に来ます。あなたのプログラムはあなたのサーバーにそれを送るべきです。 しかし、それらを作る方法は? SEGFAULTを適切に処理し、同時に開発者にわかりやすい情報を送信する方法は?



Habréには、クラッシュの処理に特化したArenimからの興味深い記事がすでにありました。 エッセンスを簡単に繰り返します。POSIXシグナルSIGSEGVをキャッチし、それを処理した後、プログラムを終了します。



void catchCrash(int signum) { reportTrouble(); //  - signal(signum, SIG_DFL); //   exit(3); //   } int main() { signal(SIGSEGV, catchCrash); //-- ... --// }
      
      





今では小さな問題です。問題を特定してください。 また、上記の方法はWindowsで動作しますが、通常のバックトレースは* nixでのみ取得できます(実際、Windowsでも取得できますが、このためのデバッグアセンブリを配布する必要がありますが、あまり良くありません)。 そのため、マニュアルを吸ってこれを行います。

 void reportTrouble() { void *callstack[128]; int frames = backtrace(callstack, 128); char **strs=backtrace_symbols(callstack, frames); //      crash_report.txt //         -  , , etc FILE *f = fopen("crash_report.txt", "w"); if (f) { for(int i = 0; i < frames; ++i) { fprintf(f, "%s\n", strs[i]); } fclose(f); } free(strs); system("curl -A \"MyAppCrashReporter\" --form report_file=@\"crash_report.txt\" http://reports.myserver.com"); }
      
      





それだけです、レポートはサーバーに送られました! 必要に応じて、送信する前にユーザーに尋ねることができます-レポーターを送信するかどうか。 もちろん、GUIプログラムでは、これは少し危険です-結局、SEGFAULTの後、グラフィックフレームワークの内部状態(適切な、または裸のX)の妥当性は保証されません。したがって、事前に(たとえば、匿名のレポートを送信します。」 主なことは、ユーザーの個人情報やその他のデータをレポートに含めることではなく、これは不道徳であるだけでなく、法律によって訴追される可能性もあります(もちろん、これに対するユーザーの同意がライセンス契約の最後に小文字で書かれている場合を除きます)。



次に、実際に概説した方法をテストします。 シンプルなクラスとシンプルな追加機能を備えたシンプルなプログラムを作成しましょう。 そして、このコードをドロップしてみてください。 最も単純なことは、クラスへのNULLポインターでメソッドを呼び出すことですが、これはあまりにも原始的であり、ポインターが「空」を指している場合はさらに良いです。 これを達成する方法は? もちろん、愛するreinterpret_cast



を私たち全員に適用してください! そのため、バックトラックをより面白くするために、関数goCrash()



およびcrash(void *)



を作成します。

 int crash(void *obj) { Crasher *crasher = reinterpret_cast<Crasher *>(obj); crasher->doSomething(); return -1; } void goCrash() { const char *str = "Hello, crash!"; const char *str2 = "Hello again, crash!"; char str3[200]; sprintf(str3, "%s\t\t%s\n", str, str2); long long add = rand() % 20000 + 1500234000l; // fire in my leg! crash(reinterpret_cast<void *>(str3 - add)); }
      
      





さて、 Crasher



クラスに特定の不明なアドレスをキャストしているようです。 とても好奇心が強い! クラスを宣言しましょう:



 #define P_DOUBLE_COUNT 10000 class Crasher { public: // c-tor Crasher() { myPrivateString = new char[100]; sprintf(myPrivateString, "%s\n", "that\'s my private string!"); myPrivateInteger = 100; for (int i = 0; i < P_DOUBLE_COUNT; ++i) myPrivateDoubles[i] = i / 100.0; } // func void doSomething() { // here we can (?) crash fprintf(stderr, "%s\n", "That\'sa function!"); doSomethingPrivate(); } private: void doSomethingPrivate() { // crash? oh, no... fprintf(stderr, "%s myPrivateInteger == %d\n", "That\'sa private function!", myPrivateInteger); fprintf(stderr, "myPrivateDoubles[1] == %f\n", myPrivateDoubles[1]); fprintf(stderr, "myPrivateString == %p\n", myPrivateString); // still alive? crash! crash! crash! ((Crasher*)NULL)->doSomething(); } private: char *myPrivateString; int myPrivateInteger; double myPrivateDoubles[P_DOUBLE_COUNT]; };
      
      





doSomethingPrivate()



関数では、nullポインターで呼び出される関数がまだあることに注意してください。 念のため。 突然、未定義のアドレスに対してdoSomething()



を呼び出した後、プログラムはまだ存続しますか?



これで、プログラムをビルドして実行できます。 そして、私たちは何を見ますか? プログラムは正常に機能しましたが、 curl



はサーバーが見つからないことを誓いました。 まあ、これはナンセンスです。クラッシュレポートをすぐに見るために、一時的に彼の呼び出しをcat crash_report.txt



置き換えることができます。 それでは、他に何が見えますか?



そして、 "That's a function!"



行が"That's a function!"



doSomething()



メソッドから派生! 面白いですね。 ポインターは空を指しますが、メソッドは機能しますか? まあ、そんなに好きじゃない。



結局、プログラムは(おそらく) doSomethingPrivate()



呼び出しでクラッシュし、バックトラックはこれを雄弁に報告します:

 0 segfault 0x000000010d0a98c8 _Z13reportTroublev + 40 1 segfault 0x000000010d0a99d0 _Z10catchCrashi + 16 2 libsystem_c.dylib 0x00007fff99b5dcfa _sigtramp + 26 3 ??? 0x00007fff00000000 0x0 + 140733193388032 4 segfault 0x000000010d0a9c67 _ZN7Crasher11doSomethingEv + 71 5 segfault 0x000000010d0a9880 _Z5crashPv + 32 6 segfault 0x000000010d0a9ac7 _Z7goCrashv + 199 7 segfault 0x000000010d0a9b33 main + 67 8 segfault 0x000000010d0a9854 start + 52
      
      





最初に実験してみましょうcrash()



呼び出すときにアドレスシフトを追加しないでください。プログラムは何を出力しますか? どこでクラッシュしますか? エヘム!

 That's a function! That's a private function! myPrivateInteger == 1752392050 myPrivateDoubles[1] == 60993401604041306737928347282702617388988841504491171140800281285302442927306116721201046092641903128620672849302937378251940003901836219046866981678295779355600933772275817062376375849852470059862498765690530537583237171035779906888043337758015488.000000 myPrivateString == 0x63202c6f6c6c6548 That's a function! 0 segfault 0x0000000109a5e8c8 _Z13reportTroublev + 40 1 segfault 0x0000000109a5e9d0 _Z10catchCrashi + 16 2 libsystem_c.dylib 0x00007fff99b5dcfa _sigtramp + 26 3 ??? 0x0000040000000000 0x0 + 4398046511104 4 segfault 0x0000000109a5ec67 _ZN7Crasher11doSomethingEv + 71 5 segfault 0x0000000109a5ec1a _ZN7Crasher18doSomethingPrivateEv + 208 6 segfault 0x0000000109a5ec67 _ZN7Crasher11doSomethingEv + 71 7 segfault 0x0000000109a5e880 _Z5crashPv + 32 8 segfault 0x0000000109a5eac4 _Z7goCrashv + 196 9 segfault 0x0000000109a5eb33 main + 67 10 segfault 0x0000000109a5e854 start + 52
      
      





doSomethingPrivate()



2番目の呼び出しがdoSomethingPrivate()



し、最初の呼び出しがdoSomethingPrivate()



doSomethingPrivate()



たことがわかりますが、意図したとおりにdoSomethingPrivate()



ませんでした。



では、なぜ、nullポインターでメソッドを呼び出しても、セグメンテーション違反は2番目の関数でのみ発生するのでしょうか? それらはどのように違いますか? 経験豊富なプラスは長い間推測しておりこの記事を読んでいませんが、残りについては説明します。 クラス変数の使用方法が異なります! 変数が使用されていない場合、隠されたthis



パラメーターは使用されないため、関数がどのポインターで呼び出されるかは重要ではありません。つまり、ガベージがあります。 2番目の例(シフトなし)では、プライベート関数が呼び出され、 this



'が文字列を指し、クラス変数がこの文字列の一部を指し、それに応じてガベージが入力されます。 そして、最初のケースでは、ポインタは、ほとんどの場合、プログラムがアクセスできないメモリ領域を参照するだけなので、プライベート関数の最初の呼び出しはすでにペイントされています。



この記事のそのような基本的なものの説明は何ですか? それでは、プログラムをクラッシュさせる方法を示す必要があります! また、無効なポインターでクラスメソッドを呼び出すと、常にクラッシュするわけではない理由を説明します。 完全なコードが興味深い場合は、いつものようにgithubに尋ねます。



一般的に、成功したデバッグ! クラッシュレポートが少なくなります;)



All Articles