概要
- 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や静的解析ツールの活用、言語移行の検討。
- 業界全体での認識共有と教育 の強化。