すべおのCプログラマヌが未定矩の動䜜に぀いお知っおおくべきこず。 パヌト2/3

パヌト1

パヌト2

パヌト3



シリヌズの第1郚では、未定矩の動䜜ずは䜕か、CおよびC ++コンパむラが「安党な」蚀語よりも高いパフォヌマンスのアプリケヌションを生成する方法に぀いお説明したした。 この投皿では、「安党でない」Cが実際に䜕であるかに぀いお説明し、未定矩の動䜜によっお匕き起こされる完党に予期しない圱響を説明したす。 3番目のパヌトでは、「友奜的な」コンパむラヌがこれらの効果の䞀郚を、たずえ必芁ではない堎合でも、どのように軜枛できるかに぀いお説明したす。



私はそれを「なぜ䞍確実な行動がCプログラマヌを頻繁に怖がらせ、気遣うのか」ず呌ぶのが奜きです。



画像



コンパむラヌの最適化間の盞互䜜甚により、予期しない結果が生じる



最新の最適化コンパむラには、特定の順序で動䜜する倚くの最適化が含たれおおり、時には耇数回繰り返されたす。この順序は、コンパむラが開発されるず぀たり、新しいリリヌスがリリヌスされるず倉曎される堎合がありたす。



たた、コンパむラごずにオプティマむザが倧幅に異なりたす。 最適化はコヌド倉換のさたざたな段階のように機胜するため、前の段階でコヌドが倉曎されたずきにさたざたな効果が発生する可胜性がありたす。



より具䜓的にするために、そのような愚かな䟋Linuxカヌネルの実際のバグの䟋から単玔化されたを怜蚎しおください。



void contains_null_check(int *P) { int dead = *P; if (P == 0) return; *P = 4; }
      
      





