デバッグ例1:NULLポインタの中身を参照しようとした場合(原因は単純ミス)

算術式のコード生成の部分を作っている。スタックを用いた方法で算術式のコード生成をしようとしている。各演算の結果は必ずレジスタ0に入るように作るとする。oparser で作成された木の root から見ていき、まず自分の左側の木の結果をスタックに積み、その後右側の木の結果をレジすら0からレジスタ1に移し先ほどスタックに入れた左側の結果をレジスタ0に入れてレジスタ0と1の間で演算を行うようなコードを生成するように書こうとしている。

ただ、左側もしくは右側の子が変数・整数・実数の場合は単にその値をレジスタにロードするだけでいいので特別な関数 ldValuetoR0 を作ってその中で値をレジスタ0にロードするコードを生成する。

さて、これをコンパイルして実行したら、以下のようなエラーが出た。

......
......
......
oparser in: (← これは再帰下降型構文解析の時の名残の出力メッセージ)
Segmentation fault (core dumped)
そこで、デバッガを使って何が悪かったのかを探ってみよう。このプログラムの名前は compiler とした。そこで、
% gdb ./compiler compiler.core
でデバッガを起動する。すると、
GDB is free software and you are welcome to distribute copies of it
 under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.16 (i386-unknown-freebsd), 
Copyright 1996 Free Software Foundation, Inc...
Core was generated by `compiler'.
Program terminated with signal 11, Segmentation fault.
Cannot access memory at address 0x20017080.
#0  0x30e9 in codeExpression (top=0x18040) at codegen.c:18
18        if(top->left->token->type != ot_Ident)
のようなメッセージが出る。最初の5行は gdb を起動すると必ず出るメッセージである。6行目はcore ファイルを引数に指定したことを示している。7,8行目がプログラムが実行を停止した直接の理由を示している。9,10行目が実行が停止した場所に相当するソースプログラムである。codegen.c の 18行目であることが示されている。

ここで、list コマンドでこの18行目付近のソースプログラムを表示させてみよう。

(gdb) list
13      }
14
15      void codeExpression(Node *top){
16        /* 算術命令のある時・スタックの方法 */
17
18        if(top->left->token->type != ot_Ident)
19          codeExpression(top->left);
20        else
21          ldValuetoR0(top->left);
22        pushR0();

ここは、左側の子が ot_Ident すなわち変数・整数・実数でないならば codeExpression を再帰的に呼び、ot_Ident すなわち変数・整数・実数ならば ldValuetoR0 を呼んでいるつもりのところである。何が悪かったのであろうか。

gdb の最初の

Cannot access memory at address 0x20017080.
というメッセージから許可されていないメモリへのアクセスをしようとしたことがわかる。ということは、ポインタでアクセスしている、プログラム18行目の top->left->token->type が怪しい。

core ファイルを利用した時は、gdb を起動した時点でプログラムが実行時に終了した時のメモリ状況が再現されている。つまり、プログラムが core を吐いて終了した時に、実際に top->left->token->type の値が何になっているかがわかるのである。

そこで、実際に top->left->token->type の値が何になっているかを調べてみよう。ある変数の中身をみるには print コマンドを使う。

(gdb) print top->left->token->type
Cannot access memory at address 0x0.
おや? 0x0 (すなわち NULL)へのアクセスはできないと言われているよ。おかしいな、NULL へのアクセスをしているつもりはないのだが…。では、top->left->token を見てみよう。
(gdb) print top->left->token      
Cannot access memory at address 0x0.
おや? まだ同じメッセージだ。では、top->left を見てみよう。
(gdb) print top->left       
$1 = (Node *) 0x0
おお、top->left が NULL になっているのか。しかし、おかしいな、ここでは、左側の子が ot_Ident すなわち変数・整数・実数でない時、すなわち演算命令の時だけこの文に来るように書いたつもりだから左側の子が NULL であるはずはないのだが…。top->token の中身を見てみよう。ポインタの中身を見る時は print * コマンドである。
(gdb) print *top->token
$4 = {string = "20", '\000' , type = INTEGER}
おや? token の string の中身が数字になっている! なぜだ? あ、token の type が INTEGER になっている! ot_Ident ではない! そうか、ot_Ident は演算子構文解析で演算子の順位を定める時に使うもので、トークンのタイプではなかったのだった!

ということで、元のソースを ot_Ident ではなく、INTEGERかつREALかつIDENT と書き換えることでこのバグを取ることができる。

sasakura@momo.cs.okayama-u.ac.jp