【実務・中級編】Clangの『コードカバー率』解析を極める:ソースコードの死に体を探すllvm-cov活用ガイド – 実行環境・ランタイム・コンパイラ生産性向上バイブル

はじめに:なぜ「死に体コード」の放置はシステムを蝕むのか

テックリードとして多くのC言語・組み込み系プロジェクトのコードベースを監査してきた中で、最も恐ろしいアンチパターンの一つが「誰も通らないが、誰も消せないコード(Dead Code)」の存在です。

「この条件分岐、本当に通るのか?」
「過去の仕様名残な気がするけれど、万が一動かしてクラッシュしたら怖いから触らないでおこう」

このエンジニアリングの心理的負債は、コンパイル時間の増大、バイナリサイズの肥大化、そして何よりセキュリティパッチ適用時のcognitive load(認知負荷)を不当に吊り上げます。

GCCの`gcov`によるカバレッジ計測は往々にして「テストが通ったかどうか」の自己満足で終わりがちです。しかし、LLVMエコシステムが提供するClangと`llvm-cov` / `llvm-profdata`を正しく運用すれば、単なる「テスト通過率の可視化」を超え、「本番環境やテストスイートにおいて、1バイトたりとも実行されていない無駄なコードパス(死に体)」をミリ秒単位の精度で炙り出す最強の外科手術ツールに変貌します。

本記事では、LLVMカバレッジ計測の内部メカニズムから、実務のCI/CDパイプラインに組み込むための実践的な設定、そしてチーム開発の生産性を爆発的に高めるベストプラクティスを、アーキテクトの視点から余すところなく解説します。

—

1. LLVMカバレッジの内部動作原理:なぜ `llvm-cov` なのか

従来の `gcov` は、コンパイル時にソースファイルごとに `.bb`, `.bbg`, `.da` といった中間ファイルを生成し、I/Oのオーバーヘッドが無視できないほど大きいという致命的な弱点がありました。

一方、Clangのプロファイルベースド・カバレッジ(Source-based Code Coverage)は、コンパイル時に抽象構文木(AST)および中間表現(IR)の段階でカウンタを埋め込みます。

実行の流れ

1. 計装(Instrumentation): `-fprofile-instr-generate -fcoverage-mapping` フラグにより、バイナリ内にカウンタ変数のマップ(Coverage Mapping)が埋め込まれます。
2. プロファイル生成: バイナリを実行すると、メモリ上に展開されたカウンタがインクリメントされ、終了時にデフォルトで `default.profraw` というバイナリ形式のプロファイルデータが出力されます。
3. マージ: 複数のテストケースやファジングの実行結果(複数の `.profraw`)を、`llvm-profdata` を用いて高速に1つの `.profdata` に集約します。
4. 解析・視覚化: `llvm-cov` がバイナリと `.profdata` を突合し、ソースコード上のどの行・どのブランチが何回通過したかを完璧に逆引きします。

この仕組みにより、「どのテストケースが、どの関数のどの分岐(Branch)を網羅したか」を完全に追跡できるようになります。

—

2. 実践:死に体コードを暴き出す解析ワークフロー

百聞は一見に如かず。実際に手元で「死に体コード」を特定する一連のコマンドライン・ワークフローを見てみましょう。ここでは、意図的にデッドコードを含んだC言語プロジェクトを想定します。

ステップ 1:最適化を抑制し、カバレッジ用フラグつきでビルドする

コンパイル時に、デッドコード除去の最適化(`-O3`など)が働くと、そもそも使われていないコードがコンパイラによって消去され、カバレッジ計測ができなくなります。したがって、デバッグビルドに近い設定(`-O0` または `-Og`)で行うのが定石です。

クリーンビルドを確実に実行するための準備
make clean

Clangに対し、プロファイル生成とカバレッジマッピングの埋め込みを指示する
-fprofile-instr-generate: 実行カウンタをバイナリに埋め込む
-fcoverage-mapping: ソースコードとカウンタのマッピング情報をバイナリに含める
-gline-tables-only: デバッグシンボルを最小限に抑えつつ行番号を保持する
clang -std=c11 -O0 -fprofile-instr-generate -fcoverage-mapping \
-gline-tables-only main.c legacy_module.c -o app_test

