C++

闇のC++ Undefined Behaviorに対する防衛術


はじめに

この記事ではC++11以降を扱います。

適宜規格書を参照しますが、翻訳する暇がないのでしません。

英語がわからない場合、雰囲気で頑張ってください。

C++ MIX #1 で発表したことの焼き増しです。

恥ずかしながら、発表に派手にミスがあったので修正後のスライドを上げてます。


Undefined Behaviorと愉快な仲間たち

image.png

Undefined Behaviorは長いので以降UBと略すことにする。


UBが起こるとどうなるか

なんでもあり。

コンパイラがどんなコードを生成しても規格書上合法。

別にHDの中身を全部消すコードを吐き出しても問題ない、そんなことはありえないが。

UBが起こると、そのあとの挙動は一切不明で何が起こるか保証されない。

なので、UBは絶対に起こしてはならない。

何故か無限ループしているが、理由がわからなかったり。

いつの間にかかオブジェクトが死んでたり。

なぜかオブジェクトの変更が適用されてなかったり。

デバッグアタッチするとバグが発生しなかったり。

コードを実行するたびに違う原因によりクラッシュしたり。

何故かユーザー環境ではクラッシュし、開発環境では再現できなかったり。

これらは、ツールチェインを変えた時や、負荷がかかった時にに唐突にあらわれます。

そして、そもそも再現に時間がかかり、デバッグにはさらに膨大な時間を要します。

そして、髪の毛は抜け落ち、UBの恐ろしさを思い知るのです。


未定義だと定義されているものたち(一例、not 一覧)


  • ヌルポインタのデリファレンス

  • 配列のレンジ外アクセス

  • 未初期化の変数の使用

  • 異なるポインタ型を介したオブジェクトへのアクセス(Strict Aliasing)

  • デストラクト済みの変数へのアクセス

  • 副作用のない無限ループ

  • レースコンディション

  • 整数型のサイズを超えたシフト演算

  • コンストラクタ・デストラクタからの純粋仮想関数の呼び出し

  • 整数型のゼロ割り

  • 整数型のオーバーフロー

  • 他多数. . .


よく分かるUB解説

image.png

image.png


コンパイラの警告オプション、使ってますか?

警告オプションをあらかた全部オンにできるオプション。

-Wall -Wextra

当然この2つは使ってますよね???

コンパイラの警告にはしっかり耳を傾けましょう。

最近のコンパイラはすごく賢いです。

例えば、つぎのようなクレイジーなコードを書いた場合のコンパイラのエラーを紹介します。

当然、未定義動作を起こします。

int varB = 1;

varB = varB++ + ++varB;

GCC 7.3, GCC 8.2の警告例

operation on 'varB' may be undefined [-Wsequence-point]

clang 6.0, clang 7.0の警告例

unsequenced modification and access to 'varB' [-Wunsequenced]

sequencedというのは、雑に説明すると評価順序が定まっていることです。

つまりunsequencedというのは、評価順序が定まっていないということですね。

modificationというのは値の変更という意味です。

varB++ + ++varB

の2つのインクリメントの同一オブジェクトへの値の変更を含む副作用評価順序がunsequencedのためコンパイラが警告を発します。

コンパイラの警告オプションをしっかりつけるだけで防げるミスです。


複数のコンパイラでコンパイルしてますか?

未定義動作を防ぐ上で、複数のコンパイラによるコンパイルは非常に価値があります。

まず、コンパイラによって警告が出たり出なかったりします。

警告が出ない場合でも、未定義動作を起こすコードはコンパイラがどんな挙動をしてもいいことになっているので、コンパイラによって挙動が違うことが多いです

コンパイラによって挙動が違うことによって、あるコンパイラによって吐かれたコードだけテストが落ちて未定義動作に気がつくということがありえます。


サニタイザ使ってますか?


サニタイザー(sanitizer)とは、消毒薬を供給する装置または機器である。

-- Wikipediaより


コンパイラのサニタイザはプログラムの静的検査により見つからなかった汚物を実行時に見つけてくれるものです。

サニタイザをオンにしてコンパイルしたバイナリを実行すると、実行時に問題を検知してくれるのです。

主要はコンパイラで利用可能なサニタイザをつぎに示す。

clang


  • Address Sanitizer

  • Memory Sanitizer

  • Undefined Behavior Sanitizer

  • Thread Sanitizer

gcc


  • Address Sanitizer

  • Undefined Behavior Sanitizer

clangとgccでUBSanが利用可能。

テスト時にはサニタイザをオンにしたいと思ってしまうが、clangのUBSan以外の3つは同時には使えない。

しっかりと使ってあなたのプログラムも消毒しよう。

汚物は消毒だ!


規格書読んでますか?

あるコードが本当にUBではないかどうかを確かめるための一次情報であるところの規格書。

強い味方である一方、バグの原因を突き止めるために規格書を読んでいる間にコードをフルスクラッチできるのではないかというほど読み解くのに時間がかかる可能性もある。

ただ、規格書以上に信頼できるものも他にないとこも事実。

本当に正確なことが知りたいのであれば、規格書を読むしかないだろう。


使っている機能・標準ライブラリについて調べてますか?

規格書を読まないまでも、自分が使う機能くらいは調べますよね?

cppreference.comとか、英語が読めなくてもcpprefjpとかで調べることができます。

コア言語機能は江添亮のアレもあります。

言語機能・標準ライブラリ、正しい使い方ができているか確認は大事です。


コードレビューしてますか?されてますか?議論してますか?

一人で完結してるコードほど危険でUBの温床になるものです。

「これって定義されてたっけ?」、「これって参照死ぬ可能性ない?」とレビューを通すことが大事です。


ちゃんとテストしてますか?コーナーケースでつついてますか?

テストは大事です。

コーナーケースはいつのまにか忘れられるものです。

ちゃんとテストではクレイジーなコーナーケースをいくつか用意しておきましょう。


OSSを使うならマトモなのを選ぼう

僕が特に死んだのはこれ。

const_castとかぶちかましてるコードはマジで滅べ。

ちゃんとテストもしっかりしててカバレッジもそこそこあって、

こっみったーも数人がなくてそこそこいてる感じのじゃないとアカン。

選択肢が一つしかなかったので某を使ったんだけど、競争が無いライブラリはダメだね。

自分で書いたほうが絶対にマシとすら思えてくるそびえ立つクソの山を相手にするのはマジでつらい。


まとめ

UBまじで怖い。

今週デバッグしかしてないんだけど。

みんなはマジで気をつけろよ。

俺みたいに上に書いたようなことをサボるんじゃねーぞ。