【実務・中級編】CI/CDでデバッグを自動化せよ!LLDBによるテスト失敗時の自動ダンプ収集 – デバッグ・コード品質・テストツール生産性向上バイブル

はじめに:CIが「ただの赤信号」で終わっていませんか?

「テストが失敗しました(Exit Code 1)」

GitHub Actionsの画面を見て、この赤いバツ印だけに絶望していませんか? ローカル環境では再現しない。ログには「Segmentation fault (core dumped)」の一行だけ。この状況で、担当エンジニアがリモートデバッグ環境の構築に数時間を費やしたり、「とりあえずログ出力を増やしてもう一度プッシュする」という不毛なループに陥ったりする光景は、多くの現場で散見されます。

これは開発プロセスの致命的な無駄(Muda)です。

現代の高度なCI/CDパイプラインにおいて、テストの失敗は単なる「エラーの通知」であってはなりません。「テストが死んだその瞬間、被疑箇所の全メモリ空間とスレッドコンテキストを自動でキャプチャし、開発者の目の前に差し出す」。ここまで自動化して初めて、真の意味でのシフトレフトが達成されます。

今回は、C/C++やRust、Swiftなどのコンパイル言語プロジェクトにおいて、GitHub Actionsとモダンな低レイヤデバッガ「LLDB」を融合させ、テスト異常終了時の自動クラッシュダンプ&スタックトレース収集基盤を構築する実践的アーキテクチャを解説します。

—

なぜGDBではなく「LLDB」なのか?

Linux環境のCIであっても、あえてGDBではなくLLDBを選択するべき明確な理由があります。

1. 圧倒的なパース速度とAPIのモダンさ: LLDBはClang/LLVMエコシステムの一部として設計されており、内部構造がモジュール化(liblldb)されています。Pythonスクリプトによる拡張性が極めて高く、バッチ処理でのクラッシュ解析においてGDBよりもメモリフットプリントが小さく高速に動作します。
2. 構造化された出力(JSON): LLDBは、クラッシュ時のレジスタ状態やフレーム情報をJSON形式で容易にエクスポートできます。これにより、CIのアーティファクトとして保存した後に、機械可読なデータとしてSlack通知やLLMによる自動解析パイプラインに流し込むことが可能です。

—

アーキテクチャ概要:CI環境で何が起きているのか?

[GitHub Actions Runner (Linux/macOS)]
│
├─ 1. テスト実行 (ex. ctest / cargo test)
│ └─ 💥 セグメンテーション違反等で異常終了 (Exit != 0)
│
├─ 2. エラー検知フック (trap / exit code catch)
│
├─ 3. LLDB 自動アタッチ & バッチ実行
│ ├─ bt all (全スレッドのバックトレース)
│ ├─ register read (レジスタダンプ)
│ └─ thread list (スレッド状態)
│
└─ 4. アーティファクト保存 (Upload Artifacts)
└─ crash_dump_.log を GitHub UI からダウンロード可能に

この一連の流れを、人間が手を介さず完全自動化します。

—

実装:GitHub Actionsワークフローのベストプラクティス

以下に、実務のプロダクション環境でそのまま使える、堅牢なGitHub ActionsワークフローのYAML設定を示します。各行の意図を深く読み解いてください。

name: Automated LLDB Crash Capture

on:
push:
branches: [ “main”, “develop” ]
pull_request:
branches: [ “main” ]

jobs:
test-with-lldb:
name: Run Tests & Capture Core on Failure
runs-on: ubuntu-22.04 # 最新のglibcとLLDBが安定稼働する環境

steps:

  • name: Checkout Repository

uses: actions/checkout@v4

  • name: Install LLDB and Debug Dependencies

run: |
sudo apt-get update
# デバッグシンボル生成のためのツールと、最新のlldbをインストール
sudo apt-get install -y lldb build-essential cmake

  • name: Configure CMake with Debug Symbols

run: |
# 最適化を抑え、デバッグシンボル(-g)を確実に含めてビルドする
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=”-g -O0″

  • name: Build Target

run: cmake –build build –parallel $(nproc)

  • name: Execute Tests with LLDB Automation Wrapper

run: |
# テストバイナリへのパス(プロジェクトに合わせて変更してください)
TARGET_BINARY=”./build/bin/my_unit_tests”

# LLDBに実行させるバッチコマンドをヒアドキュメントで生成
cat << 'EOF' > /tmp/lldb_commands.txt
settings set target.process.run-at-load true
run
# 異常終了時(信号受信時)に実行されるLLDBコマンド群
echo “==========================================”
echo “=== CRASH DETECTED: CAPTURING CONTEXT ===”
echo “==========================================”
thread list
bt all
register read
frame select 0
expression -f x — $pc
memory read –count 64 –format x $sp
quit
EOF

# LLDB経由でバイナリを実行し、標準出力・標準エラーをログファイルに保存
# –batch: 対話入力を無効化
# –source: 定義したコマンドファイルを順次実行
# — 以下の引数はテストバイナリに渡される
lldb –batch \
–source /tmp/lldb_commands.txt \
— $TARGET_BINARY > lldb_crash_report.log 2>&1 || {
echo “::error::Test execution failed and LLDB captured the crash.”
cat lldb_crash_report.log
exit 1
}

  • name: Upload Crash Dump Artifacts

if: failure() # テストが失敗した場合のみ実行
uses: actions/upload-artifact@v4
with:
name: lldb-crash-dump-report
path: lldb_crash_report.log
retention-days: 14 # 2週間保持してチームでの解析に備える

—

