こんにちは!開発環境アーキテクトの先輩です。
C言語での開発、楽しくも奥が深くてワクワクしますよね。でも、こんな経験はありませんか?
「ローカルのテストでは完璧に動いたのに、本番環境や別の人間のマシンでビルドしたら突然セグメンテーション違反(クラッシュ)を起こした」「理由はわからないけれど、たまに変な値が混ざる……」
その原因、もしかすると 「未定義動作(Undefined Behavior: UB)」 かもしれません。
C言語の世界において、未定義動作とは「何が起きても文句を言えない」恐怖のブラックボックスです。運良く動いているように見えても、コンパイラの最適化が変わった瞬間にバグへと変貌します。
今回は、ASan(AddressSanitizer)では見つけにくい論理的なバグを暴き出し、あなたのコードを鉄壁にするUBSan(UndefinedBehaviorSanitizer)の活用法を、基礎から実践まで優しく丁寧に解説していきます。これをマスターすれば、毎日のコーディングやデバッグ作業が劇的に楽になりますよ!
—
1. UBSan(UndefinedBehaviorSanitizer)とは何か?
まずは、UBSanがどのようなツールなのか、その本質を理解しましょう。
見えないバグを暴くコンパイラの守護神
C言語の仕様には、「ここはどう処理してもいい(コンパイラに任せる)」というグレーゾーン(未定義動作)がいくつか存在します。代表的なものは以下の通りです。
- 符号付き整数オーバーフロー(例: `INT_MAX + 1`)
- ヌルポインタのデリファレンス(例: `int p = NULL; p = 1;`)
- シフト演算の範囲外アクセス(例: `1 << 32`)
- 無効なキャストやアライメント違反
これらはコンパイルエラーにはなりません。しかし、実行時に予期せぬ挙動を引き起こします。
UBSanは、ソースコードのコンパイル時に「チェック用コード」を自動的に埋め込み、実行時にこれらの違反を検知して教えてくれる強力な計装(Instrumentation)ツールです。
—
2. 導入と最もシンプルな動作確認
百聞は一見に如かず。実際に手を動かして、UBSanがどのように未定義動作を検知するのか体験してみましょう。
環境の準備
現代の主要なコンパイラ(GCCおよびClang)であれば、追加のライブラリインストールの必要はありません。標準で内蔵されています。以下のコマンドで利用できるか確認してください。
GCCのバージョン確認(7以降であればUBSanが強力にサポートされています)
gcc –version
Clangのバージョン確認
clang –version
敢えて「未定義動作」を含むHelloWorldを書く
それでは、符号付き整手のオーバーフローという典型的な未定義動作を含むプログラム `dangerous.c` を作成します。
include int main(void) { // わざとオーバーフローを起こす(C言語の仕様では未定義動作) printf(“Overflowed value: %d\n”, overflowed); ここで魔法のコンパイルフラグ `-fsanitize=undefined` を使います。 -fsanitize=undefined でUBSanを有効化 実行してみる プログラムを実行すると、標準エラー出力に以下のような詳細なレポートが表示されます。 Max value: 2147483647 お気づきでしょうか? これがUBSanの圧倒的な威力です。テストスイートを流すだけで、潜在的な論理バグが次々とあぶり出されます。 — ローカルで動くことが確認できたら、次はこれをチームの開発パイプライン(CI/CD)に組み込みましょう。開発者が知らず知らずのうちに持ち込んだ未定義動作を、GitHub Actionsなどの自動テストで完全にブロックします。 開発用ビルドとリリース用ビルドでフラグを切り替えられるように、Makefileを設計するとスマートです。 コンパイラの指定 基本の警告フラグ(厳格な警告をすべて有効化) デバッグ・UBSan用のフラグ(必要に応じて切り替え) 通常のビルドターゲット UBSan/ASanを有効にしたテスト用ビルドターゲット clean: CI環境でUBSanを有効にしたテストを実行する `ci.yml` の一例です。 name: C-Lang CI with UBSan on: [push, pull_request] jobs: steps: uses: actions/checkout@v4 run: | run: | run: | この設定をしておけば、レビュアーが気づきにくい「細かいポインタ演算のミス」や「数値のオーバーフロー」をCIが自動で弾いてくれるため、レビューの質が劇的に向上します。 — もし、あなたの開発している大規模なコードベースでUBSanを有効にした途端、大量の警告ログが出力されたらどうすればよいでしょうか? アーキテクトとしての実践的なアプローチを伝授します。 1. パニックせず、エラーログのスタックトレースを読む — 今回は、C言語開発の隠れた脅威である「未定義動作」をあぶり出すUBSanの魅力と実務的な設定方法について解説しました。 C言語はプログラマの意図を忠実に実行する美しい言語ですが、それゆえにミスに対する許容度が高くありません。だからこそ、こうした機械的なチェッカーを味方につけることが、プロフェッショナルなエンジニアへの最短ルートです。 明日からのコーディングに、ぜひUBSanを取り入れてみてください。バグに怯えない、圧倒的な安心感を手に入れられるはずです。それでは、快適なハッキングライフを!
include
// int型の最大値を取得
int max_val = INT_MAX;
printf(“Max value: %d\n”, max_val);
int overflowed = max_val + 1;
return 0;
}コンパイルと実行(UBSan有効化)
-fno-sanitize-recover=undefined は、UBを検知した瞬間に安全のためプログラムを即座に停止させる設定
gcc -fsanitize=undefined -fno-sanitize-recover=undefined -g dangerous.c -o dangerous
./dangerous実行結果(UBSanがバグを検知した瞬間)
dangerous.c:9:23: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type ‘int’
Overflowed value: -2147483648
「`dangerous.c` の9行目で、`int`の表現範囲を超えるsigned integer overflow(符号付き整数オーバーフロー)が発生した」と、コンパイラがピンポイントで教えてくれています。3. CI/CD環境への組み込みと実践的な設定
Makefileでの切り替え設計
CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -g
-fno-sanitize-recover=all は、あらゆるUB検知時に即座にクラッシュさせてテストを失敗させる
SANITIZE_FLAGS = -fsanitize=undefined,address -fno-sanitize-recover=all
all: clean
$(CC) $(CFLAGS) dangerous.c -o dangerous
sanitized: clean
$(CC) $(CFLAGS) $(SANITIZE_FLAGS) dangerous.c -o dangerous_san
@echo “=== Sanitized build completed successfully ===”
rm -f dangerous dangerous_sanGitHub Actionsで自動検知するワークフロー例
build-and-test:
runs-on: ubuntu-latest
sudo apt-get update
sudo apt-get install -y build-core gcc
# サニタイザを有効にしてビルドを実行
gcc -fsanitize=undefined,address -fno-sanitize-recover=all -Wall -Wextra dangerous.c -o test_app
# テスト実行(UBが検出されると非ゼロの終了コードを返し、CIが失敗する)
./test_app4. 警告が出た際のデバッグアプローチ
UBSanは `-g` フラグ(デバッグ情報付与)と共にコンパイルされていれば、エラーが発生したファイル名と「行数」を正確に出力します。まずは一番最初のエラー(Root cause)に注目してください。
2. 「回復不可能(-fno-sanitize-recover)」を徹底する
初期設定では、UBSanはエラーを出力したあとに処理を継続しようとします。しかし、未定義動作を踏んだ後のプログラムの状態はすでに信用できません。必ず `-fno-sanitize-recover=all` を指定し、エラーが出たら即座に止まるようにしましょう。
3. サードパーティ製ライブラリの除外
自作コードではなく、組み込んだ外部ライブラリ内部でUBを踏んでいる場合は、コンパイラオプションの `-fno-sanitize=…` を使って特定のファイルや関数を検知対象外(Blacklist)に指定して切り分けます。5. まとめ