116
88

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

オープンソースまるごと by NRI OpenStandiaAdvent Calendar 2018

Day 13

初心者のためのコアファイルの解析方法について

Last updated at Posted at 2018-12-12

コアファイルの解析方法について

Linux で動作しているサーバープログラムにおいて、

  • プロセスが CPU / メモリを食いつぶしている
  • プロセスが異常動作をしている
  • セグメンテーション違反でプロセスが落ちた

などの原因調査のためにコアファイル(メモリーダンプ)からメモリー内の状態を確認したいことがあります。ここでは、CentOS 7で出力されたコアファイルに対して、gdb コマンドを使って解析する方法をまとめてみました。とはいうものの、私自身もまだ gdb 初心者なので、基本的にはビギナー向けの内容になっております。

gdb コマンドの利用方法の説明だけだと実際の解析イメージが湧きづらいかもしれないので、後半は Apache で実際にコアダンプを発生させて、原因箇所がわかるかどうかのケーススタディも行っています。

事前準備

環境情報

OS 構成 追加パッケージ
CentOS 7.5.1804 最小インストール ・gdb
・yum-utils
・httpd

コアファイルの取得

gcore コマンドによる取得

コアファイル解析を実施するためには、問題発生時のコアファイルが必要です。

特定プロセスによる CPU 高負荷やメモリ逼迫、異常動作などの現象が継続しているものに関しては、当該プロセスを停止する前に、以下の gcore コマンドから該当プロセスのコアファイルを取得できます。

# gcore コマンドでコアファイルの取得
gcore {プロセスID}

※ コマンドを実行したディレクトリ配下に core.{プロセスID} というファイルで保存されます。
※ コアファイルはメモリのダンプなので、当該プロセスが使用しているメモリが多いほどサイズが大きくなります。そのため、大量のメモリを使用するプロセスのコアファイルを取得する際には、ディスクの空き容量に気をつけてください。

OS の機能によるコアファイルの取得

CentOS 6 以前の SystemV 系では、ulimit の制限によりコアファイルの出力有無が制御されていましたが、CentOS 7 の systemd 系からは、この制限方法が変わっているので注意が必要です。具体的には、systemd の該当ユニットの設定ファイルをオーバーライドして、[Service] セクションに LimitCORE=infinity を追加することになります。

以下は、httpd ユニットのコアファイルの出力制限を解除する例です。
※ 後半のケーススタディも実施してみる方は、この対応を実施しておいてください。

# httpd.service のオーバーライド設定を作成
systemctl edit httpd.service
/etc/systemd/system/httpd.service.d/override.conf
[Service]
LimitCORE=infinity

:information_source: その他のリソース制限を付加する場合も、基本的には同様の手順になります。各パラメータ名と ulimit の対応関係は、man systemd.exec から確認できます。

:information_source: Apache の場合は、この LimitCORE の設定追加だけではなく、Apache の設定である CoreDumpDirectory ディレクティブの変更も必要です。CoreDumpDirectory が指定されていない場合は、ServerRoot(/etc/httpd) が出力ディレクトリになるのですが、このディレクトリは、root がオーナーになっているため、apache ユーザーでは出力ができないためです。

参考: http://httpd.apache.org/docs/2.4/mod/mpm_common.html#coredumpdirectory

コアファイル解析環境の設定

解析環境の構築

コアファイルを解析する際に、コアファイルが出力された実際の環境(特に本番環境などでは)でそのまま調査することは基本的に難しいはずです。そのため、コアファイルの解析にはコアファイルが出力された環境と同等の OS、パッケージ、実行モジュールが配備された環境を新たに用意します。パッケージバージョンが少しでも異なると、コアファイル解析時に、情報が欠損したり、コールスタックやソースコード行の位置が適切に表示されないので注意が必要です。

環境をあわせるために、コアファイルを取得した環境で以下の情報を確認しておきます。

  1. OS の種類・バージョン・アーキテクチャ
  2. パッケージの詳細バージョン

OS の種類・バージョン・アーキテクチャ

OS の種類・バージョン・アーキテクチャを確認します。

# OS の種類・バージョンを確認
cat /etc/redhat-release

# アーキテクチャ確認
uname -m

この OS と同一の環境を新規でセットアップします。

