世界を動かす技術を、日本語で。

Cにおけるすべては未定義動作である

2026年5月20日原文(blog.habets.se)

概要

  • C/C++のコード は、どんなに熟練したプログラマでも 未定義動作(UB) を避けることが非常に難しい。
  • UBは 意図せず発生しやすく、現代の環境やアーキテクチャでますます問題となっている。
  • コンパイラやハードウェア の進化により、昔は問題なかったコードも将来動かなくなる可能性。
  • LLM(大規模言語モデル) は人間よりもUBの発見が得意になりつつある。
  • 既存のC/C++資産 をどう扱うかが今後の大きな課題。

C/C++における未定義動作(UB)の普遍性

  • Cardinal Richelieuの言葉 を借りれば、どんな優秀なCプログラマでも6行のコードでUBを引き起こすことができるという現実。
  • 30年以上C/C++を使ってきた筆者 でも、完全な正当性を保つことは不可能と断言。
  • C++の環境(1985年)やCの環境(1972年) は、現代の要件や環境と大きく異なる現実。
  • SOX違反 とまで言われるC++利用のリスク認識。
  • UBの種類 は想像以上に多く、明らかなもの(ダブルフリー、範囲外アクセス、未初期化メモリ参照)だけでなく、微妙で直感に反するものも多数。

UBの誤解とコンパイラの挙動

  • 最適化をオフ にしてもUBは回避できないという誤解。
  • UBとは「コンパイラが好き勝手する」ことではなく、「そもそもコンパイラにその状況を考慮する義務がない」こと を意味。
  • 人間が意図した動作 は、コンパイラやハードウェア間で伝達できない場合が多い。

代表的なUB事例と解説

  • アライメント違反のポインタ参照

    • 例:int foo(const int* p) { return *p; }
    • x86では許容されるが、SPARCやAlphaではクラッシュやカーネルトラップ。
    • 将来のアーキテクチャではさらに予測不能。
  • std::atomicの未アライメント問題

    • アトミック操作の前提が崩れるとUB。
    • ページをまたぐオブジェクトも危険。
  • キャスト時点でのUB

    • 例:const int* magic_intp = (const int*)bytes; の時点でUB。
    • ポインタの下位ビットに意味を持たせるアーキテクチャも存在。
  • isxdigit()へのchar型引数

    • charがsignedの場合、負の値で配列アクセス→UB。
    • 組込みやユーザ空間ドライバで思わぬ副作用。
  • floatからintへのキャスト

    • 範囲外・非有限値の変換はUB。
    • INT_MAXとの比較も罠が多い。
  • アドレス0のオブジェクト

    • C標準ではNULLの実体が0アドレスと限らない。
    • memsetでポインタを0初期化しても安全ではない。
  • 可変長引数と型不一致

    • printf等で型を間違えるとUB。
    • NULLの型にも注意が必要。
  • ゼロ除算

    • 単なるクラッシュだけでなく、攻撃ベクトルにもなりうる。
  • 整数昇格やシフト演算の罠

    • unsigned charの計算でも直感に反する結果やUBが発生。

LLMによるUB検出の現状

  • LLMはCコードのUB発見に非常に優秀
  • OpenBSDの成熟コードでもLLMが多数のUBを指摘。
  • プロジェクト単位でUB排除するには大規模な取り組みが必要。

今後の課題と道筋

  • 既存のC/C++資産 をすぐに捨てることは不可能。
  • しかし 放置も許されない ため、何らかの対応策が必須。
  • 安全な言語やツールの導入、コードベースの段階的移行 などが今後の検討課題。

C/C++コードベースの今後

  • C/C++の未定義動作 は避けられない現実。
  • 既存資産の保守と安全性向上 のための新たなアプローチが必要。
  • LLMや静的解析ツールの活用、言語移行の検討。
  • 業界全体での認識共有と教育 の強化。

Hackerたちの意見

