Core DumpとGDBでLinuxアプリケーションのクラッシュをデバッグする:systemd-coredumpセットアップガイド

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

誰もが経験したことがあるはずだ — 午前2時にサービスがクラッシュし、プロセスが消え去り、syslogに埋もれた「Segmentation fault」だけが手がかりとして残される。翌朝のデバッグは、証拠の半分が失われた状態での法医学的調査のように感じる。コアダンプを適切に設定することで、その調査が実際に可能になる。

厄介なのは、Linuxにはコアダンプを扱う方法がいくつかあり、本番環境ではそれぞれがまったく異なる挙動をする点だ。開発環境でうまく動いていたものが、systemdが絡んだ途端に気づかないうちに失敗することがよくある。実際のサーバー運用経験をもとに、本当に機能する方法を紹介しよう。

アプローチ比較:Linuxでコアダンプを扱う3つの方法

1. 従来のulimit + kernel.core_pattern

この古典的なアプローチはsystemdより何十年も前から存在する。コアファイルのサイズ制限を設定し、カーネルにダンプの書き込み先を指定する:

# 現在のコアダンプ設定を確認する
ulimit -c

# コアダンプサイズを無制限にする(現在のセッションのみ)
ulimit -c unlimited

# コアダンプの保存先とファイル名パターンを設定する
echo '/tmp/cores/core.%e.%p.%t' | sudo tee /proc/sys/kernel/core_pattern

# 再起動後も設定を永続化する
echo 'kernel.core_pattern = /tmp/cores/core.%e.%p.%t' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

# ディレクトリが存在し、書き込み可能であることを確認する
sudo mkdir -p /tmp/cores
sudo chmod 1777 /tmp/cores

systemdで管理されているサービスの場合、セッションごとのulimitは引き継がれない。ユニットファイルにも以下の設定が必要だ:

# /etc/systemd/system/myapp.service.d/override.conf
[Service]
LimitCORE=infinity

2. systemd-coredump(モダンなアプローチ)

systemdがinitシステムとして使われている場合—これはモダンなUbuntu、Debian、RHEL、Fedoraすべてに当てはまる—systemd-coredumpはカーネルのダンプ機構に直接フックする。ダンプを自動でキャプチャし、/var/lib/systemd/coredump/に圧縮して保存し、journalでインデックス化する。coredumpctlで即座に検索できる。

# カーネルがダンプをsystemd経由でルーティングしていることを確認する
cat /proc/sys/kernel/core_pattern
# 期待される出力:
# |/lib/systemd/systemd-coredump %P %u %g %s %t %c %h

3. ABRT(Automatic Bug Reporting Tool)

ABRTはRed Hatが開発し、FedoraとRHELにデフォルトで搭載されている。クラッシュを監視し、キャプチャし、オプションでBugzillaへのバグレポートを自動送信する。複数のデーモンが動作し、GUIコンポーネントも持つ—デスクトップワークステーションには便利だが、本番サーバーには重い。Ubuntuベースのシステムや最小構成のインストールでは、そのオーバーヘッドはほとんど正当化できない。

各アプローチのメリット・デメリット

従来のulimit

  • メリット:追加の依存関係なしにどこでも動作する。ファイルの保存先を完全に制御できる。バックグラウンドデーモンが不要。
  • デメリット:定期的なクリーンアップをしないとコアファイルがディスクをすぐに埋め尽くす。自動圧縮やメタデータがない。systemdユニットでサービスごとの制限を見落としやすい。対象のコアファイルを手動で探す必要がある。

systemd-coredump

  • メリット:自動zstd圧縮(ヒープダンプで3〜5倍の圧縮率が期待できる)。journalにリッチなメタデータを保存(PID、シグナル、実行ファイルパス、タイムスタンプ、cgroup)。coredumpctlで検索と抽出が容易になる。journalctlと統合してログの相互参照が可能。
  • デメリット:systemdが必要(モダンなディストリビューションでは問題なし)。/var/lib/systemd/coredump/にダンプが蓄積されるため、十分なディスク容量が必要。圧縮処理のため、クラッシュ時のオーバーヘッドがやや増える。

ABRT

  • メリット:アップストリームへの自動レポート送信。ワークステーション用途に優れたGUI統合。
  • デメリット:リソース消費が多い。デフォルトで外部にデータを送信する(プライバシーの懸念)。Debian/Ubuntuでは大がかりな手動設定なしには使えない。サーバーのポストモーテムデバッグには過剰。

推奨設定:systemd-coredumpが最善の理由

systemdを動かすあらゆる本番Linuxサーバーにおいて、systemd-coredumpは最良の選択だ。4GB RAMのUbuntu 22.04マシンでは、最初の重大インシデントの後にその差がはっきりした。以前はfind / -name 'core.*'を実行し、誰かがすでに上書きしていないことを祈るしかなかった。今ではcoredumpctl listが1秒以内にすべてのクラッシュを表示する—バイナリパス、シグナル、タイムスタンプまで。

zstd圧縮は実際に大きな恩恵だ。400MBのヒープダンプが約120MBに圧縮されるため、デプロイ失敗時に繰り返しクラッシュしてもディスクを食い尽くさない。設定は10行。あとはcoredumpctlとGDBが素早いトリアージから50フレームのコールスタック展開まで、すべてを処理してくれる。

実装ガイド

ステップ1:systemd-coredumpが有効か確認する