:information_source: 本番環境では CentOS ではなく、RHEL が使われていることが多いかもしれません。RHEL のコアファイルは、同じバージョンの CentOS であっても、基本的に解析できないと考えたほうがよいです。一部、断片的に情報が見られたりすることはありますが、不完全なことが多いです。RHEL のコアファイルは RHEL で解析することを推奨します。

パッケージの詳細バージョン

続いて、コアファイルを出力した環境の OS に導入されているパッケージおよび、その詳細バージョンを確認します。

# 導入パッケージバージョンの確認
rpm -qa  | sort 

導入されているパッケージの結果を diff するなどして、解析用に構築した OS に導入されているパッケージを調整します。OS の初期インストール後に定期的にパッケージのアップデートを行っているような環境の場合には注意してバージョンを合わせる必要があります。

特にバージョンに注意する必要があるパッケージは

  • コアファイル解析を行う主体のパッケージ
  • コアファイル解析を行う主体のパッケージが依存しているパッケージ
  • 共通 C ライブラリ(glibc)

の 3 つになります。これらは最低限一致させておくのがよいです。

debuginfo パッケージの導入

gdb コマンドでコアファイル解析を行うためには、解析を行う該当パッケージのデバッグシンボルが必要になります。デバッグシンボルは通常のパッケージには含まれていないので、yum の base-debuginfo レポジトリから追加インストールが必要になります。

デバッグシンボルの導入に関しては、debuginfo-install コマンドを利用すると、依存パッケージも含めて debuginfo パッケージが導入されるので便利です。

たとえば、httpd のコアファイルがある場合、以下のように gdb コマンドからコアファイルを読み込ませると、不足している debuginfo のパッケージを明示してくれます。

[root@openstandia ~]# gdb /usr/sbin/httpd {httpdのコアファイル}
・
・
(省略)
・
・
Missing separate debuginfos, use: debuginfo-install httpd-2.4.6-80.el7.centos.1.x86_64

このメッセージの指示どおり、debuginfo-install コマンドを実行すると、関連する debuginfo パッケージ群をまとめて導入することができます。

[root@openstandia ~]# debuginfo-install -y httpd-2.4.6-80.el7.centos.1.x86_64
読み込んだプラグイン:auto-update-debuginfo, fastestmirror
enabling base-debuginfo
Loading mirror speeds from cached hostfile
 * base: ftp-srv2.kddilabs.jp
 * extras: centos.usonyx.net
 * updates: ftp-srv2.kddilabs.jp
パッケージ yum-plugin-auto-update-debug-info-1.1.31-46.el7_5.noarch はインストール済みか最新バージョンです
--> トランザクションの確認を実行しています。
---> パッケージ apr-debuginfo.x86_64 0:1.4.8-3.el7_4.1 を インストール
---> パッケージ apr-util-debuginfo.x86_64 0:1.5.2-6.el7 を インストール
---> パッケージ expat-debuginfo.x86_64 0:2.1.0-10.el7_3 を インストール
---> パッケージ glibc-debuginfo.x86_64 0:2.17-222.el7 を インストール
--> 依存性の処理をしています: glibc-debuginfo-common = 2.17-222.el7 のパッケージ: glibc-debuginfo-2.17-222.el7.x86_64
---> パッケージ httpd-debuginfo.x86_64 0:2.4.6-80.el7.centos.1 を インストール
---> パッケージ libdb-debuginfo.x86_64 0:5.3.21-24.el7 を インストール
---> パッケージ libselinux-debuginfo.x86_64 0:2.5-12.el7 を インストール
---> パッケージ lua-debuginfo.x86_64 0:5.1.4-15.el7 を インストール
---> パッケージ pcre-debuginfo.x86_64 0:8.32-17.el7 を インストール
---> パッケージ systemd-debuginfo.x86_64 0:219-57.el7_5.3 を インストール
---> パッケージ zlib-debuginfo.x86_64 0:1.2.7-17.el7 を インストール
--> トランザクションの確認を実行しています。
---> パッケージ glibc-debuginfo-common.x86_64 0:2.17-222.el7 を インストール
--> 依存性解決を終了しました。

(省略)

debuginfo-install コマンドは、引数に指定されたパッケージの debuginfo パッケージを base-debuginfo レポジトリから取得するように実装された python スクリプトです。内部的に yum コマンドが利用されているため、依存パッケージの debuginfo も含めてすべてが導入されます。
※ 解析するコアファイルによっては、不足するパッケージが複数回にわたって出力されることがあります。その場合も、その都度、debuginfo-install コマンドを実行します。

手動導入モジュールの展開

RPM 以外で導入したモジュール(手動ビルド、もしくは、zip で展開)などがある場合は、コアファイルが出力された環境と同様のパスに展開しておきます。コアファイル解析を行うプロセスが参照しているライブラリ(so ファイルなど)が参照できればよいので、実際のセットアップが必要かどうかは、プロダクトに依存します。

以上がコアファイル解析までの事前準備となります。

解析環境になんらかの不備があった場合には、コアファイル解析段階でいろいろなエラーメッセージが表示されるため、最初はそこまで慎重になる必要はありません。足りないものがあれば、随時、パッケージやライブラリを追加していくことになります。

コアファイルの解析

事前に取得したコアファイルを任意のディレクトリに配置し、以下のように gdb コマンドに、実行ファイル、コアファイルを引数に指定して実行します。

# コアファイルの解析
gdb {実行ファイル} {コアファイル}

{実行ファイル} にはコアファイルを出力したプロセスのメインプログラムを指定します。たとえば、OS 標準の Apache であれば、/usr/sbin/httpd となります。
※ 一般的に、ps コマンドで見た場合に、COMMAND 列に表示されているのが実行ファイルです。

gdb コマンドを実行すると、コアファイルの解析に必要な依存ライブラリの存在確認やバージョン確認、デバッグシンボルの確認などのメッセージが表示されます。その後、gdb の対話型コンソールが起動します。解析環境が適切に整っていれば、各種コマンドから情報が取得できるようになります。

以下は Apache のコアファイルに関して、対話型コンソールで backtrace を呼び出した場合の結果例です。

(gdb) backtrace
#0  0x00007f80b7c37fe7 in accept4 (fd=3, addr=addr@entry=..., addr_len=addr_len@entry=0x7ffe2894b880, flags=flags@entry=524288)
    at ../sysdeps/unix/sysv/linux/accept4.c:33
#1  0x00007f80b834343a in apr_socket_accept (new=new@entry=0x7ffe2894b970, sock=0x557458231db8, connection_context=0x5574582da1f8)
    at network_io/unix/sockets.c:210
#2  0x00005574560030a3 in ap_unixd_accept (accepted=0x7ffe2894ba08, lr=0x557458231d78, ptrans=<optimized out>) at unixd.c:301
#3  0x00007f80af27d73f in child_main (child_num_arg=child_num_arg@entry=0) at prefork.c:685
#4  0x00007f80af27d9f5 in make_child (s=0x557458236370, slot=slot@entry=0) at prefork.c:810
#5  0x00007f80af27da56 in startup_children (number_to_start=5) at prefork.c:828
#6  0x00007f80af27e760 in prefork_run (_pconf=<optimized out>, plog=0x55745823a378, s=0x557458236370) at prefork.c:986
#7  0x0000557455fcbffe in ap_run_mpm (pconf=pconf@entry=0x55745820d158, plog=0x55745823a378, s=0x557458236370) at mpm_common.c:96
#8  0x0000557455fc4d76 in main (argc=2, argv=0x7ffe2894be18) at main.c:783

backtrace では、現在参照しているプロセスのコールスタック(コードの呼び出し階層)を確認できます。

#0(frame 0) が現在のプログラムの実行位置です。
frame 番号が増えるに従い、呼び出し元の関数を示していることになります。

以下は、debuginfo パッケージが適切に導入できていない場合の backtrace の表示例です。

(gdb) backtrace
#0  0x00007f80b7c37fe7 in accept4 () from /lib64/libc.so.6
#1  0x00007f80b834343a in apr_socket_accept () from /lib64/libapr-1.so.0
#2  0x00005574560030a3 in ap_unixd_accept ()
#3  0x00007f80af27d73f in child_main () from /etc/httpd/modules/mod_mpm_prefork.so
#4  0x00007f80af27d9f5 in make_child () from /etc/httpd/modules/mod_mpm_prefork.so
#5  0x00007f80af27da56 in startup_children () from /etc/httpd/modules/mod_mpm_prefork.so
#6  0x00007f80af27e760 in prefork_run () from /etc/httpd/modules/mod_mpm_prefork.so
#7  0x0000557455fcbffe in ap_run_mpm ()
#8  0x0000557455fc4d76 in main ()

バックトレースにより、ライブラリファイル名や関数名に関して表示されていることがわかりますが、実際のソースコード行の位置や、引数の値が表示されていないことがわかります。この状態ではコアファイルを適切に解析することができません。

よくあるエラーメッセージについて

gdb コマンド実行直後に表示されるメッセージの中で、特長的なメッセージを3つ列挙します。
これらメッセージに関しては、全てを完全に解決する必要はないですが、解析が必要な部分に関連するエラーが発生している場合は、解消する必要があります。

デバッグシンボル(debuginfo) が不足する場合のメッセージ

以下のメッセージが表示されている場合は、デバッグシンボル(debuginfo) が不足しています。
debuginfo-install コマンドで不足している debuginfo パッケージを導入してください。

Reading symbols from {プログラム/ライブラリファイルのフルパス} ...(no debugging symbols found)...done.
・
・
(省略)
・
・
Missing separate debuginfos, use: debuginfo-install {不足パッケージのリスト...}

ライブラリのバージョンが一致していない場合のメッセージ

以下のメッセージが表示されている場合は、バージョンがミスマッチしています。
該当のライブラリファイルのバージョンおよび、debuginfoのバージョン確認が必要です。

debuginfo-install コマンドを使わず、手動で異なるバージョンの debuginfo を導入してしまった場合などに表示されるメッセージです。

warning: the debug information found in {デバッグシンボルファイル} does not match {プログラム/ライブラリファイルのフルパス} (CRC mismatch).

ライブラリが不足している場合のメッセージ

以下のメッセージが表示されている場合は、ライブラリが欠損しています。
RPM 以外で導入されたライブラリなどが使用されており、環境ミスマッチが起きている可能性が高いです。欠損しているライブラリを確認し、必要があれば関連プロダクトのビルドもしくはインストールにより、欠損が生じているライブラリを適切な位置に展開する必要があります。

warning: Could not load shared library symbols for {ライブラリファイルのフルパス}

主要な gdb コマンド

コアファイル内の情報が一通り問題なく確認できるようになったら、gdb の複数のコマンドを組み合わせて、プログラムの実行状況や、変数の値などを確認し、エラーが発生した原因を調査します。

gdb コマンドは大量にあるため、以下はほんの一例となります。

  • バックトレース表示(backtrace)

    現在の参照しているスレッドでのプログラムの呼び出し階層を表示します。
    #0 の frame が現在の実行位置を示します。
(gdb) backtrace
#0  0x00007f80b7c37fe7 in accept4 (fd=3, addr=addr@entry=..., addr_len=addr_len@entry=0x7ffe2894b880,
    flags=flags@entry=524288) at ../sysdeps/unix/sysv/linux/accept4.c:33
#1  0x00007f80b834343a in apr_socket_accept (new=new@entry=0x7ffe2894b970, sock=0x557458231db8,
    connection_context=0x5574582da1f8) at network_io/unix/sockets.c:210
#2  0x00005574560030a3 in ap_unixd_accept (accepted=0x7ffe2894ba08, lr=0x557458231d78, ptrans=<optimized out>)
    at unixd.c:301
#3  0x00007f80af27d73f in child_main (child_num_arg=child_num_arg@entry=0) at prefork.c:685
#4  0x00007f80af27d9f5 in make_child (s=0x557458236370, slot=slot@entry=0) at prefork.c:810
#5  0x00007f80af27da56 in startup_children (number_to_start=5) at prefork.c:828
#6  0x00007f80af27e760 in prefork_run (_pconf=<optimized out>, plog=0x55745823a378, s=0x557458236370) at prefork.c:986
#7  0x0000557455fcbffe in ap_run_mpm (pconf=pconf@entry=0x55745820d158, plog=0x55745823a378, s=0x557458236370)
    at mpm_common.c:96
#8  0x0000557455fc4d76 in main (argc=2, argv=0x7ffe2894be18) at main.c:783

:information_source: 対話型コンソールでのコマンドは重複するものがない限り、短く省略することが可能です。たとえば、backtrace の場合、最小では ba もしくは、省略形の bt で実行できます。省略形があるものは、各コマンドのリファレンスに記載されています。

  • フレーム切り替え(frame {フレーム番号})

    参照するスタックフレームを切り替えます。
    {フレーム番号} には、backtrace コマンドで表示されている #{数字} の番号を指定します。
    スタックフレームを切り替えることで、その関数内での引数、変数などを確認することができます。
