最新

val it : α → α = fun

<<  2006/02  >>

2006/02/25 関数ポインタとラベル変数の怪しい関係(あるいはキャストの気持ち悪さについて)

さて。まず前提知識として関数ポインタとラベル変数について解説しましょう。

関数ポインタはさすがに知っていると思いますが、関数のアドレスを持つポインタですね。

int f()
{
  return 1;
}

int main(){
  int (*g)();
  g = f;
  printf("%d\n", f());
  printf("%d\n", g());
  printf("%p\n", f);
}

このようになるのでした。関数ポインタgのアドレスはfに一致し、あたかもそのような名前の関数があるかのように呼べるわけでした。

さて、よく知られているように C 言語には goto 文があり、指定したラベルにジャンプすることができますが、 gcc には、このラベルを指示するための変数という独自拡張があります。

int main(){
  void *ptr;
 LABEL:
  ptr = &&LABEL;
  printf("test\n");
  goto *ptr;
}

かように無限ループを作成できます。 && は gcc で独自に定義される1項演算子であり(&が二重になっているのではない)、これによってラベルのアドレスが ptr に渡せます。そして void* な変数を参照する goto を書くことができるのでした。

さて、両者を混ぜたらどうなるか?

まず、ラベルに対して関数呼び出ししてみましょう。

int main(){
  void (*ptr)();
  int i = 0;
  ptr = (void(*)())&&LABEL;
 LABEL:
  printf("%d\n", i++);
  if (i < 10) {
    ptr();
    printf("called\n");
  }
  return 0;
}

さてこのコードの動作を正しく推定できますか。ちなみに答えは「0〜9までが印字される」です。私も間違えました。まず、 ptr には LABEL の位置が渡されるわけです(キャストで無理矢理あてはめています)。これを ptr() のように指定するのがまず通ってしまう。そして実際に LABEL へ処理が移ります。 x86 で gcc -S するとわかりますが、これには call を使っています。しかし戻り先のアドレスをプッシュするわけではありませんから、 called のパートは最後まで実行されることはありません。

なかなか気持ち悪いですね。では次は逆です。

int f() {
  printf("f\n");
  return 1;
}

int main(){
  void *ptr;
  ptr = (void*)f;
  goto *ptr;
}

さあこの実行結果は何か。

% ./a.out
f
zsh: 48087 bus error  ./a.out

というわけでバスエラーになってしまいました。ちょっと gdb してみましょう。

(gdb) run
Starting program: /home/mukai/./a.out
f

Program received signal SIGBUS, Bus error.
0xbfbff6d1 in ?? ()
(gdb) bt
#0  0xbfbff6d1 in ?? ()
#1  0x8048406 in _start ()

なんだか得体の知れないことになっていますが、 main が消えてしまっているのが問題ありそうです。では main からさらに間接的に呼んでみましょう。

int f()
{
  printf("f\n");
  return 1;
}

int f2(){
  void *ptr;
  ptr = (void*)f;
  goto *ptr;
  return 2;
}

int main(){
  f2();
}

こんな感じです。すると、

% ./a.out
f
zsh: 48116 segmentation fault  ./a.out

おお、 segmentation fault になってしまいました。意味がわからん。では gdb してみましょう。

(gdb) run
Starting program: /home/mukai/a.out
f

Program received signal SIGSEGV, Segmentation fault.
0x28069080 in ?? ()
(gdb) bt
#0  0x28069080 in ?? ()
#1  0x80484e3 in main () at test.c:14

というわけで _start がちゃんと main になったものの、やっぱりなんだかよくわからないようです(念のために書いておくとちゃんと -g でコンパイルしています)。

ちなみに、今の手元の環境(FreeBSD 4.x機)では bus error → segmentationfault でしたが、 Linux で gcc-4.0 でやったときには segmentation fault → ふつうに動作終了になりました。ただし、最後のふつうに動作終了時にも、どこにどんな数値を指定してもなぜか main には 0 が帰ってくるという謎の挙動が出てきて頭をかかえましたが。

まあ何にしても gcc 限定の話だし、挙動もアーキテクチャやOSやgccのバージョンに依存するでしょうという話でした。

けっきょくわけがわからなくなっただけでしたが、いかがでしたでしょうか。わたしの感想としては「キャストってやっぱこえーなー」という感じでした。

あ、ちなみにこれも今日知りました。

int f() {
  printf("f\n");
  return 1;
}

int main(){
  f(10);
}

いやー () ってずーっと (void) の略記だと思ってましたよ。まさか (int) の省略形だったとは。まあどっちにせよ引数の値は取り出せないのですが……いやちょっと待てよ、スタックを触ってみたり、 va_args を使ったりすれば実は取り出せるのかな。