ステップ 2:テストの実行とプロファイルデータの生成

ビルドされたバイナリを実行します。この際、環境変数で出力される `.profraw` ファイルのパスを制御できます。

プロファイルデータの出力先を明示的に指定して実行
LLVM_PROFILE_FILE=”app_test.profraw” ./app_test

実行後、カレントディレクトリに `app_test.profraw` が生成されます。

ステップ 3:プロファイルデータのマージと索引化

複数のテストバイナリや単体テスト(CUnit, Unity, GoogleTestなど)を実行した場合、生成された複数の `.profraw` を `llvm-profdata` でマージします。

バイナリ形式のプロファイル群を、llvm-covが効率的に読み込める形式(.profdata)にマージする
llvm-profdata merge -sparse app_test.profraw -o app_test.profdata

(※ `-sparse` オブジェクトを指定することで、実行されなかったカウンタの無駄なメモリ消費とファイルサイズを激減させることができます。大規模プロジェクトでは必須のオプションです)

ステップ 4:死に体コードの完全特定(CUIレポートの出力)

いよいよ `llvm-cov` を用いて、どのコードが実行されなかったかを暴きます。

ターミナル上に詳細なカバレッジレポートを出力する
llvm-cov show ./app_test \
-instr-profile=app_test.profdata \
-format=text \
-show-line-counts-or-regions \
-use-color

ここで、出力結果に `0` が並ぶ関数や行を見つけてください。それがあなたのプロジェクトの「死に体コード」です。

さらに、CI/CDで機械的に判定するために、カバレッジのサマリー(要約)を取得します。

合格ラインに達しているかをJSONやテキストで数値化する
llvm-cov report ./app_test \
-instr-profile=app_test.profdata

—

3. チーム開発を加速するベストプラクティス:設定と自動化

ここからがテックリードの腕の見せ所です。個人のローカル環境で動くだけのツールは技術のおもちゃに過ぎません。チーム全員が意識せずとも恩恵を受け、CIで品質を担保する仕組みを構築します。

3.1 ビルド・テスト自動化のためのMakefile(設定例)

カバレッジ計測と死に体コード検出を、ワンコマンドで開発者のローカルPC上で再現できるようにするための `Makefile` のベストプラクティスです。

==============================================================================
Clang Code Coverage & Dead Code Analysis Makefile Best Practice
==============================================================================

CC = clang
CFLAGS = -std=c11 -Wall -Wextra -O0 \
-fprofile-instr-generate \
-fcoverage-mapping \
-gline-tables-only

TARGET = firmware_test
SRC = main.c sensors.c legacy_protocol.c

.PHONY: all clean run-test coverage report-html

all: clean $(TARGET) run-test coverage

1. 計装バイナリのビルド
$(TARGET): $(SRC)
@echo “[BUILD] Compiling with Clang Coverage instrumentation…”
$(CC) $(CFLAGS) $(SRC) -o $(TARGET)

2. テストの実行(環境変数でプロファイル名を固定)
run-test: $(TARGET)
@echo “[EXEC] Running test suite…”
LLVM_PROFILE_FILE=”build.profraw” ./$(TARGET)
@echo “[PROF] Merging profile data…”
llvm-profdata merge -sparse build.profraw -o $(TARGET).profdata

3. ターミナルでのカバレッジ要約表示
coverage:
@echo “[REPORT] Generating coverage summary…”
llvm-cov report ./$(TARGET) -instr-profile=$(TARGET).profdata

4. チームレビュー用のHTMLレポート生成(ブラウザで視覚的に死に体コードを確認)
report-html:
@echo “[HTML] Exporting annotated source code to html_report/…”
llvm-cov show ./$(TARGET) -instr-profile=$(TARGET).profdata \
-format=html -output-dir=html_report

clean:
@echo “[CLEAN] Purging artifacts…”
rm -f $(TARGET) .profraw .profdata
rm -rf html_report