(gdb) frame 3
#3  0x00007f80af27d73f in child_main (child_num_arg=child_num_arg@entry=0) at prefork.c:685
685             status = lr->accept_func(&csd, lr, ptrans);
  • コード参照(list)

    現在参照しているフレーム内のソースコードの一部(前後 5 行)を表示します。
    ソースが表示できるのは、該当するパッケージの debuginfo が導入されている、もしくはライブラリ自体にデバッグシンボルが付加されているものに限ります。
(gdb) list
680             }
681         got_fd:
682             /* if we accept() something we don't want to die, so we have to
683              * defer the exit
684              */
685             status = lr->accept_func(&csd, lr, ptrans);
686
687             SAFE_ACCEPT(accept_mutex_off());      /* unlock after "accept" */
688
689             if (status == APR_EGENERAL) {
  • ローカル変数参照(info locals)

    現在の参照位置から参照できるローカル変数の一覧を表示します。
(gdb) info locals
current_conn = <optimized out>
csd = 0x0
thd = 0x5574582d8260
osthd = 140190842857600
ptrans = 0x5574582da1f8
allocator = 0x5574582d80f0
status = <optimized out>
i = <optimized out>
lr = <optimized out>
pollset = 0x5574582d86b8
sbh = 0x5574582d86b0
last_poll_idx = 0
lockfile = <optimized out>
  • 変数値の参照(print {変数名})

    引数やローカル変数、グローバル変数の値を確認します。
    {変数名} がポインタの場合には、* をつける必要があります。
    構造体変数の場合は、構造体の各変数についてもすべての値がまとめて出力されます。
(gdb) print *thd
$2 = {pool = 0x5574582d81e8, td = 0x7ffe2894b9e0, data = 0x0, func = 0x0, exitval = 0}
  • スレッド一覧取得(info threads)

    プロセス内で稼動している LWP スレッドがあればその一覧を表示します。
    * がついたスレッドは現在参照しているスレッドを表します。
(gdb) info threads
  Id   Target Id         Frame
  27   Thread 0x7fb8e19e2880 (LWP 19575) 0x00007fb8e04bf7fd in read () at ../sysdeps/unix/syscall-template.S:81
  26   Thread 0x7fb8c27ec700 (LWP 19606) 0x00007fb8dffe003f in accept4 (fd=4, addr=..., addr@entry=..., addr_len=addr_len@entry=0x7fb8c27ebc10, flags=flags@entry=524288)
    at ../sysdeps/unix/sysv/linux/accept4.c:37
  25   Thread 0x7fb8c2fed700 (LWP 19605) pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
  24   Thread 0x7fb8c37ee700 (LWP 19604) pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
・
・
(省略)
・
・
  4    Thread 0x7fb8d188f700 (LWP 19584) pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
  3    Thread 0x7fb8d2090700 (LWP 19583) pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
  2    Thread 0x7fb8d2891700 (LWP 19582) pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
* 1    Thread 0x7fb8d3092700 (LWP 19581) pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
  • スレッド切り替え(thread {スレッド番号})

    参照するスレッドを切り替えます。
    {スレッド番号} には、info threads コマンドで表示される先頭のスレッドの番号を指定します。
(gdb) thread 2
[Switching to thread 2 (Thread 0x7fb8d2891700 (LWP 19582))]
#0  pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
185     62:     movl    (%rsp), %edi
  • コマンドヘルプ(help {コマンド名})

    コマンドの説明や指定する引数のパターンなどを表示してくれます。
    また、help all と打てば、全てのコマンドの一覧をみることができます。
(gdb) help backtrace
Print backtrace of all stack frames, or innermost COUNT frames.
With a negative argument, print outermost -COUNT frames.
Use of the 'full' qualifier also prints the values of the local variables.
Use of the 'no-filters' qualifier prohibits frame filters from executing
on this backtrace.

ケーススタディ

以下は、Apache でコアダンプが発生した場合をサンプルに、そのコアファイルの解析を行う簡単なケーススタディです。

意図的なコアダンプ発生モジュール作成

ここではサンプルとして、意図的にセグメンテーション違反を発生させる Apache モジュールを作成します。

# Apache モジュール作成のために開発者用パッケージを追加導入
yum install -y httpd-devel gcc

# Apache モジュールの雛形作成
apxs -g -n core_dumper

# Apache モジュールの雛形ディレクトリに移動
cd core_dumper

雛形で出力される mod_core_dumper.c の core_dumper_handler 関数を以下のような実装に変更します。char の2次元配列である cArray の文字列について、1文字ずつ出力していき文字列の終わりに "!" と改行を出力していくモジュールのようです。どこに問題があるか一目瞭然かもしれませんが、記事の都合上、このまま先に進みます。

--- mod_core_dumper.c.org       2018-12-03 16:24:02.384964375 +0900
+++ mod_core_dumper.c   2018-12-03 18:25:44.210170418 +0900
@@ -50,8 +50,14 @@
     }
     r->content_type = "text/html";

