次のページ 前のページ 目次へ

4. 移植とコンパイル

4.1 自動定義されるシンボル

手元の gcc でどんなシンボルが自動定義されているかを調べるには -v スイッチをつけて gcc を実行します。私の場合は以下のようになり ました。

$ echo 'main(){printf("hello world\n");}' | gcc -E -v -
Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specs
gcc version 2.7.2
 /usr/lib/gcc-lib/i486-box-linux/2.7.2/cpp -lang-c -v -undef
-D__GNUC__=2 -D__GNUC_MINOR__=7 -D__ELF__ -Dunix -Di386 -Dlinux
-D__ELF__ -D__unix__ -D__i386__ -D__linux__ -D__unix -D__i386
-D__linux -Asystem(unix) -Asystem(posix) -Acpu(i386)
-Amachine(i386) -D__i486__ -

Linux 特有の機能に依存したコードを書いている場合はその部分を以下のよう に囲っておくと良いでしょう。

#ifdef __linux__
/* ... funky stuff ... */
#endif /* linux */

この目的には __linux__ を用います。 linux は用いるべきでは ありません。後者は POSIX 準拠ではないからです。

4.2 コンパイラへの指示

コンパイラのスイッチに関する文書は gcc の info に書かれています。 (Emacs からは C-h i として `gcc' オプションを選びます)。インス トールに用いたバイナリ配布パッケージによってはこれが入っていなかったり 古かったりすることがあります。その場合は gcc のソースアーカイブを ftp://prep.ai.mit.edu/pub/gnu やミラーサイトから入手し、その中の info をコピーして使いましょう。

gcc の man ページ gcc.1 は、一言で言ってしまうと内容が古いです。 これは man ページそのものの中でも警告されています。

コンパイラのフラグ

gcc のコマンドラインに -On をつけると出力される コードを最適化することができます。 n は整数です(省略すると 1 と みなされます)。意味のある n の値とそれぞれの値に対する実質的な効 果はコンパイラのバージョンによって変わりますが、通常は 0 (最適化なし) から 2(たくさん)あるいは 3(とてもたくさん)までが意味を持ちます。

gcc 内部ではこれらの値は -f-m オプション群に展開されます。 -O オプションのそれぞれのレベルにどのようなオプションが対応してい るかを調べるためには gcc を -v-Q オプションをつけて実行 します(後者のオプションはマニュアルには載っていません)。例えば私の場 合 -O2 に対しては以下のようになります。

enabled: -fdefer-pop -fcse-follow-jumps -fcse-skip-blocks
-fexpensive-optimizations
         -fthread-jumps -fpeephole -fforce-mem -ffunction-cse -finline
         -fcaller-saves -fpcc-struct-return -frerun-cse-after-loop
         -fcommon -fgnu-linker -m80387 -mhard-float -mno-soft-float
         -mno-386 -m486 -mieee-fp -mfp-ret-in-387

使っているコンパイラでの最も高い最適化レベルよりも高い数値を指定した場 合(例えば -O6 など)の動作は、最高レベルの最適化を指定したのと同 じになります。しかし配布するコードにこのようなやり方で最適化を指定する のは良いアイディアとは言えません。もし将来リリースされるコンパイラで更 なる最適化が導入された場合、コードがうまく動かなくなる可能性があるから です。

gcc 2.7.0 および 2.7.2 のユーザは、これらの版の -O2 にはバグがあ ることに気をつけて下さい。具体的には strength reduction が動作しないの です。 gcc を再コンパイルする場合はパッチを当てることによってこの問題 は解決できます。そうしない場合はコンパイルの際に常に -fno-strength-reduce を指定するようにして下さい。

プロセッサ固有のフラグ

-O オプションのどのレベルでも指定はされませんが、 -m は 有用なフラグ群です。その最たるものは -m386-m486 で、それ ぞれ 386 と 486 に有利なコードを出力するように指定します。これらのオプ ションを指定してコンパイルしたコードは、それぞれ他のチップでも動作しま す。 486 のコードは大きくなりますが、386 の上でも遅くなることはありま せん。

現在はまだ -mpentium あるいは -m586 というオプションは存在し ません。 Linus によれば -m486 -malign-loops=2 -malign-jumps=2 -malign-functions=2 を使うとアラインメントのための大きなギャップを作 ることなく 486 のコードを最適化できるそうです(Pentium ではそも そもアラインメントが必要とされません)。 Cygnus の Michael Meissner は こう言っています。

個人的には -mno-strength-reduce を x86 のコードに指定すると速度は 向上すると思う(strength reduction のバグのことを言っているわけではな いことに注意されたい。それはまた別の話である)。 x86 CPU ではレジスタ が不足しやすいため、 gcc で用いている手法(レジスタ群を spill レジスタとそれ以外へグループ分けする)と相性が悪いからである。 strength reduction では乗算を加算で置き換える際により多くのレジスタを 使用する。私は -fcaller-saves も同様に性能低下の原因になると考える。
もう一つ私見を。 -fomit-frame-pointer は有利に働く場合も 不利に働く場合もあると思う。このオプションは他のレジスタを割り当て可能 にする。一方 x86 が命令セットを解釈するやり方から考えて、スタック相対 アドレスはフレーム相対アドレスよりも大きなスペースを必要とする。したがっ てプログラムで利用できる I キャッシュが少なくなってしまう。同様に -fomit-frame-pointer を指定するとコンパイラはスタックポインタを命 令コールのたびに再配置するが、フレームがある場合はスタックアキュムレー タを数命令で使いきってしまうことになる。

この話題の最後は再び Linus の言葉で締めくくりましょう。

最高の性能を得るには私を信じちゃいけません。テストして下さい。 gcc コ ンパイラにはたくさんのオプションが在り、その組み合せのうちの一つがあな たにとってのベストな最適化となるはずです。

Internal compiler error: cc1 got fatal signal 11

シグナル 11 は SIGSEGV または「セグメント違反」を意味します。通常こ れはプログラム中でポインタが混乱し、プログラムで管理していないメモリ領 域に書き込みを行おうとした結果です。したがってこれは gcc のバグである 可能性もあります。

しかし gcc (のほとんどの部分)は細部までテストされた信頼すべきソフト ウェアと言えます。一方 gcc では数多くの複雑なデータ構造や無数のポイン タを用いています。つまり通常手に入る中で最も優秀な RAM のテスターであ るとも言えるのです。もしバグが再現されなければ(コンパイルを再び行なっ たときに同じところで止まるのでなければ)それは多分間違いなく使っている ハードウェア(CPU、メモリ、マザーボードまたはキャッシュ)の障害です。 システムが電源投入時のチェックをパスするからといって、あるいは Windows で問題なく動作するからといってこの障害をバグと言ってはいけません。これ らの『テスト』は一般に価値が無いとみなされているからです(正当な判断と 言えます!)。またカーネルのコンパイルがいつも `make zImage' の途 中で停止するからといって、これをバグだと言ってこないでください --- そ りゃ確かにバグかもしれませんけどね。 `make zImage' はおそらく 200 以上のファイルをコンパイルします。我々が知りたいのはもう少し小さな範囲 なのです。

もしバグが再現できたら、また(より望むらくは)バグを引き起こす短いプロ グラムがあったら、その問題に関するバグレポートを FSF か linux-gcc メー リングリストに送りましょう。 gcc の文書を良く読んで、彼らが必要とする 情報に関して理解してからにしましょう。

4.3 移植性

最近では『もし Linux に移植されていないプログラムがあったとしたら、 それはそもそも移植されるべき価値が無いのだ』とも言われています。 :-)

