コアファイルを gdb with VSCode で解析

とあるサーバで1年ぐらい前に発生していた Apache が異常終了したときに abrt によって収集されていたコアファイルを解析したメモ。

abrt について

abrt というのはプロセスでコアダンプが発生したときにそれを所定のディレクトリに保管したり(デフォルトだとカレントディレクトリなのでシステム内にコアファイルがまき散らされてしまう)、発生時の状況を収集して保存したり、さらにメール等で通知してくれるやつです。

RHEL7 系ディストリなら @base をインストールすれば有効になるので大抵の環境で有効になってると思います。

RHEL8 系ディストリだと別途インストールのうえ有効にする必要があります。RHEL8 ならコアダンプ時は systemd-coredump によって abrt と似たような情報が収集されるので必要無い、ということかもしれません。

むしろ RHEL8 だと abrt は非奨励らしいので有効にしないほうがよいのかも。

abrt の保管場所

下記のような場所に保管されています。

/var/spool/abrt/ccpp-2022-02-11-03:21:02-7654

この中にはコアダンプ発生時の様々な情報が入っています。

例えば core_backtrace には発生時のスタックトレースが入っています。この内容は ELF ファイル(Windows でいうところの exe とか dll とかのいわゆる実行可能ファイル)に含まれている情報を元にしているのでソースファイルや行番号などといった詳細な情報は含まれていません。辛うじて .so ファイルがエクスポートしているシンボル名だけは解決されています。

{
  "signal": 6,
  "executable": "/usr/sbin/httpd",
  "stacktrace": [
    {
      "crash_thread": true,
      "frames": [
        {
          "address": 139743219704439,
          "build_id": "cb4b7554d1adbef2f001142dd6f0a5139fc9aa69",
          "build_id_offset": 221815,
          "function_name": "raise",
          "file_name": "/usr/lib64/libc-2.17.so"
        },
        {
          "address": 139743219710312,
          "build_id": "cb4b7554d1adbef2f001142dd6f0a5139fc9aa69",
          "build_id_offset": 227688,
          "function_name": "abort",
          "file_name": "/usr/lib64/libc-2.17.so"
        },
        :
        :
      ]
    }
  ]
}

var_log_messages には発生時の /var/log/messages が入っています。

[System Logs]:
:
:
Feb 11 03:21:02 ore-no-server abrt-hook-ccpp[13577]: Process 7654 (httpd) of user 0 killed by SIGABRT - dumping core
Feb 11 03:21:02 ore-no-server systemd[1]: httpd.service: main process exited, code=dumped, status=6/ABRT
Feb 11 03:21:02 ore-no-server systemd[1]: httpd.service: control process exited, code=exited status=1
Feb 11 03:21:02 ore-no-server systemd[1]: Unit httpd.service entered failed state.
Feb 11 03:21:02 ore-no-server systemd[1]: httpd.service failed.

coredump がいわゆるコアファイルです。

コアファイルを開くための Docker 環境

コアファイルを gdb で開くために同じディストリの環境を Docker で作ります。元環境は CentOS 7.5.1804 でした。

docker run -it -v "$PWD:$PWD" -w "$PWD" --name debug centos:7.5.1804

コアファイルのスタックトレースに対応する debuginfo が必要です。これはバージョンも完全に一致する必要があります。前述の core_backtrace に含まれていた build_id は ELF バイナリを一意に識別するなにかのハッシュ値になっていて、このハッシュ値をもとにした /usr/lib/debug/.build-id/80/619621be79fdb8007b60fbf5c634dfbe12a318 のようなシンボリックリンクが debuginfo パッケージには含まれています。

要するにこういうことです。

rpm -qf /usr/lib/debug/.build-id/80/619621be79fdb8007b60fbf5c634dfbe12a318
#=> curl-debuginfo-7.29.0-46.el7.x86_64

readlink -f /usr/lib/debug/.build-id/80/619621be79fdb8007b60fbf5c634dfbe12a318
#=> /usr/lib64/libcurl.so.4.3.0

ので build_id を手がかりにバージョンも完全に一致する debuginfo をインストールできます。まずは core_backtrace に含まれる build_id をリストアップします。

cat core_backtrace | jq .stacktrace[].frames[].build_id -r | sort | uniq
#=> 468cdcf74ee3ea5d4b33541534c67444dd8619a3
#=> 584240581e74bb6f7a0b99111286cd8fc2a43695
#=> 80619621be79fdb8007b60fbf5c634dfbe12a318
#=> cb4b7554d1adbef2f001142dd6f0a5139fc9aa69
#=> d260f476eb23fd11cbc0a825b7e36b0f0c66f190
#=> de762a28174110911b273e175d54f222b313cfe0

シンボリックリンク名に読み替えて debuginfo をインストールします。