-    if (!r->header_only)
-        ap_rputs("The sample page from mod_core_dumper.c\n", r);
+    char cArray[3][10] = {"self", "core", "dumper"};
+    int i, j;
+    for (i = 0; i < sizeof(cArray) / sizeof(cArray[0]); i++) {
+        for (j = 0; i < strlen(cArray[i]); j++) {
+            ap_rprintf(r, "%c", cArray[i][j]);
+        }
+        ap_rputs("!\n", r);
+    }
     return OK;
 }
# Apache モジュールのコンパイル
apxs -c mod_core_dumper.c

# Apache モジュールのインストールおよび、LoadModule の組み込み
apxs -i -a mod_core_dumper.la

# コアダンプの出力用ディレクトリを作成
mkdir -p /var/coredumps
chmod a+w /var/coredumps

:information_source: apxs コマンドでコンパイルした場合、"-O2 -g" というオプションでコンパイルされます。そのため、ライブラリ自身(mod_core_dumper.so)内にデバッグシンボルが組み込まれた状態になるので、このライブラリが配置されている環境であれば、デバッグシンボルを見つけることができます。これが別環境でのみ配置されているライブラリであった場合は、コアファイル解析時にライブラリの欠損が発生します。

最後に Apache に以下の設定ファイルを追加して、mod_core_dumper と URI をマッピングします。

/etc/httpd/conf.d/mod_core_dumper.conf
# コアダンプの出力ディレクトリを /var/coredumps に変更
CoreDumpDirectory /var/coredumps

<Location /core_dumper>
  SetHandler core_dumper
</Location>
# Apache を起動
systemctl start httpd

httpd プロセスでコアダンプ発生確認

当該の処理を curl コマンドで呼び出すと、見事に落ちます。

# curl から mod_core_dumper の処理を呼び出すと応答エラーが発生
[root@openstandia ~]# curl http://localhost/core_dumper
curl: (52) Empty reply from server

# Apache の error_log を tail すると、コアファイル出力を示すメッセージがでている
[root@openstandia ~]# tail -1 /var/log/httpd/error_log
[Wed Dec 05 13:41:12.964910 2018] [core:notice] [pid 2351] AH00051: child pid 2354 exit signal Segmentation fault (11), possible coredump in /var/coredumps

# コアファイルが、/var/coredumps 直下に出力されています
[root@openstandia ~]# ls -la  /var/coredumps
合計 2804
drwxrwxrwx.  2 root   root        23 12月  7 16:41 .
drwxr-xr-x. 21 root   root      4096 12月  5 13:39 ..
-rw-------.  1 apache apache 2867200 12月  5 13:41 core.2354

実際のコアファイル解析の例

さっそく、gdb コマンドで解析してみます。