Ubuntu 20.04以降や最新のsystemdベースのディストリビューションでは、systemd-coredumpはデフォルトでインストールされている。念のため確認しよう:

# Ubuntu/Debian
sudo apt install systemd-coredump

# RHEL/Fedora
sudo dnf install systemd

# カーネルがダンプをsystemd経由でルーティングしていることを確認する
cat /proc/sys/kernel/core_pattern
# 表示されるはず: |/lib/systemd/systemd-coredump %P %u %g %s %t %c %h

ステップ2:coredump.confをチューニングする

サーバーのメモリプロファイルに合わせて/etc/systemd/coredump.confを編集する:

[Coredump]
# 圧縮ファイルとしてディスクに保存する(journalに埋め込まない)
Storage=external

# zstd圧縮を使用する
Compress=yes

# コアダンプ1件あたりの最大サイズ — 最大プロセスのRSSに合わせて調整する
ProcessSizeMax=2G

# 保存するすべてのコアの合計ディスク使用量の上限
ExternalSizeMax=10G
MaxUse=5G
KeepFree=1G
# 設定を適用する
sudo systemctl daemon-reload

4GB RAMのサーバーでは、ProcessSizeMax=2Gによって暴走プロセスのヒープ全体をキャプチャできる。また、ダンプ自体がOOMキルを引き起こさないよう十分な余裕も確保される。

ステップ3:テスト用クラッシュを発生させる

意図的なNULLポインタ参照を含む小さなCプログラムを書く:

// crash_test.c
#include <stdio.h>
#include <stdlib.h>

void broken_function() {
    int *ptr = NULL;
    *ptr = 42;  // ここでセグフォルトが発生する
}

int main() {
    printf("クラッシュします...\n");
    broken_function();
    return 0;
}
# デバッグシンボル付きでコンパイルする — 読みやすいGDB出力に不可欠
gcc -g -o crash_test crash_test.c

# 実行する
./crash_test
# 出力: クラッシュします...
# Segmentation fault (core dumped)

ステップ4:コアを検索して抽出する

# キャプチャされたすべてのコアダンプを一覧表示する
coredumpctl list

# 出力例:
# TIME                             PID  UID  GID SIG     COREFILE EXE
# Tue 2026-06-03 14:22:11 JST    4821 1000 1000 SIGSEGV present  /home/user/crash_test

# 最新クラッシュの詳細メタデータ
coredumpctl info

# GDB解析用にコアファイルを抽出する
coredumpctl dump -o ./core_crash_test

ステップ5:GDBで解析する

バイナリと抽出したコアを指定してGDBを起動する:

gdb ./crash_test ./core_crash_test

# またはcoredumpctlに直接処理させる:
coredumpctl gdb

GDB内で実際に作業を行うコマンドは以下の通りだ:

# クラッシュ時のフルコールスタックを表示する
(gdb) bt
(gdb) bt full          # 各フレームのローカル変数も含む

# 特定のスタックフレームに移動する
(gdb) frame 1

# 現在のフレームの変数を調査する
(gdb) info locals

# 特定の変数を表示する
(gdb) print ptr

# クラッシュ時点のCPUレジスタを表示する
(gdb) info registers

# クラッシュ周辺のソースコードを表示する
(gdb) list

# 逆アセンブルする(シンボルがstrippedなバイナリに有効)
(gdb) disassemble

上記のテストプログラムでは、btが正確な行番号とptrのNULLポインタを示してbroken_functionを直接指し示す。バイナリ、クラッシュ箇所、変数の状態—発生から何時間後でも完全な状況を再構築できる。

デバッグシンボルなしの本番バイナリをデバッグする

実際の本番バイナリは通常strippedされている。利用可能であればdebuginfoパッケージをインストールする:

# Ubuntu/Debian — デバッグシンボルのリポジトリを有効にする
sudo apt install ubuntu-dbgsym-keyring
echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse" | \
  sudo tee /etc/apt/sources.list.d/ddebs.list
sudo apt update

# 特定パッケージのデバッグシンボルをインストールする(例: nginx)
sudo apt install nginx-dbgsym

自分でコンパイルしたアプリの場合は、strippedバイナリの横にデバッグシンボルファイルを別途保管する:

# シンボル付きでビルドし、デプロイ用にstripしながらデバッグ情報を保持する
objcopy --only-keep-debug myapp myapp.debug
strip --strip-debug myapp

# GDBでシンボルを手動でロードする:
(gdb) symbol-file /path/to/myapp.debug

インシデントランブックへの組み込み方

systemd-coredumpを設定すれば、以下の手順は2分で完了する。クラッシュインシデントが発生したら最初にやるべきことだ:

  1. coredumpctl list --since "2 hours ago"を実行して最近のクラッシュを確認する
  2. journalctl -u yourservice --since "..."でタイムスタンプとログを照合する
  3. ローテーションされる前にコアをアーカイブする: coredumpctl dump PID -o /var/incident-cores/$(date +%Y%m%d_%H%M%S).core
  4. 自分でコンパイルしたサービスは、unstrippedバイナリまたは.debugファイルをビルドに紐付けたバージョン管理された場所に必ず保管する

コアダンプなしのクラッシュは謎のままだ。コアダンプがあれば、それはただのデバッグセッションになる。一度設定してしまえば—次に午前2時のアラートが鳴ったとき、完全な状況が手元に揃っている。

Share: