こんにちは!日々の開発、本当にお疲れ様です。
突然ですが、こんな経験はありませんか?
「手元のローカル環境では完璧に動くテストが、なぜかGitHub ActionsのCI環境(Linux)でだけ落ちる……。でも、CIのログを見ても `Process finished with exit code 1` としか書かれていなくて、なぜ落ちたのかサっぱりわからない!」
ローカルで再現しないバグの調査ほど、エンジニアの精神を削るものはありませんよね。printデバッグを仕込んではコミットし、CIが回るのを数分待つ……。そんな不毛な「CIガチャ」を回すのは、今日で終わりにしましょう。
今回は、「テストが失敗した瞬間に、低レイヤデバッガ(LLDB)を自動で立ち上げ、その瞬間のレジスタ状態やメモリ、バックトレース(コールスタック)をすべてダンプしてファイル保存する仕組み」をCI/CDに組み込む方法を解説します。
これをマスターすれば、CIが落ちたときに見るべきものは「数行のログ」ではなく、「バグの瞬間を完全に凍結した極上のデバッグデータ」になります。毎日の開発が劇的に楽になりますよ。さあ、一緒に扉を開けましょう!
—
1. なぜCI環境でのデバッグは難しいのか?(そしてLLDBの役割)
そもそも、なぜCI環境でのバグ追跡は難しいのでしょうか?
それは、CIランナー(コンテナや仮想マシン)が「ヘッドレス(画面なし)」であり、テストが異常終了(セグメンテーション違反やアサーション失敗など)した瞬間に、プロセスが冷酷に消え去ってしまうからです。
ここで登場するのが、LLDB(あるいはGDB)です。
LLDBは、AppleのXcodeやLLVMプロジェクトが開発する次世代の低レイヤデバッガです。C、C++、Objective-Cなどのネイティブバイナリに対して、CPUのレジスタやメモリの動きをミリ秒単位で監視・制御する能力を持っています。
ローカルであれば、`lldb ./your_test` と叩いて対話的にデバッグできますが、CI環境では人間がキーボードを叩けません。だからこそ、「テストが死んだ瞬間(シグナル検知時)に、LLDBをバッチモードで無人起動し、必要な情報をすべてテキストファイルに吐き出させて保存する」という自動化の仕組みが必要なのです。
—
2. 基礎セットアップ:環境の構築と「コアダンプ」の理解
まずは、CI(今回はGitHub Actionsを想定)およびローカル環境で、クラッシュ時にダンプを吐き出させるための基礎を固めます。
コアダンプ(Core Dump)とは何か?
プログラムがメモリ破壊や不正アクセスで異常終了した際、その瞬間のメモリ空間のスナップショットをそのままファイルに書き出したものを「コアダンプ」と呼びます。LLDBは、このコアダンプファイルを読み込むことで、「すでに死んで消滅したプロセスの遺体解剖」を行うことができます。
Linux環境(Ubuntu)でコアダンプを有効にするには、以下の設定が必要です。
システム全体のコアダンプのサイズ制限を無制限(unlimited)に設定する
これをやらないと、巨大なメモリ空間を持つプロセスのダンプが途中で途切れてしまいます
ulimit -c unlimited
コアダンプファイルの出力先とファイル名のフォーマットを指定する
%e は実行ファイル名、%p はプロセスID、%t はタイムスタンプ
sudo sysctl -w kernel.core_pattern=/tmp/core-%e-%p-%t
—
3. 精度高い「HelloWorld」:意図的にクラッシュさせてダンプを取る
いきなり複雑なテストスイートに組み込む前に、まずは「わざとゼロ除算や不正なポインタアクセスでクラッシュする小さなプログラム」を作り、LLDBが自動でダンプを吐く挙動をローカルで確認(HelloWorld的検証)してみましょう。
検証用コード: `crash_sample.c`
include
// 意図的に不正なメモリアクセス(ヌルポインタデリファレンス)を引き起こす関数
void cause_crash() {
int ptr = NULL; // ヌルポインタを定義
ptr = 42; // 存在しないアドレスに値を書き込もうとしてセグメンテーション違反を発生させる
}
int main() {
printf(“デバッグテストプログラムを開始します…\n”);
cause_crash();
printf(“この行には到達しません。\n”);
return 0;
}
コンパイルとデバッグ情報の付与
デバッガを限界まで活かすためには、コンパイル時にデバッグシンボル(-gオプション)を必ず付与してください。これがないと、メモリ上のアドレスが「どの関数の何行目か」を人間が読める形に翻訳できなくなります。
-g オプションを付けてコンパイルし、デバッグ情報をバイナリに埋め込む
gcc -g -O0 crash_sample.c -o crash_sample
LLDBのバッチコマンドファイルを用意する
人間が対話的に操作する代わりに、LLDBに実行させたい命令をあらかじめ記述したバッチファイル(スクリプト)を用意します。
ファイル名: `lldb_commands.txt`
プロセスがクラッシュした、あるいはアタッチされた際に自動実行されるコマンド群
バックトレース(コールスタック)を表示:どこから呼ばれてクラッシュに至ったかの一覧
bt
すべてのスレッドのバックトレースを表示(マルチスレッド環境のバグに必須)
thread backtrace all
現在のフレームにおけるすべてのローカル変数の値を出力
frame variable
レジスタの状態(CPUのレジスタに何が入っていたか)を表示
register read
デバッガを終了する
quit
実行と動作確認
次のようにコマンドを実行してみます。
プログラムを実行し、クラッシュ時にLLDBにキャッチさせる、あるいはコアファイルを読む
今回はシンプルに、lldbの `-b`(バッチモード)と `-s`(スクリプト指定)を使います
lldb -b -s lldb_commands.txt ./crash_sample
実行結果のイメージ:
コンソールに、`cause_crash` 関数の何行目でセグメンテーション違反(`EXC_BAD_ACCESS` や `SIGSEGV`)が起きたのか、当時のローカル変数の値は何だったのかが美しく出力されます。これが、今回CI上で自動化したい「極上のダンプ」です。
—
4. CI/CDの核心:GitHub Actionsでテスト失敗時にLLDBダンプを自動回収するワークフロー
さあ、いよいよ本丸です。GitHub ActionsなどのCI環境で、テスト(例えばGoogle TestやCMakeによるC++テストなど)が失敗した瞬間に、上記の一連の処理を自動で行うワークフローを構築します。
以下のYAMLファイルを作成してください。
ファイルパス: `.github/workflows/ci_debug_automation.yml`
name: CI with Automated LLDB Dump
on:
push:
branches: [ “main” ]
pull_request:
branches: [ “main” ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
# 1. リポジトリのソースコードをチェックアウト
- name: Checkout repository
uses: actions/checkout@v4
# 2. 必要なビルドツールとLLDBのインストール
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake lldb
# 3. テストプログラムのビルド(デバッグ情報を付与)
- name: Configure and Build
run: |
mkdir build
cd build
# -DCMAKE_BUILD_TYPE=Debug により、確実にデバッグシンボル付きでビルドする
cmake -DCMAKE_BUILD_TYPE=Debug ..
make
# 4. コアダンプの出力を有効化(Linuxランナーの設定)
- name: Enable Core Dumps
run: |
ulimit -c unlimited
echo “/tmp/core-%e-%p-%t” | sudo tee /proc/sys/kernel/core_pattern
# 5. テストの実行 & 失敗時のLLDB自動ダンプキャプチャ
- name: Run Tests with LLDB Fallback
run: |
cd build
# テストを実行する。もしテストが異常終了(exit code != 0)した場合の処理を記述
# ./your_test_binary は実際のテスト実行ファイル名に読み替えてください
./crash_sample || {
echo “==========================================”
echo “🚨 テストが異常終了しました! LLDBでダンプを収集します…”
echo “==========================================”
# 生成されたコアダンプファイルを特定する
CORE_FILE=$(ls -t /tmp/core- | head -n 1)
if [ -f “$CORE_FILE” ]; then
echo “コアダンプを検出しました: $CORE_FILE”
# LLDBをバッチモードで起動し、クラッシュ解析結果を lldb_crash_report.txt に出力する
# 引数: 実行ファイル と コアダンプファイル を指定
lldb -b \
-o “bt” \
-o “thread backtrace all” \
-o “frame variable” \
-o “register read” \
./crash_sample “$CORE_FILE” > ../lldb_crash_report.txt
else
echo “コアダンプが見つかりませんでした。通常のプロセスアタッチでログを取得します…”
# コアが出ないタイプの失敗(アサーション失敗など)の場合のフォールバック
lldb -b \
-o “run” \
-o “bt” \
-o “thread backtrace all” \
./crash_sample > ../lldb_crash_report.txt
fi
# テストの失敗ステータスをそのまま維持してCIを落とす
exit 1
}
# 6. 【超重要】失敗時にLLDBが吐き出したダンプをGitHubの成果物(Artifacts)として保存
- name: Upload LLDB Crash Dump
if: failure() # 前段のテストステップが失敗したときのみ実行される
uses: actions/upload-artifact@v4
with:
name: lldb-crash-dump-report
path: lldb_crash_report.txt
retention-days: 7 # 7日間保存
—
5. このアーキテクチャがもたらす圧倒的な実務メリット
このワークフローを導入したチームに何が起きるか、想像してみてください。
1. 「ローカルで再現しない地獄」からの解放
CIが落ちた通知を受け取ったエンジニアは、GitHub Actionsの画面から `lldb-crash-dump-report.zip` をダウンロードするだけですそこには、クラッシュした瞬間の全スレッドのコールスタック、変数の値、レジスタの状況が完璧に記録されています。「なぜその値がNULLになったのか」の証拠が、最初から手元にあるのです。
2. コード修正のループタイムの短縮
原因特定までの時間が「数時間(あるいは数日)」から「数秒」に短縮されます。無駄なprintデバッグコミットの山を作る必要もありません。
3. 心理的安全性の向上
「PRを出したらCIが謎の落ち方をしたどうしよう……」という恐怖感が消え、「もし落ちても、LLDBが詳細な遺書を残してくれる」という安心感のもとで、ダイナミックなリファクタリングや機能開発に集中できるようになります。
—
先輩エンジニアからのエール
低レイヤデバッガやCIの奥深い設定と聞くと、最初は少し難しく感じるかもしれません。しかし、今回紹介した仕組みは、あなたの開発チームの「デバッグ的負債」を根絶やしにする強力な武器になります。
「これをマスターすれば、毎日のコーディングが劇的に楽になりますよ。」
ぜひ、あなたのプロジェクトのCIパイプラインにもこの「自動ダンプ収集メカニズム」を組み込んでみてください。あなたのエンジニアリングライフが、よりスマートで快適なものになることを心から応援しています!