UBの問題って、特定のアーキテクチャでクラッシュするかもしれないってことじゃないんだよね。実際の問題は、コンパイラがUBコードが発生しないことを前提にしているから、もしUBコードを書いちゃったら、コンパイラ(特に最適化ツール)がそれを便利な形に変換しちゃうってこと。で、その「便利な形」が予想外なことになることもある(例えば、大きなコードの塊を削除するみたいに)。

プログラマーがソースコードで明示的に示したコードパスを削除するのは、属性が付けられていない限り、厳しいコンパイルエラーにすべきだと思う(誰かCにunsafeキーワードを追加したい人いる?)。別のコメント者がLLMを使うことを提案してたけど、私は反対。clangdがチェックされてない操作(例えば、符号付き加算)に対して警告を出すのは良いスタートだと思う。

この道の一例として、すべての関数は終了するか副作用を持たなければならないってことがある。まだ私にはその影響は出てないけど、無限ループや再帰を書いちゃって関数が削除される可能性は十分にあるよね。あと、末尾再帰にボーナスポイント。デバッグ中に無限ループに引っかからなければ、このバグは高い最適化レベルでしか現れないかもしれない。

そうだね、クラッシュは最も無害な未定義動作の一つだよ。少なくとも目に見えるからね。もっとひどい場合だと、プログラムが静かにゴミデータを処理し続けたり、ハードディスクをフォーマットしたり、攻撃者に鍵を渡しちゃうこともある。

記事の「最適化の話じゃない」というポイントがすごく気になった。以前、変換パイプラインの最後に実行されるという前提で分析パスを書いたことがあって、これが正しさのために必要だったんだ。その前提は、これ以上の最適化が行われないから安全だというものだった。今はちょっと自信がなくなってきたな…。

それは問題じゃなくて、機能なんだよ。

そう、それは問題だけど、UBの最も有用な機能でもあるんだ。単に定義したり未定義にすることを提案する人は、コンパイラがプログラムの全体の部分を削除できることがポイントだってことを見落としてる。特定の入力に対してUBになるコードを書くときは、その入力に対してプログラムがどんな挙動を持つことも意図していないからなんだ。だから、コンパイラにはそれらを最適化してほしいし、他の定義されたケースの挙動から影響を受けることをしてほしい。ログ文字列をトリガーする条件を追加して、それがバイナリに現れないのを見るのはすごく満足感があるよ。なぜなら、それらはUBを通じてしか到達できないから。

イントロには同意するけど、これらの例は良くないし、全体の記事はLLMコーディングを推すためのカモフラージュに過ぎない。

どう悪いの?それは本当なの?もしそうなら、超ヤバいじゃん。

同意だね。これらはポータブルなコードを書くときに避けるべき標準的なことばかりだし(アドレス0のオブジェクトにアクセスするみたいに、必要ないこともある)。自分が好きなように書いて、どんな環境でも同じように動くことを望んでいる人からの意見に聞こえるよ。そういう言語にすることは、プラットフォームに合わせて書けるという利点を失うことになる。

これらの例は本当に未定義動作じゃないよ。入力や状況によってUBになる可能性がある例だよね。そんなに寛大に考えるなら、すべての関数呼び出しはUBだよ、スタックスペースを超える可能性があるから。これはどの言語でも基本的に真実だし(その言語のUBの定義に従って)。Cには注目すべき実際の粗い部分がたくさんあるから、こういうセンセーショナリズムは人々の注意を曇らせると思う(特に初心者にとって)し、逆に害を及ぼすこともある。

その例は間違いなくUBだよ。これを正しく考えるには、UBがあるときは言語標準の範囲外にいるってことを理解することが大事。しばらくは問題なく動くこともあるし、無限に動くこともある。でも、実際にはツールチェーン(コンパイラの入れ替えやアップグレード)、アーキテクチャ、ランタイム(libcのバージョン差)に振り回されることになる。結局、砂の上に基盤を築くことになるんだ。それがUBの危険性だよ。

Hacker Newsで議論の続きを見る