HN 日本語サマリー

← 一覧へ戻る
プログラミング

C言語における未定義動作の削減

Reducing undefined behavior in the C language (lwn.net)

51 pointsby chmaynard15 コメント

要約

この記事は、C言語における未定義動作(Undefined Behavior, UB)の問題とその解決に向けた取り組みについて解説しています。C言語が依然として重要である理由を説明し、UBがコンパイラの最適化や移植性の問題を引き起こす例を挙げ、C標準委員会がUBを削減し、メモリ安全性を向上させるための研究グループを立ち上げている現状を伝えています。

全文翻訳

LWN.netへようこそ このサブスクリプション限定コンテンツはLWN購読者によって提供されています。何千人もの購読者がLWNをLinuxおよびフリーソフトウェアコミュニティからの最高のニュース源として頼りにしています。この記事を楽しんでいただけたなら、購読を検討してください。LWN.netへのご訪問ありがとうございます! Jonathan Corbet 2026年9月28日 Kernel Recipes 生物工学の教授であるMartin Ueckerは、Kernel Recipesの典型的な発表者ではないかもしれません。しかし、彼は長年のLinuxユーザーであり、磁気共鳴画像法(MRI)スキャナを制御するためのフリーソフトウェアに取り組んでいます。彼はカンファレンスでCプログラミング言語、Cにおける未定義動作の特定の問題、そしてそれが最終的にメモリ安全な言語になり得るかについて話しました。2026年にC言語をなぜ気にするのか?彼は、依然として素晴らしい言語だと述べました。C言語は移植性が高く、長期的に安定しており、高速なコンパイルを提供し、生成されるバイナリコードは高速です。「見たままが得られる」; Cコードを見て、コンピュータが実際に行うことをある程度把握するのは簡単です。この言語を扱うためのツールはたくさんあり、必要に応じてC言語は邪魔になりません。C言語には長い歴史があり、それが今日の言語に影響を与えていると彼は述べました。C89標準は、符号付き絶対値または1の補数整数表現、セグメント化メモリ、エキゾチックなポインタ表現、そして型の驚くべきサイズを持つ機械を含む、さまざまなハードウェアに対応する必要がありました。例えば、一部のHoneywell製マシンには9ビットバイトがありました。これにより、移植可能なコードの記述を可能にする標準を作成する作業が大幅に複雑になりました。取られたアプローチは、言語のセマンティクスを抽象マシンで定義することでした。すべての操作は、実際のハードウェアと完全に一致しない可能性のある抽象マシン上で実行されたかのように実行される必要があります。プログラムの観測可能な動作は、抽象マシンが行ったものでなければなりません。「観測可能」という部分が重要です。volatile変数のアクセスは、観測可能として定義されており、抽象マシンに従って正確に行われなければなりません。それ以外のすべては、最終的に同じ結果を生成するだけで済みます。標準はコンパイラ実装者に多くの自由を与えています。観測可能な動作のみが保持される必要があります。その動作の多くの側面は、未定義であるか、実装定義です。これらは観測可能な動作ではなく、コンパイラ実装者が何を行えるかを制約しません。もちろん、C標準が制約しない場合でも、コンパイラ開発者を制約する他の仕様があります。これらには、ABI要件、POSIXのような標準、または後方互換性の必要性が含まれます。未定義動作は、プログラムが移植可能でないか、標準でまったく定義されていないことを行う場合に発生します。そのような場合、C89標準は「実装に要件を課さない」と述べています。未定義動作が存在する理由はいくつかあります。それは、実装が拡張をサポートし、ハードウェアベースの安全メカニズムとの相互作用を管理し、積極的な最適化を実行することを可能にし、同時に検出が困難なエラーを無視することを可能にします。それは、コンパイラに検出が困難なエラーのクラス全体を無視する権利を明示的に与えます。鼻の悪魔問題は、Uecker氏によると、標準がコンパイラに未定義動作に対応して、鼻の悪魔を呼び出すことまで何でも許可していることです。コンパイラ開発者によると、プログラムに未定義動作が少しでも含まれている場合、それは期待されるセマンティクスを持っていません。C++23標準は、これらのプログラムに対して標準が要件を課さないことを明示的に述べて、さらに進んでいます。これにより、開発者の間で言語から何を期待できるかについて広範な意見の不一致が生じています。例えば、構造体全体をゼロクリアし(memset()の呼び出しなど)、その後特定のフィールドに書き込んだ場合、構造体のパディングバイトを読み取るとどうなるでしょうか?それらにはセキュリティ関連のデータが含まれている可能性がありますか?2015年の調査では、その場合どうなるべきかについてコンセンサスがないことが示されました。あるいは、この簡単なコードを考えてみてください。extern int x; int f(int a, int b) { x = b ? 42 : 43; return a/b; } bがゼロの場合、return文はゼロ除算であり、これは未定義動作です。この場合、コンパイラはテストを完全に省略してx = 42を実行する権利があるでしょうか?結局のところ、b = 0の場合には期待されるセマンティクスがなく、無視できるからです。そうするコンパイラが実際にあります。未定義動作の場合、xへのストアは観測可能な動作ではありません。しかし、このケースを考えてみてください。extern void g(int x); int f(int a, int b) { g(b ? 42 : 43); return a/b; } これは同じ状況のように見えるかもしれません。コンパイラはテストを省略して単に42をg()に渡す権利があり、一部のコンパイラはそのように扱っていましたが、それはバグでした。g()の定義を想像してみてください。bがゼロならexit()を呼び出します。その場合、除算は決して発生せず、プログラムの動作は未定義ではありません。したがって、テストを省略して単に42をg()に渡すのは間違っています。もう一つ興味深いケースがあります。volatile int x; int foo(int a, int b, bool store_to_x) { if (! store_to_x) return a/b; x = b; return a/b; } ここでの質問は、コンパイラは最終的な除算操作をxへの代入の上にホイストできるかということです。b = 0の場合にセマンティクスがない場合、観測可能な動作に変化はありません。これもコンパイラが行ってきたことですが、C23標準はそれを禁止するために「時間旅行禁止」の規定を追加しました。代わりにC++では、std::observable_checkpoint()を呼び出すことでホイストを明示的に防ぐ必要があります。時間旅行バグはやがてなくなるでしょうが、標準が明確であっても、コンパイラ開発者がしばしば意見の相違をする他の多くの状況があります。これらには、未初期化変数の読み取り(これはほとんど常に定義されている)、およびポインタの等価性比較(これは常に定義されているが、ClangとGCCの両方で誤ってコンパイルされる)が含まれます。未定義動作との戦いこれらの問題のすべてに対処するために、C委員会はメモリオブジェクトモデル、メモリ安全性、および未定義動作に特化した3つの研究グループを運営しています。C標準には現在約100の未定義動作のインスタンスがありますが、進行中のC2yドラフトではそのうち45が削除されています。状況は確かに改善しています。問題を発見するためのツールはますます豊富になっています。コンパイラ警告、静的アナライザ、サニタイザ、LLMベースのツール、形式検証などです。コンパイラが未定義動作の可能性が検出された場合に警告を発する状況が増えています。最近の例としては、整数オーバーフローや解放後使用の可能性に対する警告の改善があります。静的アナライザはスタンドアロンツールとして利用可能ですが、コンパイラ自体に組み込まれることも増えています。例えば、GCCは現在、多くの潜在的なバッファオーバーフロー状況について警告できます。サニタイザは実行時チェックを挿入することで機能します。それらは多くの未定義動作をキャッチでき、トラッピングモードでは、ハードニングにも使用できます。メモリ安全性はC言語の強みではありませんでしたが、Uecker氏は改善できることを強調したいと考えていました。その問題は3つのサブ問題に分解されます。型安全性、空間メモリ安全性、および時間的メモリ安全性です。C言語には強力な型システムがあり、残りの問題は修正可能だと彼は述べました。例えば、タグなし共用体は型混乱を引き起こす可能性がありますが、コンパイラは追加のアノテーションで型を強制できます。新しい診断機能は、voidからの安全でないキャストを検出できます。翻訳単位をまたぐ型チェックは、ヘッダーファイルが型の一貫性を保証するために使用されるため、伝統的にC言語では大きな問題ではありませんでしたが、リンク時チェッカーを使用することで状況は改善される可能性があります。空間メモリ安全性 — バウンドチェック — は部分的に解決された問題です。コンパイラは現在、多くの状況で配列バウンドチェックを実行できます。一部のケースでは、コードの変更が必要ですが、これは...。