組込み開発の現場での C と品質と生産性の話をセグメンテーションフォールトの観点で

C で書かれた手作りの自然言語処理のアプリケーションをソースコードごと頂き、使わせて頂いていたのですが時々セグメンテーションフォールトで止まります。開発者に心当たりがないか訪ねたところいろいろあるようなのですが gdb で調べろとのこと。コードを頂いている以上はもっともな話なのでちょっと見てみます。ちょと手順をおさらい。
1) そのままでは core ダンプしてくれないので
ulimit -c unlimit
で、フォールト時に core を吐いてもらうようにします。で、アプリケーションを実行して、セグメンテーションフォールとが起きて core を吐いているのを確認します。20年程前は宴会でお酒飲みすぎて吐いたりすると 「core 吐いてる」とか良くいったものなのですが、最近の Linux は core を吐かない設定がデフォルトなせいか、あまり聞かなくなりました。
2) gdb 実行ファイル coreファイル
3) bt でトレース、あれれ、fwrite の中で落ちてる??なにを喰わせたんだろ?
4) frame フレーム番号で呼び出し元を見る、ありり?ただの文字列書いてるだけなんだけど?
5) p 変数名 で、ファイルポインタをみると番地とは思えない値。ここで、スタック壊されたなと見当をつける
6) b 行数 で、fopen() でファイルポインタを取得しているところにブレークポイントを当てる
7) r 実行時引数列 で再実行。6) であてたブレークポイントでとまる
8) n で一個すすめて fopen() を実行して、取得したファイルポインタの値を見る
9) watch ファイルポインタ==取得した値 でワッチポイントを設定
10) c 実行をコンティニューし、ファイルポインタが壊された所でとまる
11) 配列の代入をしているので p で添字を確認

と、ある意味 C で開発している人にとっては日常の風景なのですが配列のオーバーランをしている場所を突き止めて、サイズを修正して make します。

15分ぐらいの作業なのですが、これが 15 分ぐらいの作業ですんでいるのは相手が PC の小さなアプリケーションだったからで、例えば携帯電話の開発みたいなリソース制約のある環境での大規模開発の場合、そもそもデバッグシンボル付きのバイナリがメモリに乗り切らない等の理由で gdb が使えないことが多々あり、メモリ設定をいじってある特別のデバッグ用ボードを用意するとか、あとは gdb をあきらめて ICE でデバッグするなどどんどん大掛かりな話になって、機材の準備とか、デバッグ用のバイナリの再ビルドに8時間かかるとか大事になったりするのですが、それですめばまだいいほうでそういうデバッグ用機材の用意もできない事情がある開発現場も多々あり、ひたすら printf を追加して、8時間かけて再ビルドして、見当をつけて printf を追加して、、、というのを2ヶ月も続けているうちに出荷遅延の大問題になって睡眠なしで上記作業をつづけた挙げ句に、配列のオーバーランが原因とか、スタック変数を return で返してたとか、そういういつもの原因が見つかったりします。

で、しみじみと思ったのは、デバッガのありがたさではなくて^^:それは当然として C のようなコンパイル言語の生産性の低さです。ソフトウェア開発の生産性(productivity)を、ときどきコーディングの生産性で議論しているのを見かけることがありますが、C で書くと20行必要なコードが Haskell だと2行だとかいうあの議論ですが、実際、開発にかかる時間のほとんどがデバッグと手直しと保守改良であることを考えると、書きやすいことはあまり生産性と関係なく、むしろ書いたものが普通に動く、動かなくてもデバッグが速いことが生産性に一番響いてきます。特に大規模な組み込み開発という、コードサイズは大きいのに、ハードウエアリソースが限られている開発を人海戦術でこなしている場合に、このデバッグ地獄で発生する人月のロスは開発費を大きく押し上げることになるでしょう。

C で書いたコードがバッファオーバーランしやすいのはそのとおりだけど、設計時にしっかりとサイズを見積もっておけばいいはなしであって、ランタイムでチェックするのは実行効率をさげる。Cで書いてある事と品質とは関係なく、それはプロセスの話では?という意見もあるかもしれません。そもそもソフトウェアの品質とはISO9126 で列挙してもらっているように「機能性」「信頼性」「使用性」「効率性」「保守性」「移植性」の複合的なモデルであらわされるものです。関係ないですがソフトウェアの開発規模も複合モデルであらわされるものなのですが、ソフトウェアの難しさは目に見えないことの他にこういう複合性があるような気がします。さらに関係ないのですがこういう複合モデルというか彼の言い方をつかうと「コンストラクティブな」モデルはたいがいいいだしっぺがバリー・ベームだったりしますね。
さて、その「移植性」を考えた時、ソフトウェアは設計当初の用件を超えた環境で手直しなしに動くことも品質として期待されます。設計当初は 5000 個のファイルが処理できればいいや、ということで配列のサイズを 5000 にしてしまうと、それは後で、ファイルが10000個開けなければならない環境に移植した際に問題を起こし、それはデバッガが使える環境なら 15分で治せるような問題かもしれないので意識に載らないかもしれませんが、多くの実開発の現場では2ヶ月かかる問題の原因になっていたりします。保守性と移植性の観点で、静的な言語は、型チェックがあるという利点がある(ダイナミックな言語でも素性のいいものには動的型推論がありますが)を割り引いても、全てをコンパイル時に決定してしまいランタイムの処理が薄いことが原因で品質の問題を起こしやすく、それが生産性に影響を与えているのではないか、というのが私の印象です。

しばらく javascriptphp にどっぷりつかっていて久しぶりに gcc を使った感想として、やっぱり動的な言語は生産性がいいなあと。大体、開発が地獄になる原因のほとんどがデバッガが使えない事と、そのために入れた printf の結果を見るまでのコンパイルの待ち時間だったりすることを考えると、デスマーチの原因はインタプリタではなくコンパイラをつかう事にあるんじゃないか、などと乱暴なことを考えたりもした一日でした。

言語批判は宗教戦争の火種になりかねないところがあって危険な話題だったかなと思いつつも、Cには宗教戦争する信者もいないかなと思いつつ。ちなみにコンパイラ言語の型チェックのありがたさも理解してますし(php で たとえば WebServiceAPI を叩いていて、動き出すまでの修正は、型チェックしてメッセージだしてくれてたら一発だったのに、というのが多々あります)オブジェクトの実行効率の良さも、フロー解析して最適化してくれるコンパイラのおかげと理解してます。それはそれとして、まあなんというか。