2002年11月15日

_ [comp] keyword: C言語 関数のネスト 言語仕様

いまさら書いても遅いとは思いつつ(笑) 関数のネストは C 言語の正式な仕様では NG のはずです。新しい C の仕様である C99 では認められているのかと思って解説ページを見てみたのですが、やっぱり入っていないっぽいですね。 ただ、gcc では独自拡張で関数のネストができたりします。最初に学科の友人に教えてもらったときには目を疑いましたが。
#include 
int f(int a, int b)
{
    int add(int n)
    {
        return a+n;
    }
    return add(b);
}
int main(void)
{
    printf("%d\n", f(1,2));
    return 0;
}

これが動くだけでもぎょっとしますが……

#include 
typedef int int2int(int);
int f(int a, int b)
{
    int add(int n)
    {
        return n+a;
    }
    return g(b, add);
}
int g(int b, int2int* padd)
{
    return (*padd)(b);
}
int main(void)
{
    printf("%d\n", f(1,2));
    return 0;
}

これもうまく動いてしまいます。吐かせたアセンブリを読む限りでは、どうも f を呼び出した時のフレームポインタを %g2 レジスタに設定してから add を呼び出すラッパ関数をスタック上に動的に生成し、そのスタック上へのアドレスを関数アドレスとして渡している模様。SPARC だからこれでも動きますけど、データ領域ではコード実行できない x86 系ではどうやっているんでしょう……。

_ と、疑問に思って x86 上で動いている FreeBSD の gcc でコンパイルしてみたら、次のコードを吐きました。eax にはスタック上に確保したメモリ領域のアドレスが、ebp は現在のフレームのポインタが、edx には eax+10 と add のアドレスの差が入っています。

  movb $185,(%eax)
  movl %ebp,1(%eax)
  movb $233,5(%eax)
  movl %edx,6(%eax)

185=0xb9 は mov cx,<imm> そして 233=0xe9 は相対 jmp です。本当にラッパを動的に生成している模様。

んむ〜。FreeBSD ではスタック領域に実行可能ビットが立っているのでしょうか〜。それとも、世の中そういうもの?

……って、ちょっとまった。なんでスタック上を指しているアドレスと、add の開始アドレスを単純に引き算なんてできるんですか?しかも相対 jmp で飛んでるのは何?なんか、そもそもメモリモデルを勘違いしている可能性大です。セレクタとか、そーゆー高級なものは存在しないのでしょうか……

_ すわ、closure か!?と期待してしまうのですが、残念ながらラッパをスタック上に取ってしまうため、関数ポインタを外に返すことはできません。あくまでネストした関数を定義したフレームが生き続けている範囲でのみ使用可能です。

ええっと、忘れてしまったのですが、このように宣言時の環境(変数名とそれ示すメモリセルの map)を関数が保持するのを deep binding っていうんでしたっけ。shallow binding や dynamic binding のどれがどれだか分からなくなりつつあります(^^;関数型言語の世界の用語ですね。

ちなみに、perl の無名関数が closure と呼ぶに足る機能を持っているのは有名な話です。fold こそないですが、map や sort でいろいろいじくっていると関数型的なプログラミングの世界を堪能できます。これが perl の懐の深さですね。

_ [comp] DEBUG

WindowsXP には標準で DEBUG コマンドがついてきてくれているので助かりました。Windows98 あたりではついていないんですよね〜。でも、DEBUG は 386 で拡張された 32bit 命令を理解できません……

_ [comp] FreeBSD ports : net/unison

unison を FreeBSD の ports から入れようとしたら、OCaml のコンパイルから始まりました……。しかも、入るのはまだ beta の unison-2.9.20 だし。さっきから延々とコンパイルしています……