[root@openstandia ~]# gdb /usr/sbin/httpd /var/coredumps/core.2354
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-110.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /usr/sbin/httpd...Reading symbols from /usr/lib/debug/usr/sbin/httpd.debug...done.
done.
[New LWP 2354]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Core was generated by `/usr/sbin/httpd -DFOREGROUND'.
Program terminated with signal 11, Segmentation fault.
#0  core_dumper_handler (r=0x5555cc9e7bc0) at mod_core_dumper.c:57
57                  ap_rprintf(r, "%c", cArray[i][j]);

適切に debuginfo が導入された状態であれば、上記のような表示となり、mod_core_dumper.c:57 で、コアダンプしたことがわかります。正直なところ、これだけでもう十分かもしれませんが、一応原因を調べてみます。

backtrace で呼び出し階層を見てみます。
今回は、意図的にコアダンプを発生させているので、当然ながら当該のモジュール名が最後の呼び出し位置になっています。Apache の main 関数からどのようなコールスタックで、core_dumper_handler が呼ばれているのかの確認にもなります。

(gdb) backtrace
#0  core_dumper_handler (r=0x55feaa99aba0) at mod_core_dumper.c:57
#1  0x000055feaa05d990 in ap_run_handler (r=r@entry=0x55feaa99aba0) at config.c:169
#2  0x000055feaa05ded9 in ap_invoke_handler (r=r@entry=0x55feaa99aba0) at config.c:439
#3  0x000055feaa072a8a in ap_process_async_request (r=r@entry=0x55feaa99aba0) at http_request.c:328
#4  0x000055feaa072d64 in ap_process_request (r=r@entry=0x55feaa99aba0) at http_request.c:363
#5  0x000055feaa06ef62 in ap_process_http_sync_connection (c=0x55feaa992ce0) at http_core.c:190
#6  ap_process_http_connection (c=0x55feaa992ce0) at http_core.c:231
#7  0x000055feaa066fc0 in ap_run_process_connection (c=c@entry=0x55feaa992ce0) at connection.c:41
#8  0x000055feaa0673a8 in ap_process_connection (c=c@entry=0x55feaa992ce0, csd=<optimized out>) at connection.c:202
#9  0x00007fe75c39e7af in child_main (child_num_arg=child_num_arg@entry=4) at prefork.c:707
#10 0x00007fe75c39e9f5 in make_child (s=0x55feaa8ec370, slot=slot@entry=4) at prefork.c:810
#11 0x00007fe75c39ea56 in startup_children (number_to_start=1) at prefork.c:828
#12 0x00007fe75c39f760 in prefork_run (_pconf=<optimized out>, plog=0x55feaa8f0378, s=0x55feaa8ec370) at prefork.c:986
#13 0x000055feaa041ffe in ap_run_mpm (pconf=pconf@entry=0x55feaa8c3158, plog=0x55feaa8f0378, s=0x55feaa8ec370) at mpm_common.c:96
#14 0x000055feaa03ad76 in main (argc=2, argv=0x7ffe74916678) at main.c:783

今回は、core_dumper_handler 関数(mod_core_dumper.c の 57行目)で落ちたようです。
念のため、コードをみてみると、2次元配列のうちの特定の1文字を出力している箇所だとわかります。

(gdb) list
52
53          char cArray[3][10] = {"self", "core", "dumper"};
54          int i, j;
55          for (i = 0; i < sizeof(cArray) / sizeof(cArray[0]); i++) {
56              for (j = 0; i < strlen(cArray[i]); j++) {
57                  ap_rprintf(r, "%c", cArray[i][j]);
58              }
59              ap_rputs("!\n", r);
60          }
61          return OK;

info locals でローカル変数を見てみると、j が配列の範囲を超えて異常な数字になっていることがすぐにわかります。print で、cArray[i][j] を表示してみると、たしかにメモリアクセスが抑止されますね。

(gdb) info locals
cArray = {"self\000\000\000\000\000", "core\000\000\000\000\000", "dumper\000\000\000"}
i = 0
j = 4032

(gdb) print cArray[i][j]
Cannot access memory at address 0x7ffe74917000

というわけで、j のカウントアップをしている 56 行目の 2 段目の for ループの終了条件式がおかしいのだろうと見当がつきます。で、コードをあらためて確認してみると、j とすべきところが、 i になっていることがわかりますね。。。これで、Apache プロセスがコアダンプした原因が明らかになりましたね!

core_dumper/mod_core_dumper.c
    for (i = 0; i < sizeof(cArray) / sizeof(cArray[0]); i++) {
        for (j = 0; i < strlen(cArray[i]); j++) {
                // ^^^(ここは j でないといけないのに、i になってる!!!)
            ap_rprintf(r, "%c", cArray[i][j]);
        }
        ap_rputs("!\n", r);
    }

まとめ

今回は、gdb コマンドによるコアファイルの解析方法についてまとめました。

正直なところ、このサンプルのように即座に原因がわかるようなコアファイルが出力されることはまずないでしょう。特に枯れたオープンソースを利用している場合には、コアファイルの解析自体が必要になるケース自体も少ないはずです。また、コアファイルが解析できたからといって、問題がすぐに解決できるという保証もありません。
ですが、コアファイルの解析方法を知っておくことで、事実関係の理解や原因箇所の切り分け、問題のなんらかの糸口が見つけられる可能性は高くなるはずです。

gdb コマンドは、今回記載したコアファイルの解析以外に、動作中のプログラムにアタッチして、ブレークポイントの設定やステップ実行などを行うことも可能です。こちらについても、いずれ投稿できたらと思います。

参考資料

116
88
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
116
88

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?