もう少し真面目に。しかし一般に Linux の 100% POSIX 準拠を満たすには ソースを少々変更するだけで良いはずです。行なった変更はプログラムの原著 者にフィードバックすると良いでしょう。以降は `make' だけで実行ファイ ルができるようにしてもらえるかもしれません。

BSD 系(bsd_ioctldaemon および <sgtty.h>

プログラムは -I/usr/include/bsd をつければコンパイルでき、 また -lbsd をつければリンクできます(つまり Makefile の CFLAGS-I/usr/include/bsd を加え、 LDFLAGS-lbsd を加えるわけです)。 BSD 形式のシグナルの振る舞いを用い るために -D__USE_BSD_SIGNAL を加える必要はもうありません。 -I/usr/include/bsd を加えて <signal.h> をインクルー ドすれば自動的に選択されます。

「失われた」シグナル(SIGBUS, SIGEMT, SIGIOT, SIGTRAP, SIGSYS など )

Linux は POSIX に準拠しています。これらは POSIX で定義されているシ グナルではありません。 ISO/IEC 9945-1:1990 (IEEE Std 1003.1-1990) の B.3.3.1.1 から引用します。

「SIGBUS、 SIGEMT、 SIGIOT、 SIGTRAP、 SIGSYS の各シグナルは POSIX.1 から削除されます。これらのシグナルの振舞いは実装によって異なっており、 適当な分類ができないからです。 POSIX 準拠の実装でもこれらのシグナル を発行することは許されていますが、発行される状況は文書化しなければなり ませんし、これらシグナルの発行に関するあらゆる制限を記述しておく必要が あります。」

これを回避する安直な方法はこれらのシグナルを SIGUNUSED として 再定義することです。正しい方法はこれらを扱っているコードを適当な #ifdef の組で囲うことです。

#ifdef SIGSYS
/* ... non-posix SIGSYS code here .... */
#endif

K & R のコード

GCC は ANSI のコンパイラです。しかし現在存在する C のコードはほと んどが ANSI 準拠ではありません。 K & R のコードに関して GCC ができ ることはコンパイラのフラグに -traditional を付けることぐらいです。 もう少し精密なコントロールをすることも可能ですが、これらをエミュレート するのは各種の頭痛の種になるでしょう。詳しくは gcc の info を参照して 下さい。

-traditional は gcc の文法を変えるだけでなく、副作用を生じること に注意して下さい。例えば -traditional によって -fwritable-string が有効になります。このスイッチにより文字列定数 はデータ領域に書き込まれます(スイッチがないとプログラムによる変更が行 われないテキスト領域に書き込まれます)。これにより、プログラムのメモリ 使用量が増加します。

プリプロセッサのシンボルがコード中のプロトタイプと衝突する

ありがちなのは、汎用の関数が Linux のヘッダファイルでもマクロとして定 義されているため、プリプロセッサがコード中の同様なプロトタイプ宣言を 認めなくなるという問題です。良くあるのは atoi()atol() です。

sprintf()

(特に SunOS から移植する際に)気をつけなければならないのは、 sprintf(string, fmt, ...) の戻り値は多くの Unix では string へのポインタであるのに対して、 Linux では(ANSI に従い)文字列へ書き込 まれた文字数になっていることです。

fcntl など。 FD_* の定義はどこにあるの?

<sys/time.h> で定義されています。 fcntl を用い る場合は <unistd.h> も一緒にインクルードする必要があるでしょ う。実際のプロトタイプはここで定義されています。

一般に、関数に必要な #include は man ページの SYNOPSIS セクショ ンに記述されています。

select() が一度タイムアウトするとプログラムがウェイトしなくなる

昔は select() の timeout 引数は変更されませんでした。し かし当時でもマニュアルには以下のように書かれていました。

select() は与えられた timeout から(もしあれば)残った時間を、time の 値を置き換えることによって返すべきです。これはシステムの将来のバージョ ンでインプリメントされるでしょう。従って timeout のポインタが select() の呼び出しによって変更されないことを仮定したコードを書くのは良くありま せん。

その将来が来たわけです、少なくともここでは。 select() から戻るとき、 timeout 引数には待ち時間の残りがセットさ れます。データが最後まで到着しなけ れば timeout は 0 になりますので、 timeout 構造体をそのままにしてもう 一度 select() を呼ぶと、すぐに制御が返って来てしまいうというわけ です。

この問題を修正するには select() を呼ぶ度にタイムアウトの値を timeout 構造体に代入してやれば良いのです。今までのコードが以下のような ものだとしたら、

      struct timeval timeout;
      timeout.tv_sec = 1; timeout.tv_usec = 0;
      while (some_condition)
            select(n,readfds,writefds,exceptfds,&timeout); 
このように変えて下さい。

      struct timeval timeout;
      while (some_condition) {
            timeout.tv_sec = 1; timeout.tv_usec = 0;
            select(n,readfds,writefds,exceptfds,&timeout);
      }

Mosaic のあるバージョンではこの問題が残っていたことがありました。回転 する地球のアニメーションが、ネットワークから到着するデータの速度と反比 例した速さで回転したのです!

システムコールが割り込まれる

症状

プログラムが Ctrl-Z で停止されてから再開される(あるいはシグナルを 発生する他の状況: Ctrl-C による中断や子プロセスの終了など)と、プログ ラムが "interruputed system call" や "write: unknown error" と言ったようなメッセージを出します。

問題点

POSIX のシステムでは古い UNIX よりもシグナルチェックをする局面が多 くなっています。 Linux は以下のようなシグナルハンドラを実行します。

他の OS では以下のようなシステムコールが対象になる場合もあります。 creat()close()getmsg()putmsg()msgrcv()msgsnd()recv()send()wait()waitpid()wait3()tcdrain()sigpause()semop()

もしプログラムがハンドラを持っているシグナルがシステムコールの 途中で発生すると、そのシグナルハンドラが呼び出されます。ハンドラからの 制御が(システムコールに)戻ると、システムコールは自分に対する割り込み を検知し、ただちに -1 を返すとともに errno = EINTR を セットします。プログラムはこのようなことが起こるとは思っていませんから、 異常終了します。

対処法は二つあります。

(1) 導入したシグナルハンドラごとに SA_RESTART を sigaction フラグに追加します。例えば

  signal (sig_nr, my_signal_handler);

のようなものを以下のように書き換えます。

  signal (sig_nr, my_signal_handler);
  { struct sigaction sa;
    sigaction (sig_nr, (struct sigaction *)0, &sa);
#ifdef SA_RESTART
    sa.sa_flags |= SA_RESTART;
#endif
#ifdef SA_INTERRUPT
    sa.sa_flags &= ~ SA_INTERRUPT;
#endif
    sigaction (sig_nr, &sa, (struct sigaction *)0);
  }

これはほとんどのシステムコールに適用できますが、 read()write()ioctl()select()pauseconnect() の各システムコールに対しては EINTR のチェックをプ ログラム中で行なう必要があります。以下を参考にして下さい。

(2) EINTR をプログラム中で明示的にチェックする。

read()ioctl() に対する二つの例を示します。

まず read() の場合です。

int result;
while (len > 0) { 
  result = read(fd,buffer,len);
  if (result < 0) break;
  buffer += result; len -= result;
}

のようなコードを以下のように書き換えます。

int result;
while (len > 0) { 
  result = read(fd,buffer,len);
  if (result < 0) { if (errno != EINTR) break; }
  else { buffer += result; len -= result; }
}

次は ioctl() の例です。

int result;
result = ioctl(fd,cmd,addr);

これを以下のように書き換えます。

int result;
do { result = ioctl(fd,cmd,addr); }
while ((result == -1) && (errno == EINTR));

BSD Unix のバージョンによっては、デフォルトでシステムコールをやり直す ことになっていることもあります。この場合システムコールを中断するには、 SV_INTERRUPUTSA_INTERRUPT フラグを用いる必要があります。

書き込み可能な文字列(プログラムがランダムに停止する)

GCC はユーザを信頼しており、文字列定数はあくまで定数として扱われる ものとみなしています。従って GCC では文字列定数はテキスト(コード)領 域に保持されます。ここはプログラムのディスクイメージにページングされま す(スワップ領域に take up される代わりに)ので、この文字列定数を書き 換えようとするとセグメント違反となります。これは仕様です!

古いプログラムの場合ではこれが問題になることがあります。例えば mktemp() を文字列定数を引数にして呼び出す場合などです。 mktemp() は引数を書き換えようとするためです。

修正するには二つの方法があります。 (a) -fwritable-string を付けて コンパイルして gcc に定数をデータ領域に保持するよう伝える。 (b) 問題と なる部分を定数でない文字列に strcpy して、こちらを用いる。

execl() が失敗する

間違った呼び出し方をしているからです。 execl の最初の引数は実 行したいプログラムです。2番目以降の引数は呼び出すプログラムの argv 配列になります。ここで argv[0] はプログラムの パスそのものであることに注意して下さい。従ってexecl の呼び出しは 以下のように書く必要があります。

execl("/bin/ls","ls",NULL);

単に以下のように書くのは間違いです。

execl("/bin/ls", NULL);

少なくとも a.out の場合は、引数を全くセットせずにプログラムを実行する と、依存しているダイナミックライブラリを表示するようになっています。 ELF ではまた違った動作となります。

もしこのライブラリの情報が必要でしたら、もっと簡単なインターフェースが あります。ダイナミックロードの節か ldd の man ページを見て下さい。


次のページ 前のページ 目次へ