私のセグメンテーションフォルトはどこへ消えたのか?
(rmpr.xyz)entrで C プログラムを繰り返しコンパイル・実行するとhelloは異常終了したが、コンパイル出力の後にSegmentation faultメッセージが表示されない- このメッセージを出力するのはクラッシュしたプログラム自身ではなく、子プロセスを回収し
SIGSEGVによる終了を確認した 親シェル bash -cを明示しても、残っているコマンドが実質 1 つだけなら Bash は新しいプロセスを作らずexecveで自分自身を置き換える ことがあり、メッセージを出す親シェルが消えてしまう- 別スクリプトでは shebang で起動した Bash が
./helloを子として実行して待機するため、SIGSEGVを確認してSegmentation fault (core dumped)を出力する ./helloを サブシェル(./hello)で実行するか、後ろに; trueを追加して後続処理を残すとexec最適化を防ぎ、エラーメッセージを確認できる
メッセージが消えた実行経路
-
次の
entrコマンドでhello.cが変わるたびにコンパイルと実行を繰り返すls hello.c | entr -s "gcc -o hello hello.c && ./hello" -
helloはセグメンテーションフォルトで終了したのに、Segmentation faultメッセージも目に見える非 0 の終了ステータスもなく、何も出力が残らない -
bash -cでコマンドを包んでも結果は同じだったls hello.c | entr -s "bash -c 'gcc -o hello hello.c && ./hello'" -
一方、同じコマンドを
run.shに移して実行するとエラーが表示される#!/bin/bash gcc -o hello hello.c && ./hellols hello.c | entr -s ./run.sh ./run.sh: line 2: 104465 Segmentation fault (core dumped) ./hello -
Segmentation faultを出力する主体- クラッシュしたプログラムはすでに終了しており、親シェルが子プロセスを回収 するときに
SIGSEGVで死んだ事実を確認してからメッセージを出力する - したがって、メッセージを出力する親シェルが残っていなければ何も表示されない
- Bash は
bash -c "some_command"で実行すべきことが実質そのコマンドだけなら、新しいプロセスを fork せずexecveで自分自身を置き換える` ことがある - この最適化はほとんどの実行では外から見えない
- スクリプト方式では
entrが./run.shを子として実行し、shebang が新しい Bash を起動する - 新しい Bash は
gcc実行後に./helloを fork して待機するため、子のSIGSEGVによる終了を確認してエラーメッセージを残せる
- クラッシュしたプログラムはすでに終了しており、親シェルが子プロセスを回収 するときに
親シェルを残す 2 つの方法
- クラッシュするコマンドを サブシェル で実行するとメッセージが再び表示される
ls hello.c | entr -s "gcc -o hello hello.c && (./hello)" hello.c: line 1: 106595 Segmentation fault (core dumped) ( ./hello )- 括弧は
./helloが fork されたサブシェルで実行されるよう強制する - Bash が親の役割を維持するため、サブシェルの
SIGSEGVによる終了を回収してメッセージを出力できる
- 括弧は
- クラッシュするコマンドの後ろに実行する処理を残しても
exec最適化 を防げるls hello.c | entr -s "bash -c 'gcc -o hello hello.c && ./hello; true'" bash: line 1: 109516 Segmentation fault (core dumped) ./hello; trueがあるため Bash は./helloが終わった後にもやることが残っており、自分自身をそのプログラムに置き換えない- 別のラッパースクリプトがなくてもセグメンテーションフォルトのメッセージを確認できる
1件のコメント
Lobste.rsの意見
最近のLinuxディストリビューションはデフォルトでコアダンプを無効化していることが多く、困ることが多い
ulimit -c unlimitedで再び有効化できるが、コアファイルは実行ファイルと同じディレクトリには作られないようで、systemdがどこかへ移しているようだOpenBSDのようにコアダンプをデフォルトで有効にし、実行ファイルと同じディレクトリに置いてくれるとよいのだが、今ではクラッシュが起きるとたいてい実行ファイルをgdbの中で再実行しなければならない
coredumpctl**で最近のコアダンプを一覧表示して取得できる/usr/binのような場所でコアダンプを探そうとは思わないし、そのような場所は容量が小さいか読み取り専用のファイルシステムであることも多いデフォルトの保存先としては作業ディレクトリのほうがずっとよいのではないか
しかもユーザーがコアファイルを有効化しても何をしているのか分かっていないと見なされ、rootですら見つけにくい場所に隠しているように感じられる
CやDなどでプログラムを書いていて終了原因を知りたいなら、
waitシステムコールの戻り値を確認すればよく、プログラムがシグナルで終了したかどうかが分かるOSがコアダンプを別の場所へ転送すれば、必要なときにすぐデバッグしたり、ディストリビューションにバグレポートを送ったりできるという利点もある
Linuxカーネルはコアダンプを生成するとき、**
/proc/sys/kernel/core_pattern**という特別なファイルを読み、保存場所を決めるファイルに書き出す代わりにプログラムへパイプで渡すこともできるので、GUIダイアログの表示、デバッガの即時起動、開発者へのレポートメール送信といった処理が可能だ
systemdはカーネルの
core_patternを自分で管理する値に設定するが、systemdを使わなくても同じカーネル機能を使えるWindows XPの開発当時、コアダンプをMicrosoftへ送信したことで、存在すら知らなかった多くのバグを修正できたという話もあり、Windowsに開発ツールをインストールすると自分のプログラムを開発する際にオンデマンドデバッガがかなり便利だ
この機能は2003年にLinuxへ導入され、詳しくは
man 5 coreで確認できるコアダンプはデバッグには優れているが、無関係に見えるファイルの横に置かれると一般ユーザーを混乱させるし、診断レポートの仕組みとして使うにはひどいこともある
提出されたWindowsのクラッシュレポートにどれほど多くのデータが入っているのかを初めて見たときは愕然とした
プライバシーを保護するクラッシュレポートの仕組みを前提にするなら、提出されたクラッシュを診断できるようにスタックとレジスタ情報だけで原因を推測する方法を身につける必要がある
Bashがスクリプト内のコマンドをときどき直接**
exec**するという事実はまったく知らず、驚くべき機能だBashプロセス自体の負荷が大きいわけでもなく、必要な場面では習慣的に
execを使っているので、やや設計を誤った機能のようにも感じられるただ、全体の実行回数を合計すると予想以上に大きな差を生むのかもしれないとも思う