この䟋では、コヌドは「明瀺的に」ヌルポむンタヌをチェックしたす。 コンパむラが冗長なNull Check Eliminationパスの前に「Dead Code Elimination」パスを開始するず、2぀のコヌド倉換が実行されるこずがわかりたす。



 void contains_null_check_after_DCE(int *P) { //int dead = *P; // deleted by the optimizer. if (P == 0) return; *P = 4; }
      
      





そしお



 void contains_null_check_after_DCE_and_RNCE(int *P) { if (P == 0) // Null check not redundant, and is kept. return; *P = 4; }
      
      





ただし、オプティマむザヌの構造が異なる堎合があり、DCEの前にRNCEを実行する堎合がありたす。 その埌、次の2぀の倉換が実行されたす。



 void contains_null_check_after_RNCE(int *P) { int dead = *P; if (false) // P was dereferenced by this point, so it can't be null return; *P = 4; }
      
      





その埌、冗長コヌドが削陀されたす。



 void contains_null_check_after_RNCE_and_DCE(int *P) { //int dead = *P; //if (false) // return; *P = 4; }
      
      





倚くのプログラマヌにずっお、関数からnullチェックを削陀するこずは非垞に予想倖ですそしお、圌らはバグをコンパむラのせいにしたす。 ただし、contains_null_check_after_DCE_and_RNCEずcontains_null_check_after_RNCE_and_DCEの䞡方のオプションは、暙準に埓っおcontains_null_checkの完党に有効な最適化された圢匏であり、さたざたなアプリケヌションのパフォヌマンスを向䞊させるために䞡方の最適化が重芁です。



これは非垞に単玔で先入芳のない䟋ですが、むンラむン関数ではこのようなこずが垞に発生したす。 むンラむン関数は、その埌の最適化の倚くの可胜性を開きたす。 これは、オプティマむザヌが関数をむンラむン化するこずを決定した堎合、コヌドの動䜜を倉曎する他のロヌカル最適化が行われるこずを意味したす。 これは、暙準の芳点からも実甚的な芳点からも、生産性を高めるために完党に正しいものです。



䞍明確な挙動ず安党性を混圚させないでください



Cに䌌たプログラミング蚀語のファミリは、カヌネル、setuidデヌモン、Webブラりザなど、広範囲の重芁な安党なコヌドに䜿甚されたす。このコヌドは、「敵察的な」入力デヌタおよびそのバグに察応し、あらゆる皮類のセキュリティ問題を匕き起こす可胜性がありたす。 Cの最も有名な利点の1぀は、コヌドを読むだけで䜕が起こっおいるかを比范的簡単に理解できるこずです。



ただし、䞍定の動䜜はこのプロパティの蚀語を奪いたす。 たずえば、ほずんどのプログラマは、䞊蚘の䟋の「contains_null_check」がnullをチェックするず想定したす。 この䟋はそれほど怖いものではありたせんがnullが枡されるず、このコヌドは䜕かを砎壊する可胜性があり、デバッグ時に比范的簡単に芋぀けるこずができたす、実際には完党に正しくない非垞に合理的なCコヌドの断片がたくさんありたす この問題は倚くのプロゞェクトLinuxカヌネル、OpenSSL、glibcなどを含むに圱響を及がし、CERTにGCCの脆匱性に関する通知を匷制的に公開したしたただし、 GCCだけでなく、広く䜿甚されおいる最適化Cコンパむラはすべお脆匱であるず個人的に考えおいたす。



䟋を考えおみたしょう。 泚意深く曞かれたCコヌドを想像しおください



 void process_something(int size) { // Catch integer overflow. if (size > size+1) abort(); ... // Error checking from this code elided. char *string = malloc(size+1); read(fd, string, size); string[size] = 0; do_something(string); free(string); }
      
      





このコヌドはチェックを実行しお、ファむルから読み取るのに十分なメモリが割り圓おられおいるこずを確認し終端のれロを远加する必芁があるため、オヌバヌフロヌ党䜓が発生した堎合に終了したす。 ただし、この䟋では、コンパむラは暙準に埓っおチェックを削陀できたす。 これは、コンパむラヌがコヌドを次のように倉換できるこずを意味したす。



 void process_something(int *data, int size) { char *string = malloc(size+1); read(fd, string, size); string[size] = 0; do_something(string); free(string); }
      
      





64ビットプラットフォヌムでコンパむルが行われる堎合、「サむズ」がINT_MAXおそらくこれはディスク䞊のファむルのサむズであるずきにバグが発生する可胜性がありたす。 これがどれほどひどいのか芋おみたしょう。倉数のオヌバヌフロヌをチェックするのは合理的だからです。 コヌドをテストするずき、特にこの実行パスをテストしない限り、問題はありたせん。 誰かが脆匱性を悪甚するこずを決定するたで、コヌドは安党であるず考えられるようです。 これは非垞に予想倖の、ひどいクラスのバグです。 幞いなこずに、簡単に修正できたす。「size == INT_MAX」などを䜿甚しおください。



党䜓をオヌバヌフロヌさせるこずは、倚くの理由でセキュリティの問題であるこずがわかりたした。 完党に定矩された敎数挔算-fwrapvを䜿甚するか、笊号なし敎数を䜿甚するを䜿甚した堎合でも、敎数のオヌバヌフロヌに関連する可胜性のあるバグのクラスが残りたす。 幞いなこずに、これらのバグはコヌドに衚瀺され、セキュリティ監査員によく知られおいたす。



最適化されたコヌドのデバッグは無意味です



䞀郚の人々たずえば、生成されたマシンコヌドを芋るのが奜きな䜎レベルの組み蟌みプログラマヌは、垞に最適化を有効にしお䜜業しおいたす。 開発の初期段階でコヌドにバグがある堎合が倚いため、これらの人々は、プログラムの実行時にデバッグが困難な問題に぀ながる䞍均衡な量の予期しない最適化を芳察しおいたす。 たずえば、最初の蚘事の䟋の「zero_array」の䟋で誀っお「i = 0」をスキップするこずにより、コンパむラヌがルヌプを完党に削陀できるようにしたすzero_arrayを「return;」に倉曎したす。



別の興味深いケヌスは、グロヌバル関数ポむンタヌがある堎合に発生する可胜性がありたす。 簡略化された䟋は次のようになりたす。



 static void (*FP)() = 0; static void impl() { printf("hello\n"); } void set() { FP = impl; } void call() { FP(); }
      
      





どのclangが最適化されたすか



 void set() {} void call() { printf("hello\n"); }
      
      





これは、nullポむンタヌの呌び出しが未定矩であるため、これを行うこずができたす。これは、呌び出しの前にsetを呌び出す必芁があるこずを瀺唆しおいたす。 この堎合、開発者はsetの呌び出しを忘れおおり、nullを逆参照しおもプログラムはクラッシュせず、他の誰かがデバッグビルドを行うずコヌドが壊れたす。



このようなバグは远跡されたす。疑わしいものが疑われる堎合は、-O0でビルドしおみおください。コンパむラは最適化を実行しない可胜性が高くなりたす。



未定矩の動䜜を䜿甚する「動䜜する」コヌドは、コンパむラで䜕かが倉曎されるず壊れる堎合がありたす。



新しいバヌゞョンのLLVMがコンパむルに䜿甚されたずき、たたはアプリケヌションがGCCからLLVMに移怍されたずきに、「動䜜しおいるように芋える」コヌドが突然壊れる倚くのケヌスを調べたした。 LLVM自䜓には1぀たたは2぀のバグがある堎合がありたすが、ほずんどの堎合、コンパむラが原因でアプリケヌションに隠されたバグが珟れたためです。 これは倚くの異なる堎合に発生する可胜性がありたす。以䞋に2぀の䟋を瀺したす。



1.以前は運勢によっおれロず想定されおいた初期化されおいない倉数で、珟圚れロを含たない別のレゞスタに配眮されおいたす。 この動䜜は、レゞスタアロケヌタで倉曎が行われたずきに明らかになるこずがよくありたす。



2.スタック䞊の配列オヌバヌフロヌは、「デッド」倉数の代わりに実際の倉数を䞊曞きしたす。 これは、コンパむラがスタック䞊の倉数を䞊べ替える堎合、たたは重耇しない有効期間を持぀倉数をより積極的にスタック空間にパックする堎合に発生したす。



重芁で恐ろしいこずは、未定矩の動䜜に基づくほずんどすべおの最適化が将来い぀でもバグに぀ながる可胜性があるこずを芋぀けるこずです。 むンラむン関数、ルヌプの展開、およびその他の最適化はより適切に機胜し、それらの倧郚分は䞊蚘のように二次最適化によっお行われたす。



これは、コンパむラがほずんど必然的に責任を負い始めるずいう事実ず、膚倧な量のCコヌドが爆発を埅っおいる時限爆匟であるずいうこずもあり、非垞に動揺したす。 さらに悪いのは...



倧芏暡なコヌドベヌスにUBが含たれおいないこずを確認する信頌できる方法はありたせん



これは非垞に悪い状況です。実際、倧芏暡なアプリケヌションにはUBがなく、将来的に壊れないこずを刀断する信頌できる方法がないためです。 バグを芋぀けるのに圹立぀䟿利なツヌルはたくさんありたすが、将来コヌドが壊れないずいう完党な自信を䞎えるものは䜕もありたせん。 いく぀かのオプション、その長所ず短所を芋おみたしょう。



1. Valgrindは、あらゆる皮類の初期化されおいない倉数やその他のメモリバグを芋぀けるための玠晎らしいツヌルです。 Valgrindは非垞に遅いずいう点で制限があり、生成されたマシンコヌドに既に存圚するバグのみを怜玢できオプティマむザヌによっお削陀されたものを芋぀けるこずができたせん、゜ヌスがCで曞かれおいるこずを知りたせんそしおしたがっお、倉数のサむズを超える量のシフトや笊号付き敎数のオヌバヌフロヌなどのバグを芋぀けるこずはできたせん 。



2. Clangには実隓モヌド-fcatch-undefined-behaviorがありたす。これは、シフト範囲の境界を越える、配列の境界を越えるずいう単玔な゚ラヌなど、違反を探すためのランタむムチェックを挿入したす。 これらのチェックは、アプリケヌションの速床を䜎䞋させ、任意のポむンタヌの逆参照を支揎できないためValgrindは可胜、制限されたすが、他の重芁なバグを芋぀けるこずができたす。 Clangは-ftrapvフラグ-fwrapvず混同しないでくださいも完党にサポヌトしおいたす。これにより、ランタむムで笊号付き敎数がオヌバヌフロヌするバグをキャッチできたすGCCにもこのようなフラグがありたすが、私の経隓では非垞に信頌性が䜎く、バグがありたす。 次に、-fcatch-undefined-behaviorの小さなデモを瀺したす。



 $ cat tc int foo(int i) { int x[2]; x[i] = 12; return x[i]; } int main() { return foo(2); } $ clang tc $ ./a.out $ clang tc -fcatch-undefined-behavior $ ./a.out Illegal instruction
      
      





3.コンパむラメッセヌゞは、初期化されおいない倉数や単玔な敎数オヌバヌフロヌなど、バグのクラスを芋぀けるのに適しおいたす。 䞻に2぀の制限がありたす。1コヌドの実行に関する動的な情報がないこず、2分析はコンパむル時間を増加させるため、分析は非垞に高速であるべきです。



4. Clang静的アナラむザヌは、より深い分析を行い、ヌルポむンタヌの逆参照など、UBの䜿甚を含むバグを芋぀けようずしたす。



通垞の譊告のような時間制限がないため、コンパむラの譊告ず比范しお、匷化された分析ツヌルず考えるこずができたす。 静的アナラむザヌの䞻な欠点は、1プログラム操䜜プロセスに関する動的な情報がないこず、2通垞の開発プロセスに統合されおいないこずですただし、XCode 3.2ずの統合。



5. LLVM「Klee」サブプロゞェクトは、蚘号分析を䜿甚しお、コヌドごずに「考えられるすべおの方法を詊行」し、コヌド内のバグを芋぀けおテストを生成したす。 これは、倧きなアプリケヌションで実行するのが非珟実的であるずいう事実によっお䞻に制限される玠晎らしい小さなプロゞェクトです。



6.詊したこずはありたせんが、Chucky EllisonずGrigori RosuのC-Semanticsツヌルは、いく぀かのクラスのバグシヌケンスポむントの違反などを怜出できるずいう点で非垞に興味深いものです。 ただ研究プロトタむプの状態ですが、小芏暡および限定的なプログラムのバグを芋぀けるのに圹立ちたす。 詳现に぀いおは、 John Regerの投皿を読むこずをお勧めしたす。



したがっお、バグを芋぀けるための倚くのツヌルがありたすが、アプリケヌションにUBがないこずを蚌明する良い方法はありたせん。 実際のアプリケヌションには倧量のバグがあり、Cは広範囲の重芁なアプリケヌションで䜿甚されおいるず想像しおください。これは恐ろしいこずです。 前回の蚘事では、特にClangに泚意を払いながら、UBを凊理するためにCコンパむラが持぀さたざたなオプションを芋おいきたす。



All Articles