1. ホーム
  2. optimization

コンパイラの最適化でバグが発生することはあるのか?

2023-11-07 18:50:52

質問

今日、私は友人と議論をし、コンパイラの最適化について2、3時間議論しました。

私は次の点を弁護しました。 時には で、コンパイラの最適化はバグや、少なくとも望ましくない動作をもたらすかもしれないという点を弁護しました。

私の友人は完全に同意せず、「コンパイラは賢い人々によって作られ、賢いことをするものだ。

決して と言っています。

彼は私を全く納得させませんでしたが、私は自分の主張を強化するための実例が不足していることを認めざるを得ません。

ここでは誰が正しいのでしょうか?もし私が正しいのであれば、コンパイラの最適化が結果のソフトウェアにバグをもたらしたという実例があるのでしょうか?もし私が間違っているなら、プログラミングをやめて、代わりに釣りを学ぶべきでしょうか?

どのように解決するのですか?

コンパイラの最適化によって、バグや望ましくない動作が発生することがあります。 そのため、それらをオフにすることができます。

1 つの例として、コンパイラはメモリ位置への読み取り/書き込みアクセスを最適化し、重複した読み取りや重複した書き込みを排除したり、特定の操作を再順序付けしたりすることができます。 問題のメモリ位置が単一のスレッドによってのみ使用され、実際にメモリである場合、それは問題ないかもしれません。 しかし、そのメモリ位置がハードウェアデバイスのIOレジスタである場合、書き込みの再順序付けや削除は完全に間違っている可能性があります。 このような状況では、通常、コンパイラーが最適化する可能性があり、したがって、単純なアプローチが機能しないことを知っているコードを記述する必要があります。

更新しました。 Adam Robinsonがコメントで指摘したように、私が上で説明したシナリオは、オプティマイザのエラーというより、プログラミングのエラーです。 しかし、私が説明しようとしたポイントは、そうでなければ正しいプログラムが、そうでなければ正しく動作するいくつかの最適化と組み合わされたときに、プログラムにバグをもたらす可能性があるということです。 場合によっては、言語仕様に「このような最適化が行われるとプログラムが失敗する可能性があるので、このようにしなければならない」と書かれていることもあり、その場合はコードのバグとなります。 しかし、コンパイラに(通常はオプションの)最適化機能がある場合、コンパイラがコードを最適化しようとしすぎたり、最適化が不適切であることを検出できなかったりして、不正なコードを生成することがあります。 この場合、プログラマは問題のある最適化をいつオンにしても安全であることを知らなければなりません。

もうひとつの例です。 例えば linux カーネルにバグがあった このバグでは、潜在的に NULL であるポインターが、そのポインターが NULL であるかどうかをテストする前にデリファレンスされます。しかし、場合によっては、メモリーをアドレス 0 にマップすることが可能で、その結果、デリファレンスが成功することがありました。コンパイラはポインタが再参照されたことに気づくと、それが NULL であるはずがないと仮定し、その後 NULL テストとそのブランチ内のすべてのコードを削除しました。 これはコードにセキュリティの脆弱性を導入しました。 というのも、この関数は攻撃者が提供したデータを含む無効なポインタを使用するようになるためです。ポインタが合法的に NULL で、メモリがアドレス 0 にマッピングされていない場合、カーネルは以前と同じように OOPS します。つまり、最適化前のコードには 1 つのバグが含まれていましたが、最適化後は 2 つのバグが含まれ、そのうちの 1 つはローカル ルートの悪用を可能にしました。

CERT によるプレゼンテーション Robert C. Seacord による "Dangerous Optimizations and the Loss of Causality" というプレゼンテーションがあり、プログラムにバグを導入 (または暴露) する多くの最適化についてリストアップしています。ハードウェアが行うことを行う」、「可能なすべての未定義動作をトラップする」、「許可されていないことは何でも行う」など、可能なさまざまな種類の最適化について説明しています。

積極的に最適化するコンパイラーが手をつけるまでは、まったく問題ないコードの例もあります。

  • オーバーフローをチェックする

    // fails because the overflow test gets removed
    if (ptr + len < ptr || ptr + len > max) return EINVAL;
    
    
  • オーバーフロー演算を全く使用しない

    // The compiler optimizes this to an infinite loop
    for (i = 1; i > 0; i += i) ++j;
    
    
  • 機密情報の記憶を消去する。

    // the compiler can remove these "useless writes"
    memset(password_buffer, 0, sizeof(password_buffer));
    
    

ここでの問題は、何十年もの間、コンパイラーは最適化にあまり積極的ではなかったため、何世代もの C プログラマーが、固定サイズの 2 の補数加算やそれがどのようにオーバーフローするのかといったことを学び、理解してきたということです。ところが、コンパイラの開発者によってC言語の規格が改正され、ハードウェアは変わらないのに、微妙なルールが変わってしまう。C 言語仕様は開発者とコンパイラの間の契約ですが、契約の条件は時間の経過とともに変更される可能性があり、すべての人がすべての詳細を理解しているわけではなく、詳細が賢明であるとさえ同意しているわけでもないのです。

このため、ほとんどのコンパイラーは最適化をオフにする (またはオンにする) フラグを提供しています。あなたのプログラムは、整数がオーバーフローするかもしれないということを理解した上で書かれていますか?それなら、オーバーフローの最適化をオフにすべきです。なぜなら、オーバーフローはバグを引き起こす可能性があるからです。あなたのプログラムは、ポインタのエイリアシングを厳密に避けていますか?それなら、ポインタが決してエイリアスされないことを前提とした最適化をオンにすればいいのです。あなたのプログラムでは、情報漏洩を防ぐためにメモリをクリアしようとしますか?ああ、その場合は運が悪いですね。デッドコード除去をオフにするか、コンパイラがあなたの "デッド" コードを除去しようとしていることを前もって知っておき、それに対する何らかの回避策を使用する必要があります。