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を使っているので、最初から -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、少なくともアイディアは面白いのものなので、覚えておくといろいろ役にたちそうな予感ですよ ;-)