関数ポインタのキャストと gcc

C の関数呼び出しは、関数定義の型と互換性のない型として呼び出したら未定義動作です。例えば以下のコードの動作は未定義です。

#include <stdio.h>
int main(void) {
  ((int (*)(char *, ...)) &printf)("Hello, world!\n");
}

printf は int printf(const char *, ...) なので、const が無くなってるのが間違ってます。未定義のプログラムはどのような実行結果になろうとも、C の規格には違反しません。
でも、現実問題としてこのコードは期待通りに動くだろー、と高をくくってました。しかし最近の gcc (3.4 以降くらい) では、このコードは落ちます。

$ gcc --version
gcc (GCC) 4.3.1
Copyright (C) 2008 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc -o hello hello.c
hello.c: In function 'main':
hello.c:6: warning: function called through a non-compatible type
hello.c:6: note: if this code is reached, the program will abort

$ ./hello
Illegal instruction

もちろん、未定義なので落ちても文句は言えません。
gcc は関数ポインタのキャストに関して明らかに未定義なプログラムに対しては、なんとわざと不正な命令を埋め込んでくれるらしいです。gcc 恐ろしい子
いやあ gcc は、これといい strict-aliasing rules といい、アセンブリ知ってると C がわかりやすいどころか、かえって落とし穴になりますね。いいぞもっとやれ。


とは言っても、静的解析しまくって執念深く追いかけたり、関数呼び出しで動的検査したりするわけではないみたいで、以下のコードは期待通りに動いちゃいます。まあ未定義だけど。

#include <stdio.h>

int main(int argc, char *argv[])
{
    void *f = (void *) &printf;
    ((int (*)(char *, ...)) f)("foo %d\n", 1);
    return 0;
}


ちなみに、このおかげでうちの環境では Ruby の openssl ライブラリのテストが落ちるわけです。

$ ./ruby test/openssl/test_ec.rb
Loaded suite test/openssl/test_ec
Started
Illegal instruction

これは完全に openssl の問題。openssl は型の扱いが超いい加減。怪しいキャストをするだけで一概にどうとは言えないけど、openssl みたいな信用第一なプロジェクトでそんなもんなのは面白いなあ。あ、openssl 0.9.8f 以降では直ってました。