はしがき
この記事はLinux Advent Calendar 2018に投稿する予定だったけど、日付に間に合いそうになかったため取りやめて、Advent Calendar関係なしに投稿する記事になります。
はじめに
この記事は全国300万人のCoredump芸人を本気で目指すITエンジニアたち(当社調べ)を応援するために書かれたものである。生半可な気持ちではこの道を極めるのは不可能だ。かつては志を持って挑んだものの破れそして去っていた同士を私はたくさん見てきた。この記事を読み始める前にあなたが本当にCoredump芸人を欲するのか今一度見つめ直してほしい。
...て本当?
本当のはじめに
この記事では、Linuxでcore dumpファイルを作るように設定するところから、実際にデバッグするところまでを、流れとして解説する。
ちょっと執筆環境の都合上、少し古いLinux-4.16あたりを見ているのでご了承を。
そもそもCoredumpとは
UNIX環境では、プログラムがバグなどでクラッシュしてしまった時に、クラッシュした原因をあとから解析できるようにするためのcore dump file(単純にコアファイル・コアダンプとも呼ばれる)を作る機能が入っていることが多い。そんなUNIX思想に習って、Linuxにもcoreファイルを作る機能がある。
coreファイルは単純に言えば、メモリの内容のダンプをとっておき、あとからその内容を確認するという流れになる。ただ最近は、mappingされたアドレスやらshared libraryやらと、たかがメモリといえども複雑になっているため、メモリの内容だけでなくそれがどのアドレスにどうmappingされていたかの情報もLinuxではelf形式で一緒にダンプしている。elfの具体的なセクションの使い方などについては、私もあまり詳しくないので、ここでは省略する。
可能ならば、問題が起こる場所・時間・環境をそのまま使ってデバッグするほうが解析が捗ることが多い。しかし現実では、なかなか問題が再現しない、地理的に遠いところでしか再現しない、周りの特定の環境でしか再現しない、機材が高価でデバッグのために専有できない、などによりライブでのデバッグができないことも多い。そのような場合にcoreファイルを利用することが多いと思う。
私も定義とか歴史とかまでは考えたこともないので、細かいところはWikipedia: コアダンプでも見ておいてほしい。...プリントアウト?そんな時代もあったんですね(遠い目)
Coredumpを吐く設定
ulimitの設定
Linuxにはプロセス毎にulimit値を持っている。ulimitには複数の項目があるが、それぞれの項目ごとにsoftlimit値とhardlimit値を持つ。
[rarul@tina ~]$ cat /proc/self/limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 63147 63147 processes
Max open files 1024 1048576 files
Max locked memory 65536 65536 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 63147 63147 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 0 0
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us
Max core file sizeの項目がCoreファイルを作る時のサイズの上限の設定になる。0にするとcoreを作らない、unlimitedにすると上限なし、となる。
- softlimitが、実際にcoreファイルを作る時の上限になる
- hardlimitは、ユーザごとの設定できる値の上限になる
つまり、ユーザ権限では、hardlimitは下げる方向にしか設定できず、softlimit値はhardlimitまでしか上限設定できない。hardlimitを上げるにはcapabilities(7)のCAP_SYS_RESOURCEが必要になる。わかりやすくいうとroot権限が必要ということ。
プロセスごとに自分のlimit値を書き換えたい場合、setlimit(2)もしくはulimit(1)を使う。ulimitコマンドの場合単位はKiBの模様。
[rarul@tina ~]$ ulimit -Sc
0
[rarul@tina ~]$ ulimit -Sc 4096
[rarul@tina ~]$ ulimit -Sc
4096
ulimit値はfork()などで親から継承する。このためSystemWideに設定したい場合、initプロセスで設定しておくのがよい。このあたりは最近はSystemdが介在するので、詳細は下記の記事あたりに任せる。
coreの出力先
/proc/sys/kernel/core_patternに書かれたpathにcoreファイルが作られる。
[rarul@tina ~]$ cat /proc/sys/kernel/core_pattern
|/usr/share/apport/apport %p %s %c %d %P
Ubuntu(16.04)だとapportというのにpipeで渡しているようだ。通常は%Pなどの制御文字を使ってファイルのpathを指定することが多い。どんな制御文字が使えるかについては、kernel/Documentation/sysctl/kernel.txtより、
core_pattern is used to specify a core dumpfile pattern name.
. max length 128 characters; default value is "core"
. core_pattern is used as a pattern template for the output filename;
certain string patterns (beginning with '%') are substituted with
their actual values.
. backward compatibility with core_uses_pid:
If core_pattern does not include "%p" (default does not)
and core_uses_pid is set, then .PID will be appended to
the filename.
. corename format specifiers:
%<NUL> '%' is dropped
%% output one '%'
%p pid
%P global pid (init PID namespace)
%i tid
%I global tid (init PID namespace)
%u uid (in initial user namespace)
%g gid (in initial user namespace)
%d dump mode, matches PR_SET_DUMPABLE and
/proc/sys/fs/suid_dumpable
%s signal number
%t UNIX time of dump
%h hostname
%e executable filename (may be shortened)
%E executable path
%<OTHER> both are dropped
. If the first character of the pattern is a '|', the kernel will treat
the rest of the pattern as a command to run. The core dump will be
written to the standard input of that program instead of to a file.
きちんとした裏付けができていないが、coreファイルは「仮想アドレスはあるが物理アドレス割当がまだない」領域についてはsparse(hole)なファイルとして生成される。このため、sparse(hole)に対応していないファイルシステム、もしくはpipeを使った場合は、実際に領域が大きく取られてしまうという問題が起こるため注意が必要。また、sparse(hole)なファイルを圧縮・コピーなどする場合も注意が必要で、手っ取り早くはGNU tarのSオプションを使ったほうが良い。
ここも、最近のデストリビューションではSystemdが起動時に設定してしまう傾向があるため、適時Systemdでの設定方法を参照してほしい。
(2019/09追記)Coredumpが吐かれないケース
これらを設定してもCoredumpが作られないケースがある。man core(5)に書かれている。かいつまんで書くと、
- Filesystemへのアクセス権がないとき
- 既存のファイルがありhardlinkが2以上のとき
- Disk Fullやinode数不足のとき
- core_patternのpathのディレクトリが存在しないとき
- RLIMIT_COREやRLIMIT_FSIZEに引っかかったとき(前の章のulimitで記載ずみ)
- 実行中のexecファイルへのreadアクセス権がないとき
- uidとeuid(gidとegid)が一致しないとき
- core_patternやcore_uses_pidが空設定のとき
- CONFIG_COREDUMPが無効のとき
- MADV_DONTDUMPを設定しているとき
セキュリティを考えたアクセス権を気にしたルールが多い。特にuidとeuid不一致の系はユーザプロセスに閉じた話になるため、システム管理者としては気づきにくいように見える・・・というかなかなか気づけなかった。suid_dumpableの設定もしくはprctl(2)のPR_SET_DUMPABLEをすることで回避できる。もちろん設定変更するとセキュリティの穴ができるのでご注意。
Coredumpの吐き方
crash
Linuxでは、プロセスがcrashしたときに、先の設定に従ってcoreファイルが作られる。SIGSEGV, SIGABRT, SIGILL, SIGBUS, SIGFPE のsignalの場合に作られる。このへんはsignal(7)あたりにのっている。
より詳細については、kernel/kernel/signal.cより、
if (sig_kernel_coredump(signr)) {
if (print_fatal_signals)
print_fatal_signal(ksig->info.si_signo);
proc_coredump_connector(current);
/*
* If it was able to dump core, this kills all
* other threads in the group and synchronizes with
* their demise. If we lost the race with another
* thread getting here, it set group_exit_code
* first and our do_group_exit call below will use
* that value and ignore the one we pass it.
*/
do_coredump(&ksig->info);
}
**sig_kernel_coredump()**は、kernel/include/linux/signal.hより、
#define SIG_KERNEL_COREDUMP_MASK (\
rt_sigmask(SIGQUIT) | rt_sigmask(SIGILL) | \
rt_sigmask(SIGTRAP) | rt_sigmask(SIGABRT) | \
rt_sigmask(SIGFPE) | rt_sigmask(SIGSEGV) | \
rt_sigmask(SIGBUS) | rt_sigmask(SIGSYS) | \
rt_sigmask(SIGXCPU) | rt_sigmask(SIGXFSZ) | \
SIGEMT_MASK )
(-----snip-----)
#define sig_kernel_coredump(sig) siginmask(sig, SIG_KERNEL_COREDUMP_MASK)
となっていて、これらのシグナルを受けるとcoreファイルを生成して終了するとなる。また上記signal.hの上の方にもテーブルがコメントに書かれていていろいろ説明してくれている。
個人的には、SIGPIPEではcoreが作られないというのが意外だった。まぁkernelに変更加えれば作ってくれるんだろうけど。
kill
上記のように、signalを受けてプロセスが強制終了するときにcoreファイルが作られる。なので、killコマンドで直接coreファイル取得したいプログラムを終了させても良い。もちろん、いきなりSIGSEGVなどを送ると「何も悪くないのにSIGSEGVが起こった」ようなcoreファイルが作られてしまうので、そのへんを了解の上でとなる。
gcore
gdbパッケージに含まれるgcore(1)というコマンドを使うと、ulimitなどの設定を回避しつつcoreファイルを取り、かつプログラムは継続実行してくれる。ごめん、あまり使ったことがないので、これくらいにさせて...
gdb
gdbでattach中のプログラムだと、gdbコマンド上からcoreファイルを作らせることができる。gdbでいいタイミングまでステップ実行/breakヒットさせそこでcoreを作らせるような使い方が一般的か。問題が再現する環境でちょっとだけデバッグに使えるんだけど長時間は無理、というような場合に、タイミングよく取得したcoreファイルでオフライン解析するようなことができる。
(gdb) generate-core-file
Saved corefile core.5962
Coreの解析のやり方
gdbでの読み込み
ただのメモリダンプだった場合、どうやって解析用ツールにデータを取り込ませるのかが悩ましい場合もある。ただ今のLinuxではほとんどの場合、elf形式で作られるので、gdbで読み込ませるのが手っ取り早い。
Current directory is ~/work/test/
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 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-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) core-file core.5962
[New LWP 5962]
Core was generated by `/home/rarul/work/test/main'.
Program terminated with signal SIGTRAP, Trace/breakpoint trap.
#0 0x0000000000400860 in ?? ()
ここでは、上の例でgdbから直接作ったcoreを読み込んでいる。上記のように読み込んだ時になぜcoreが作られたのかも一緒に提示してくれる場合もある。
ここではホスト上でセルフでデバッグシンボル入りプログラムを実行したためデバッグ情報も出ているが、組み込みやクロス環境ではstrip済みのプログラムを実行していることが多い。そういう場合、実行していたプログラムと同じときのビルドのデバッグシンボル入りファイルがないと、デバッグ情報が得られない。デバッグシンボルを利用する場合はfileコマンドを使う。
(gdb) file main
Reading symbols from main...done.
ダイナミックリンクライブラリに依存している場合は、それらもデバッグシンボル入りが欲しくなる。手っ取り早くしたい場合はこんな感じ、
(gdb) set sysroot ~/some/where/root
### もしくは、
(gdb) set solib-absolute-prefix ~/some/where/root
確認はinfo sharedで
(gdb) info shared
From To Syms Read Shared Object Library
0x00007ffff7aded20 0x00007ffff7b8acc9 No /usr/lib/x86_64-linux-gnu/libstdc++.so.6
0x00007ffff7841a70 0x00007ffff78518b5 No /lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007ffff74948b0 0x00007ffff75e7b04 No /lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7171600 0x00007ffff71e2d0a No /lib/x86_64-linux-gnu/libm.so.6
0x00007ffff7dd7ac0 0x00007ffff7df5850 No /lib64/ld-linux-x86-64.so.2
1つ1つadd-symbol-fileで読むのも良いが、いちいちアドレスやらオフセットやらをセクションごとに手動で計算する必要があるため、あまりおすすめできない。
デバッグ情報がビルド時の絶対pathになっていて、ソースコードがうまく自動表示できないことがある。私の場合は、gdbのdirectoryコマンドでごまかすか、パッケージの数が多い場合はもう根っこにシンボリックリンクを作って逃げることが多い。
なお、デストリビューションでも通常はデバッグシンボル入りが標準では入らないので、標準のコマンドやライブラリもデバッグしたい場合はそれらを入手する必要がある。下記のようなページを参照してほしい。
...と書きつつ、私はなぜかいつもデストリビューション環境のデバッグはうまくいかないんだよなあ...
gdbでのデバッグのポイント
SIGSEGVでNULLにアクセスした、なんてのが多いので、通常は落ちた箇所の$pcの命令や変数の中身を確認して即終了という場合が多い。gdbの場合は「(gdb) info registers」「(gdb) x/i $pc」「(gdb) printf (変数名)」など。
想定しない関数呼び出しや引数なんてのも多いため、backtraceや引数・ローカル変数一覧もとりあえず出しておこう。gdbの場合は「(gdb) backtrace」「(gdb) info locals」「(gdb) info args」「(gdb) up」「(gdb) down」など。
排他処理がまずくて複数スレッドから同時にアクセスしていた、なんてのも多く、落ちたときのcurrentのスレッドだけでなくプロセス内の他のスレッドも確認する必要がある。gdbの場合は「(gdb) info threads」「(gdb) thread (番号)」切り替えた後の「(gdb) backtrace」など。
スタック破壊していてgdbがうまくbacktraceを出してくれないこともあり、とりあえずスタック周辺をダンプしてそれっぽいシンボルからbacktraceの残骸を掘り起こすこともある。gdbの場合は「(gdb) info registers」「(gdb) x/i $pc」「(gdb) x/i $lr」「(gdb) datainfo $sp 1600」など。datainfoについては、こんな感じのユーザ定義マクロを~/.gdbinitあたりに書いておいてくれ。
define datainfo
set $s = $arg0
set $max = ((((long)$arg1) * 4) / 4)
set $o = 0
printf "address offset data\n"
while ($o < $max)
printf "0x%08lx [%4d]: ", $s, $o
output/a *(unsigned long *)($s)
printf "\n"
set $o += 4
set $s += 4
end
end
...なんとなく、coreファイルからgdbデバッグ中にもついステップ実行したくなってしまうのは私だけでしょうか...
一部のネイティブバイナリアンになってくると、デバッグシンボルなしにdlmallocのarenasを掘り出して、どのサイズのchunkでどれくらいオーバランしたかを推定したり、上書きされた時の値から誰が書き込んだかを推定して書き込む箇所周辺のバグを探す、なんてこともあるんだとかなんとか。
(ポエム) Debuggable Systemの思想
Debugにかかるコストは必要なコストだ
組み込みシステムでは、要求する仕様を満たせる最小限のハードウェア資源だけを用意して、できる限りコストを下げようとすることがよくある。もちろん、使いもしないものにコストをかけるのはムダではあるが、あまりに切り詰めた結果、ソフトウェアのデバッグ用のツールやソースコードの変更すらままならないこともよく起こる。ハードウェアコストは最小になったが、ソフトウェアの開発費が膨れ上がり、トータルではかえって費用が高くなった、なんて経験をしている人も多いと思う。
ハードウェアのコストは原材料費や限界利益としてのしかかるが、ソフトウェア開発コストは開発開始前に予算がきっちり決められ、以後はただの固定費扱いされるため、このジレンマは経営レベルではなかなか理解され難い。ある程度は信用が置かれた人が経験と勘を元にドンブリ勘定でどのあたりがボーダかを事前に取り決めるしかない。
Debugありきの開発環境を作ろう
もう少し具体的には、CPU/RAM/Storage/Networkはデバッグツール類が動く前提でリソースを見積もる、ソフトウェアの更新を安全・確実・簡単にできる環境を必ず用意する、ソフトウェアのバージョン管理を確実にかつ一元的に行う、あたりがポイントか。ワンストップで確実なビルド環境の構築や、gitなどバージョン管理システムを使うことは、さすがに今は世間的にも当たり前であってほしいと思う。バグ管理システムももちろん必須だが、ソフトウェア工学的な話にいってしまうので、ちょっとここではあまり考えないことにする。もうちょっと個別に考えてみる。
仕様で必要な機能を無効にしないとserial consoleが使えない、なんてのは回避すべき。Debugする上でconsoleは非常に大事なので、最終商品で無効にするかはともかく、「使えない」状態なのはリソース不足と言える。一部に「Ether TCPでtelnetできればいらないんじゃね?」という意見もあるが、私は賛同できない。根っこに関わるバグがあるとEtherやTCP/IPをしゃべれないなんてことはよくある。
CPU/RAM/Storage/Networkの制限でデバッグツールが使えない、というのも回避すべき。ただ無尽蔵にリソースを使ってしまうツールもあるため、多少のバランスは必要かもしれない。ps, top, cat, du の実行やその結果の保存、1秒おきの実行に耐えられる、くらいは規模によっては欲しくなる。もちろんツールの実行のリソースを減らすためにbusyboxなどの活用も考えたい。strace, perf, tcpdump, ftraceあたりだとさすがにフルで動かせる環境を常に用意するのは贅沢かもしれない。
ソフトウェアの書き換えにかかるコストは、ソフトウェア開発者ほど見落としやすい。書き換えに特殊なJIGが必要だと書き換える可能性ある人・場所ごとにJIGが必要だし、頻繁にアップデート・ダウングレードするとなると書き換えにかかる時間も短くしたい。書き換えが不安定で失敗すると復旧に手間暇かかるともなると本当にヒヤヒヤものだ。そういったことにならないように、確実に書き換えできる環境を事前に設計する必要がある。また、何かしら問題が起こった時は、手間暇かけてでも開発の初期のうちに確実に直しておくべきとなる。
バグが起こった時にどう解析するかも事前に考える必要がある。ログベースで解析する必要が多いだろうが、そうなると、確実にログを保存する仕組みやら、本当に大事な箇所のトレースログを抽出する仕組みが必要になる。経験が浅いと、とりあえずログを出しまくってしまい、結果問題が起こった時に大事なログが埋もれてしまったり流れてしまったりする。個人的には「不必要なログを出さない勇気」が必要だと思っている。
最終的にはcoreファイルやログでバグ解析するにしても、ライブ環境に他のデバッグ手段がないのは痛い。serial consoleはともかく、TCPごしのtelnetやgdbserverの準備、CPU/RAMの統計情報・プロセスの状態の確認、などのできる環境は常に用意しておきたい。CPU不足・RAM不足・ディスク溢れ・inode不足・fd数の上限・プロセス数の上限・pipe,socketの状態確認、あたりが該当か?
ソフトウェアバージョンの確実な管理は、バグ報告時の再現テストや修正確認に必要なのはともかく、coreファイルの解析時のデバッグシンボル合わない問題にも関わってくる。バージョン付与に一貫性がない、いつのソフトなのかわからない、当時のビルドを再現できない、ログに出力されるバージョンが実はウソだった、なんてことになるとムダに解析に時間を取られることになる。
ソフトウェア開発ではよく変更と動作確認を何度も繰り返すサイクルになる。ターゲットシステムのrootfsを部分書き換えする方法でもいいんだけど、組み込みだとreadonlyで部分書き換え不可のファイルシステムだったりするんで、部分書き換えするのにもひと手間ふた手間かかってしまう。個人的には、常にnfsrootありきの開発環境にしておき、変更・ビルド・即実行、とできる開発環境を整えておきたい。開発初期は特に、いかにして開発を快適に進められるかの環境整備に十分に工数をかけておきたい。
もとの趣旨のcoreの解析であげると、ローカル変数やヒープ領域は値の永続性やアドレスの特定が難しいので、デバッグ時に重要となるような情報はあえてグローバル変数に残しておき、coreダンプ時などに変数の中身をすぐ確認できるようにするような手法もある。グローバル変数領域がムダになるが、そこにはあえて目をつむり、そのグローバル変数の値ありきの設計にして、デバッグのしやすさと状態を管理することをやったりもする。
あとがき
ちょっと最後の方がグチくさい内容になってしまったけど、coreファイルを使ったデバッグの一通りの流れについて書いてみた。解析のための断片的な情報は結構あるんだけど、一気通貫した記事はなかったので、これで役に立つのならばどうぞと考えている。
これまで私はStorageが限られていることもあってあまりcoreファイルありきの解析をしてこなかったので、今でもまだ試行錯誤なところが多い。指摘があれば積極的に記事を更新していきたいと思っている。
Binary Hacksみたいな本が最近はなかなかなくて、こういう内容を学ぼうとしても情報が分散しててなかなか大変だなぁと感じた。Linux Device Drivers 4th Editionも結局出ないみたいで、インターネットで便利になったのか、はたまた体系的な勉強がやりづらくなったのか、とかとか時代の変化を感じてしまうくらいには歳をとってしまったか。
・・・ところで、Coredump芸人っていったい何なんですか?朝から晩まで送られてきたcoreファイルをひたすら解析するようなソフトウェア技術者のことなんでしょうか?
Special thanks
Linux Advent Calendar 2018の私の空いた穴をすぐに埋めてくれてありがとう。