yum --enablerepo='*debug*' install \
  /usr/lib/debug/.build-id/46/8cdcf74ee3ea5d4b33541534c67444dd8619a3 \
  /usr/lib/debug/.build-id/58/4240581e74bb6f7a0b99111286cd8fc2a43695 \
  /usr/lib/debug/.build-id/80/619621be79fdb8007b60fbf5c634dfbe12a318 \
  /usr/lib/debug/.build-id/cb/4b7554d1adbef2f001142dd6f0a5139fc9aa69 \
  /usr/lib/debug/.build-id/d2/60f476eb23fd11cbc0a825b7e36b0f0c66f190 \
  /usr/lib/debug/.build-id/de/762a28174110911b273e175d54f222b313cfe0

次の警告が表示されました。

No package /usr/lib/debug/.build-id/58/4240581e74bb6f7a0b99111286cd8fc2a43695 available.
No package /usr/lib/debug/.build-id/d2/60f476eb23fd11cbc0a825b7e36b0f0c66f190 available.

これは PHP の mod_php と curl 拡張です。remi リポジトリのものなので remi-release をインストールしてリポジトリを追加する必要があります・・と言いたいところですが remi リポジトリは古いパッケージが維持されていないので remi リポジトリを追加しても対応する debuginfo はインストールできません。

とりあえず PHP は諦めるとして、他は次の通りに debuginfo がインストールされました。

curl-debuginfo   7.29.0-46.el7          x86_64
glibc-debuginfo  2.17-222.el7           x86_64
httpd-debuginfo  2.4.6-80.el7.centos.1  x86_64
nspr-debuginfo   4.19.0-1.el7_5         x86_64

これで gdb でコアダンプを読み込める・・と思ったのですが、どうやら debuginfo だけでなく元の ELF バイナリそのものが必要なようです・・?

curl/glibc/nspr は CentOS 7.5.1804 の base に最初から入っているものなので対応不要ですが、httpd だけは updates にあったものなので CentOS Vault から入れる必要があります。直接 RPM をダウンロードしたり yum リポジトリを追加するなどしても良いですが、次のようにすると手っ取り早いです。

# CentOS の yum リポジトリを最新にする
yum update centos-release

# Vault の C7.5.1804 リポジトリを有効にしてインストール
yum --enablerepo='C7.5.1804-*' install httpd-2.4.6-80.el7.centos.1.x86_64

これで gdb でコアファイルを開けるようになります。試しにスタックトレースを表示してみると次のようにソースファイルや行番号まで表示されるようになりました。

gdb -c coredump --ex bt
 :
Core was generated by `/usr/sbin/httpd -DWEB -DFOREGROUND'.
Program terminated with signal 6, Aborted.
#0  0x00007f1880f86277 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
56        return INLINE_SYSCALL (tgkill, 3, pid, selftid, sig);
#0  0x00007f1880f86277 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
#1  0x00007f1880f87968 in __GI_abort () at abort.c:90
#2  0x00007f187888a7bc in PR_Assert (s=s@entry=0x7f18788aa62a "0 == rv", file=file@entry=0x7f18788aa3f0 "../../.././nspr/pr/src/pthreads/ptthread.c", ln=ln@entry=973)
    at ../../../nspr/pr/src/io/prlog.c:553
#3  0x00007f18788a368d in _PR_InitThreads (type=type@entry=PR_USER_THREAD, priority=priority@entry=PR_PRIORITY_NORMAL, maxPTDs=maxPTDs@entry=0) at ../../../nspr/pr/src/pthreads/ptthread.c:973
#4  0x00007f1878894334 in _PR_InitStuff () at ../../../nspr/pr/src/misc/prinit.c:180
#5  0x00007f18788944e5 in _PR_ImplicitInitialization () at ../../../nspr/pr/src/misc/prinit.c:265
#6  PR_Init (type=type@entry=PR_USER_THREAD, priority=priority@entry=PR_PRIORITY_NORMAL, maxPTDs=maxPTDs@entry=256) at ../../../nspr/pr/src/misc/prinit.c:266
#7  0x00007f1879d36935 in Curl_nss_init () at nss.c:1059
#8  0x00007f1879d2c8dc in Curl_ssl_init () at sslgen.c:191
#9  0x00007f1879d1e1b5 in curl_global_init (flags=3) at easy.c:225
#10 0x00007f1879f634b8 in ?? ()
 :

ここまでの内容で一旦 Docker イメージを作っておきます。

docker commit debug debug

VSCode でコアファイルを開く

VSCode で Dev Container を↑で作った Docker イメージで実行します。

// .devcontainer/devcontainer.json
{
    "name": "debug",
    "image": "debug",
    "extensions": [
        "ms-vscode.cpptools"
    ]
}

デバッグを始めるための設定をします。下記の URL を参考にしました。

// .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "cppdbg",
            "request": "launch",
            "name": "Open a core dump(c/c++)",
            "program": "/usr/sbin/httpd",
            "coreDumpPath": "${file}",
            "cwd": "${workspaceFolder}",
            "MIMode": "gdb"
        }
    ]
}

VSCode でコアファイルを選択して F5 です。

すごいべんり。

さいごに

解析結果 → ぜんぜんわからない。