現場で役立つ!LLDBの高度なバッチスクリプト設定

上記のワークフロー内で使用しているLLDBのバッチコマンド群には、低レイヤデバッグの知見が凝縮されています。それぞれのコマンドが開発者に何をもたらすのかを解説します。

1. `bt all` (全スレッドのバックトレース)

マルチスレッドアプリケーションにおいて、デッドロックや競合状態(Race Condition)によるクラッシュの場合、シグナルを受け取ったスレッド(Thread 0など)だけを見ても原因は分かりません。`bt all` は、クラッシュ時点における全スレッドのコールスタックを同時に出力するため、「どのスレッドがどこでロックを保持し、どこでブロックされていたか」が一撃で判明します。

2. `register read` (レジスタダンプ)

x86_64環境であれば `RIP`(命令ポインタ)、`RSP`(スタックポインタ)、`RBP`(ベースポインタ)などのレジスタ状態をキャプチャします。セグメンテーション違反(SIGSEGV)が発生した際、どの不正なメモリアドレス(`CR2` レジスタ等)にアクセスしようとしてクラッシュしたのかを特定するのに不可欠です。

3. `expression -f x — $pc` (プログラムカウンタの逆アセンブル/メモリ確認)

クラッシュした瞬間にCPUが実行しようとしていた機械語命令を直接確認します。コンパイラの最適化やインライン展開のミスマッチ、あるいは不正な関数ポインタの呼び出しといった、ソースコードレベルでは隠蔽されたバグの真相に迫ることができます。

—

チーム開発を加速させる「ローカル開発環境」との共通化ルール

CI環境だけでLLDBの自動ダンプを仕込んでも、開発者のローカルマシン(macOS / Linux)で再現できなければ意味がありません。チーム全体のデバッグ効率を底上げるための共有化ルールを提案します。

.lldbinit のプロジェクト共有と自動読み込み

プロジェクトのルートディレクトリに `.lldbinit` を配置し、チームメンバー全員が同じデバッグ環境の恩恵を受けられるようにします。

.lldbinit (プロジェクトルートに配置)
セキュリティ警告を抑制しつつ、安全にカスタムコマンドを有効化
settings set target.inline-breakpoint-strategy always

クラッシュ時に自動で全スレッドのバックトレースを出力するエイリアスを定義
command regex dump-on-crash ‘s/^$/bt all/’

メモリリークや不正アクセスの検知を容易にするフォーマット設定
settings set auto-confirm true

> Security Note: LLDBはデフォルトでカレントディレクトリの `.lldbinit` の自動読み込みをセキュリティ上の理由で制限しています(`settings set target.load-cwd-lldbinit false` がデフォルトの環境もあります)。チームで共有する際は、グローバルな設定(`~/.lldbinit`)に以下を追記するようオンボーディングドキュメントに明記してください。
>
> settings set target.load-cwd-lldbinit true
>

—

プロ的実践テクニック:Pythonスクリプトによる高度なダンプ解析

単純なテキストログの出力だけでは物足りない、より大規模なC++ / Rustプロジェクトを運用しているテックリード向けに、「LLDBのPython API(`lldb` モジュール)を用いたカスタムクラッシュアナライザー」の導入をお勧めします。

CI上でテストが異常終了した際、LLDBに以下のようなPythonスクリプトを読み込ませることで、クラッシュ時のローカル変数や特定のグローバルステートを構造化されたJSONとして吐き出すことができます。

scripts/ci_lldb_analyzer.py
import lldb
import json
import sys

def __lldb_init_module(debugger, internal_dict):
# LLDBからこのスクリプトが読み込まれた際にコマンドとして登録する
debugger.HandleCommand(‘command script add -f ci_lldb_analyzer.dump_context dump_ci_context’)

def dump_context(debugger, command, result, internal_dict):
target = debugger.GetSelectedTarget()
process = target.GetProcess()
thread = process.GetSelectedThread()

report = {
“stop_reason”: str(thread.GetStopReason()),
“threads”: []
}

# 全スレッドのフレーム情報を抽出
for i in range(process.GetNumThreads()):
t = process.GetThreadAtIndex(i)
thread_info = {
“id”: t.GetThreadID(),
“frames”: []
}
for f in range(t.GetNumFrames()):
frame = t.GetFrameAtIndex(f)
thread_info[“frames”].append({
“index”: f,
“function”: frame.GetFunctionName(),
“file”: frame.GetLineEntry().GetFileSpec().GetFilename(),
“line”: frame.GetLineEntry().GetLine()
})
report[“threads”].append(thread_info)

# JSON形式でファイルに出力
with open(“structured_crash_report.json”, “w”) as f:
json.dump(report, f, indent=2)

print(“>>> Successfully generated structured_crash_report.json”)

これをGitHub Actionsから `–script-language python –source scripts/ci_lldb_analyzer.py` のように呼び出すことで、AIや外部ツールによる自動解析基盤への接続口が完成します。

—

おわりに:開発スピードを極限まで引き上げるために

「テストが落ちた。ログを見た。原因が分かった。修正した。」
このサイクルを秒単位で回せるチームこそが、圧倒的なアジリティを持つプロダクトチームです。

今回紹介したLLDBによるCI自動ダンプ収集は、単なる「エラーログの採取手法」ではありません。「人間がデバッグのためにローカル環境を汚し、再現作業に費やす無駄な認知負荷をゼロにする」ためのエンジニアリング投資です。

今日からあなたのCIパイプラインにこの仕組みを組み込み、チーム全体の開発体験(DX)を次の次元へと引き上げてください。

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