GNU Nana

GNU nana: improved support for assertion checking and logging in GNU C/C++

という、C/C++デバッグライブラリがある。開発は1999年以降止まっている模様だが、「達人プログラマ」でも紹介されている、ユニークな考え方のライブラリです。日本語の解説はないようなので、少し書いてみよう。

GNU Nana 概要


GNU Nana は、 C/C++のassert関数を置換するためのソフトウェアである。

標準のassert関数や、プログラマがプロジェクトの度に書き起こす類似の関数(以下、表明関数とでもしましょう)は、次のような欠点を持つ。

  • 2つのバイナリが出来てしまう: 表明関数を有効にしたデバッグ版バイナリと、無効にしたリリース版バイナリ。
  • 表明関数はコードを肥大させる(条件文や、エラーメッセージや etc...)
  • 表明に失敗したとき、メッセージを表示して死ぬのみである。本当は、何故表明に失敗したのかデバッガで調べたいはずだが、それがしにくい。

nanaはこの問題を解決してくれる。具体的には

  • バイナリを一つしか作らなくて済む
  • そのため、表明関数から生成されるコードは、時間と空間の効率がよくなければならない。典型的な表明関数は数十バイトのアセンブリコードとなるが(glibcのassertはi386だと53バイト)、nanaは10バイト以下、時には1バイトのコードとなる。
  • 表明関数はランタイムに無効化できなければならない

となる。

使ってみましょう(1)


まずは、"10バイトのコードに置換されるassert"から試してみましょう。次のようなコードを書きます。

#include <nana.h> /* this file includes the other nana .h files */
int floor_sqrt(int i) { /* returns floor(sqrt(i) */ 
  int answer;
  I(i >= 0); /* assert(i >= 0) if i -ve then exit */
      /* code to calculate sqrt(i) */
  L("floor_sqrt(%d) == %d\n", 
        i, answer);  /* logs a printf style message */
}

これをcppにかけると次のようになります。

int floor_sqrt(int i) {
    int answer;
    do { if((1)) { if(!(i >= 0)) { _I_default_handler("I(""i >= 0"")","nana.c",5); } } } while(0);
      /* code to calculate sqrt(i) */
    do { if((1)) { ; fprintf (stderr,"floor_sqrt(%d) == %d\n", i, answer); } } while(0);
}

普通・・・ですね。_I_default_handler()も、次のようにfprintfしてfflushしてabortするだけです。I(...)から生成されたアセンブリコードも20バイト以上あるような。

(gdb) disas _I_default_handler 
Dump of assembler code for function _I_default_handler:
0x0804843c <_I_default_handler+0>:      push   %ebp
0x0804843d <_I_default_handler+1>:      mov    %esp,%ebp
0x0804843f <_I_default_handler+3>:      sub    $0x14,%esp
0x08048442 <_I_default_handler+6>:      pushl  0x8(%ebp)
0x08048445 <_I_default_handler+9>:      pushl  0x10(%ebp)
0x08048448 <_I_default_handler+12>:     pushl  0xc(%ebp)
0x0804844b <_I_default_handler+15>:     push   $0x8048560
0x08048450 <_I_default_handler+20>:     pushl  0x8049698
0x08048456 <_I_default_handler+26>:     call   0x80482ec <fprintf>
0x0804845b <_I_default_handler+31>:     add    $0x14,%esp
0x0804845e <_I_default_handler+34>:     pushl  0x8049698
0x08048464 <_I_default_handler+40>:     call   0x80482fc <fflush>
0x08048469 <_I_default_handler+45>:     call   0x804831c <abort>
End of assembler dump.

というわけでこっちは面白くもなんともありません。ステましょう。「表明関数が1バイトのコードに変換される」方に期待です。そっちは結構面白いので :-)

使ってみましょう(2)


さて、「表明関数が1バイトのコードに変換される」の方です。次のようなコードを用意し、

// nana2.c
#include <nana.h> /* this includes the other nana .h files */
int floor_sqrt(int i){
  int answer;
  DI(i >= 0); /* assert(i >= 0) if i -ve then exit */
     /* code to calculate sqrt(i) */
  DL("floor_sqrt(%d) == %d\n", i, answer);  /* logs a printf style message */
}
// main関数は省略

をcppにかけると、次のようになります。

int floor_sqrt(int i){
    int answer;
    asm("nop");
     /* code to calculate sqrt(i) */
    asm("nop");
}

おお、こりゃ確かに1バイトだ(笑)。これだけでは表明関数として役に立ちませんが、nanaはもう一仕事します。

$ nana nana2.c
break nana2.c:5
condition $bpnum (1) && (!(i >= 0))
command $bpnum
silent
echo "DI(""i >= 0"")" has failed at f:l with \n
where
end
break nana2.c:7
condition $bpnum (1)
command $bpnum
silent
printf "floor_sqrt(%d) == %d\n", i, answer
cont
end

このように、gdbに食わせるスクリプトを用意してくれるんですねー。上記出力を例えば nana2.gdb に出力しておき、nana2.c を -O2 -g でコンパイルしておき、nana-run コマンドを使って実行すると次のようになります。

  • nanaが出力したgdbスクリプトが"ファイル名:行番号"形式でbreakを貼っている関係で、デバッグ情報なしはNG。-g か -ggdb を付与し、stripしないでください。
  • "シングルバイナリ"を善しとするnanaを使っているので、最初から -O2 としてみました。
$ gcc -Wall -g -O2 nana2.c
$ nana-run a.out -x nana2.gdb 
Breakpoint 1 at 0x804833f: file nana2.c, line 6.
Breakpoint 2 at 0x8048340: file nana2.c, line 8.
floor_sqrt(9) == 3 
$

nana-runは、内部でgdbを起動して自動実行してるだけですから、

$ gdb a.out
(gdb) source nana2.gdb
(gdb) run

でもOKです。なお、私の環境だとnana2.gdbをよみこんだあとに (gdb) enable と一発入力しておかないとうまく動きませんでした。nanaコマンドの出力がおかしいのかなぁ

以上、簡単な解説でした。このnana、少なくともアイディアは面白いのものなので、覚えておくといろいろ役にたちそうな予感ですよ ;-)