子供のころからできるだけ手抜きして成果を挙げることだけは長けている山本です。
今回は、C/C++ で作ったプログラムが運用中にクラッシュするときのデバッグ方法のお話しです。 開発中のデバッグは gdb などでソース追いながらデバッグできますが、運用中ですと [strip][] していたり最適化していたりしてデバッグが難しくなります。 そもそも、いきなりクラッシュすると情報が残らずに困ってしまいます。そんなときどうするか。
久しぶりに社内でプログラミングのレクチャーをすることにしたのだけれども、準備をしていて自分がどうやってプログラマになったかをふと思い返してしまった。結論を書くとあっさり一言で済んでしまうのだけれども、やはり苦労話(?)もないと面白くないのでぐだぐだ書いてみる。
◆コンピューターとの出会い
小学三年生くらいの頃、読んでいた雑誌(確か子供の科学)にマイコンのことがのっていて、四則演算とか表示とかをする簡単な BASIC を見たのが最初です。ゲームウォッチみたいなものを自分で作れるような夢の機械に見えて、それからしばらくは本屋さんでマイコンの本を立ち読みしてました。
当時は英語も読めなかったので、たとえば PRINT のことを「ピー・アール・インテジャー」と読んでいました。INTはインテジャーと読む、と別のところで覚えて、でも PRINT は読み方が分からなかったので。それでプログラミングを覚えられたかというと全くそんなことはなかったです。立ち読みの紹介記事ではさもありなん。
で、親にパソコンをねだり続けたわけですが、確か六年生のころかな、MSX2を買ってもらいました。正確には親のためのものだったんですが、親がキーボードも打てないのを良いことに事実上自分のものにしたのですけど。それでようやく本物のパソコンを触れるようになり、信長の野望(初代は BASIC でかかれていたので、STOP すればソースが読めた)や Wizardry で遊びつつ、マシン語(当時はOS)も覚えようとしたのですが。。
全然だめでした。いや、Z80は読み書きできるようになったんですが、I/Oや割り込みの仕方を調べられなくて、面白くもなんともなかったというか。あの当時他の人はどうやってそういう仕様を学んでいたのか、いまだに不思議に思っていたりします。
◆コンピューターとの別れ
で、ゲーム専用機と化した MSX2 で毎日遊んでいたら、中学2年生の頃に、親にパソコン捨てられました(笑)。その後はコンピューターに触る機会なかったのですが、コンピューターを触る仕事に就くという点だけは譲りませんでした。親は医者にさせたかったようですが、頑として拒否した結果、医者でなければ東京大学に行けという妥協案を出されました。
第一希望は1年目からコンピューターを触れる(と思っていた。入らなかったので実際は不明)東工大の5類だったのですが、東大にはコンピューターを触れそうな専攻がないと思っていたので嫌でした。でも親(のお金)には逆らえず、しぶしぶ調べたところ、理学部情報科学科というところでは触れそうと分かりました。
入学後2年は専門ではなく教養、というのがまた嫌なところだったのですが。
◆再会と挫折
無事入学後も、何とかしてコンピューターに触りたかった私は、駒場の授業で「計算機入門」というコマをとることにしました。でもその授業は座学で、実際のコンピューターにはさらわず、ソートなどのアルゴリズムを紹介するものでした。まあでも、このとき初めて、プログラミングというのはマシン語を並べて I/O するだけじゃないんだ、と知ったわけですが。
その授業で知ったマージソートというアルゴリズムが簡単そうだったので、また別の授業(プログラミング入門だったかな?)で Pascal という言語を習ったときに、試しに実装しようとしてみました。・・・実装できませんでした。
今となってはなぜ当時の自分がこんな簡単なアルゴリズムを実装できなかったか分かりません。でも、事実として、作ったプログラムは動かなかったのです。再帰という概念やデータ構造といった基本的な道具立てがなければ、ごくシンプルなプログラムであっても難しい、のでしょう。
さらに、なぜ動かないのか教えてもらおうと思って計算機センターの相談員の人に見てもらいました。・・・教えてもらえませんでした。今となっては、読むに堪えない代物だったのだろうと思います。が、当時はマージソートというのはとても難しいものなのだな、と理解?したのでした。
とりあえず今日はここまで。
前の記事で例外は投げ捨てみたいなことを書いておいてなんだが、投げ捨てるのはあまりよろしくない。
例外を catch しないと std::terminate() が呼び出されて最後は abort(3) されるのだけれども、この場合は RAII が効かず、スタックオブジェクトのデストラクタが呼び出されない。結果としてゴミが残ったりしてしまうことになる。std::set_terminate() でスタックトレースを出力するハンドラをセットして様子を見てみると以下のような出力が得られるはずだ。
$ ./test Backtrace: ./test(_Z12my_terminatev+0x3b)[0x42249d] /usr/lib/libstdc++.so.6(+0xcad16)[0x7fd612aa4d16] /usr/lib/libstdc++.so.6(+0xcad43)[0x7fd612aa4d43] /usr/lib/libstdc++.so.6(+0xcae3e)[0x7fd612aa4e3e] ./test(_ZN6cybozu4saveERKN4Poco4PathE+0xde)[0x422dd2] ./test(main+0x114)[0x4225f3] /lib/libc.so.6(__libc_start_main+0xfd)[0x7fd6121dbc4d] ./test[0x4223a9]
cybozu::save() あたりから C++ のランタイムに入って、スタックが巻き戻ることなく my_terminate() が呼び出されているのが分かる。例外処理のランタイムはスタックを検査して対応する catch まで巻き戻るといったことをするわけだが、みつからないときは一切スタックを巻き戻さずにそのまま terminate 処理をするわけだ。
であるからして、やはり C++ で catch( ... ) するイディオムには意味はあるわけですな。
int main() { try { // do something } catch( const std::exception& e ) { // } catch( ... ) { // } }
最近 120 年ぶりに C++ を書くことにしたので、もだーんとは何かという禅問答を繰り返した記録を公開。
なぜ C ではなく C++ を使うのか。その理由の 99.999% は std::string にある。
char* は忘れるんだ! std::string を使うだけで↓のように幸せになれる!
void f( const std::string& s) { g( "hoge " +s); } |
C ならこんな感じになるところだ。
void f( const char * s) { char buf[256]; sprintf (buf, "hoge %s" , s); g(buf); } |
STL は機能リッチとはとても言えないので、ちょっとなにかやりたいことがあるとライブラリをまず探すという面倒なことになる。まあ主要なところをいくつかまとめておく。
用途 | 名称 | URL | コメント |
---|---|---|---|
広範 | Boost | http://www.boost.org/ | 事実上標準的な地位にある。機能はとても豊富。でもテンプレートメタプログラミングを駆使しているのでコンパイルがとても遅い。私は好きじゃない。 |
広範 | POCO | http://pocoproject.org/ | MySQL ライブラリ等を備えていて boost よりも実用的アプリに向く。ネットワークアプリを作るのにとても便利。激しくお勧め。 |
マルチスレッド | TinyThread++ | http://tinythread.sourceforge.net/ | ミニマリスト。生スレッドの 100 倍はマシ。 |
JSON | picojson | https://github.com/kazuho/picojson | @kazuho 作。お勧め。 |
void f() { std::auto_ptr<T> t( new T(...)); ... do anything ... // no need for delete t } |
char * buf = &(v[0]); |
class C { C(...): m_object( new T(...)), m_array(SIZE) {} std::auto_ptr<T> m_object; std::vector<T> m_array; }; |
#include <stdexcept> class MyException: public std::runtime_error { ... }; try { throw MyException( "error" ); } catch ( const MyException& e ) { ... } |
std::terminate_handler orig_handler = 0; const int MAX_BACKTRACE = 100; void my_terminate() { std::cerr << "Backtrace:" << std::endl; void * bt[MAX_BACKTRACE]; int bt_num = backtrace(bt, MAX_BACKTRACE); backtrace_symbols_fd(bt, bt_num, 2); std::cerr << std::endl; if ( orig_handler ) orig_handler(); abort (); } int main( int argc, char ** argv) { orig_handler = std::set_terminate(my_terminate); throw std::runtime_error( "Failed to open " +argv[0]); return 0; } |
実行例:
Backtrace: ./tbs[0x402b4f] /usr/lib/libstdc++.so.6(+0xcad16)[0x7f040877cd16] /usr/lib/libstdc++.so.6(+0xcad43)[0x7f040877cd43] /usr/lib/libstdc++.so.6(+0xcae3e)[0x7f040877ce3e] ./tbs[0x40305e] ./tbs[0x402c35] /lib/libc.so.6(__libc_start_main+0xfd)[0x7f0407eb3c4d] ./tbs[0x401c09] terminate called after throwing an instance of 'std::runtime_error' what(): Failed to open /home/ymmt/eieie Abort
namespace { int local = 3; } |
Object o(); |
正しくはこう。
Object o; |
毎年、元旦には一年の計を立てることにしています。
その時に気をつけているのは、
ことです。
まず、一年の計を立てても一年後に忘れてしまっていたら達成のしようがないわけで、覚えやすいのはとても重要です。つぎに、「今年は健康にきをつける」みたいな目標は達成できたかどうか判定ができないので目標として追えません。「今年は昇進する」「今年は体重マイナス3キロ」など達成できたかできないかすぐにわかるものにしないといけません。最後に、色々と欲張って立ててみたくなりますけど人間そんなに器用に立ち回れないので、一年にひとつずつしっかり目標が達成できれば良しとしています。
最初にこの方針で立てた目標は、「今年はマネージャーになる」でした。マネージャーを通り越していきなり開発部長になってしまいどたばたしたのですが、、まあ達成したのかな。去年の目標は「cybozu.comリリース」。これはきっちり達成できました。
今年は家族孝行にしようと思います。「毎月一度、子供を行ったことのない場所に連れていく」。
過去最高に難しそうです。