C言語のデバッグ方法について
C言語はデバッグが難しい言語です。大体新機能はルンルンと書き始めるのですが、実際にコンパイルをして、動かしてみると、バグの嵐で絶望しか残りません。プロジェクトが大きくなってくるとデバッグに2、3日かかるとかザラです。ただ、動いた時のスッキリ感はすごいです。
コンパイルエラーについては出てくれるだけマシです。すぐ修正できるでしょう。問題は動的エラーで、特に難しいの論理的エラーです。
デバッグ方法としては主力はprintf,putsでトレースする方法です。printfは好きな場所で変数やメモリの中身を見れます。この時のコツとしてはインデントを無視してprintfを書き、egrep ^p *.cなどとデバッグコードを探せるようにしておくことです。デバッグが済んだらegrep ^p *.cとしてファイルを特定してvimで/^pとして消していけばいいでしょう。
セグメンテーションフォルトが起きる場合はすぐにgdbを使いましょう。-gオプションをつけてコンパイルしておけば、セグメンテーションフォルトが起こったソースファイル名、行番号がわかります。
ソースファイル名と行場号が分かれば、直前のところでprintfで関数の引数や構造体でフィールドアクセスを起こったところなどを見てみましょう。大抵0が入っていることが多いです。JavaでいうNull Pointer Exceptionですね。
C言語の動的エラーですが、セグメンテーションフォルトを起こすのは不正なアドレスが入ったポインタのdereffrence(*)、不正なアドレスの構造体のフィールドへのアクセス(これもdereffrenceとも言えます)、そして、さっきのNull Pointer Exception(これも0に対するdereffrence)です。
あとは配列やヒープなどの有効なメモリの範囲外アクセスもあります(ポインタが有効なメモリの外を指していた)。
gdbでは不正なメモリアドレスはちゃんとその旨を明記してくれます。btでバッグトレース、p 変数名でメモリを覗けます。
たまに落ちるなど不安定な場合はvalgrindを使いましょう。不正なメモリアクセスを出力してくれます。特にメモリの範囲外アクセスに強いです。-gオプションをつけてvalgrindを使いましょう。スタックトレースも表示してくれて、非常に便利です。あとはソフトウェアを作っていく過程で、たまにvalgrindでテストを行い、メモリアクセスが間違ってないか確認しておきましょう。valgrindが検出してくれるバグは常に0にしておくべきです。
ただ、このツールも論理的バグは修正してくれません。論理的バグはやはり2、3日、行頭にprintfを使い、まるで大きな山から犬と共に一匹のクマを探すかのように、追跡しないといけません。
もう一つ動的バグと言えるのはメモリリークです。mallocしたがfreeしてないってやつですね。valgrindでも検出できます。ただvalgrindのメモリリーク検出機能はバグがあるかもしれません。mallocしてfreeされたメモリがもう一度mallocされた場合の検出に不備がある様子です。Macだとleaksというコマンドがあるので、それで検出した方がいいかもしれません。また、mtraceというツールもある様子です。こっちの方が正確かもしれません。
C言語を長らくしていると
「C言語なんてさ。動け、動け、動いてくれー、動かん、死ぬ、死ぬ、死ぬ、動いたあああああああああみたいな感じ。Pythonなんて動いて、ふーん、だろ。C言語は楽しんだよ」
ってノリになってくると思います。
まあ、デバッグができた瞬間が嬉しくてたまりません。
もう、ノリは新撰組の斎藤一で
「バグ!即!斬!無論死ぬまで」
です。
デバッグで完全になってソフトウェアが完成した時の感じは
「棺桶にいたのは、俺だった、、、、」
です。
僕はcomelang2というモダンなCコンパイラを作っていますが、そちらは-cgオプションをつけると、デバッグがまだ楽かもしれません。メモリリークについては完全に検出してくれます。C++よりも便利なんじゃないかと思ってます。
良かったら試してみてください。Mac, Windows(WSL), Linux, ラズパイ、userland(android), termux(android), Dockerで動きます。
Discussion