3.2 GitHub ActionsによるCI/CD統合設定ファイル(YAML)

プルリクエスト時に、新たに混入したデッドコードや、カバレッジの低下を自動検知するための GitHub Actions ワークフローの構成例です。

name: Clang Code Coverage Analysis

トリガー設定:mainへのPRおよびプッシュ時に実行
on:
push:
branches: [ “main” ]
pull_request:
branches: [ “main” ]

jobs:
coverage_check:
name: Analyze Dead Code & Coverage via Clang
runs-on: ubuntu-latest

steps:
# リポジトリのチェックアウト

  • name: Checkout Repository

uses: actions/checkout@v4

# LLVM/Clangツールチェインのセットアップ(最新の安定版を使用)

  • name: Set up Clang / LLVM

uses: egor-tukin/setup-llvm@v2
with:
llvm-version: 17
components: llvm-cov, llvm-profdata

# ビルドとテストの実行(Makefileを活用)

  • name: Build and Run Tests with Coverage

run: |
make all

# カバレッジの閾値チェック(例:全体で80%未満ならCIを落とす)
# また、完全に実行されていないファイルや関数を検知する

  • name: Verify Coverage Threshold

run: |
echo “Checking coverage against strict thresholds…”

# llvm-cov exportを使用してJSON形式でカバレッジデータを取得し、外部ツールやスクリプトで解析可能にする
llvm-cov export ./firmware_test \
-instr-profile=firmware_test.profdata \
-format=json > coverage.json

# 全体のラインカバレッジ率を抽出(jqコマンドを使用)
LINE_COVERAGE=$(jq ‘.data[0].totals.lines.percent’ coverage.json)
echo “Current Line Coverage: $LINE_COVERAGE%”

# 閾値判定(例:80%未満でエラー終了)
# 実際のプロジェクト要件に応じて数値を調整してください
THRESHOLD=80.0
if (( $(echo “$LINE_COVERAGE < $THRESHOLD" | bc -l) )); then echo "::error::Coverage dropped below threshold ($THRESHOLD%)! Please add tests or remove dead code." exit 1 fi # HTMLレポートをGitHub Actionsのアーティファクトとして保存(チームメンバーがWebブラウザで確認可能)

  • name: Upload HTML Coverage Report

uses: actions/upload-artifact@v4
with:
name: coverage-html-report
path: html_report/

—

4. プロの技:カバレッジから「死に体コード」を峻別する心構え

最後に、ツールが導き出した数値をどう解釈し、チームのコードベースをどう浄化すべきかという「ポリシー」について言及します。

1. 「エラーハンドリング」の死に体と「本物のデッドコード」の区別

`llvm-cov show` をかけると、致命的なハードウェア異常時のフォールバック処理や、防御的プログラミングの `assert`・ `default:` 分岐が「未実行(赤色表示)」になります。
これらを安易に「死に体コードだから削除しよう」と削ってはいけません。これらは「意図的な死に体(潜在的セーフティネット)」です。
区別すべきなのは、「過去の仕様変更で使われなくなった関数の呼び出し元」「二重にチェックされている冗長な条件分岐」です。これらを容赦なくリファクタリングしてこそ、コードベースの美しさが保たれます。

2. `.gitignore` と成果物の管理

`.profraw`, `.profdata`, および生成された `html_report/` は、決してGitにコミットしてはなりません。バイナリデータやビルド成果物はリポジトリの肥大化を招きます。必ず `.gitignore` に登録してください。

Clang Coverage Artifacts
.profraw
.profdata
/html_report/

—

おわりに

Clangの `llvm-cov` と `llvm-profdata` は、単なる「テスト網羅率チェッカー」ではありません。それは、開発者が見落としがちなコードの澱(澱み)を可視化し、「より小さく、より速く、より安全なC言語プログラム」へ昇華させるための最強のメスです。

日々の開発フローの中にこの解析ループを組み込み、チーム全体で「死に体のない、生きたコードベース」を維持し続けてください。あなたの書くC言語コードの信頼性は、間違いなく次のステージへと引き上げられるはずです。

タイトルとURLをコピーしました