非常に稀なセグメンテーションフォルトの実例

セグメンテーションフォルト

はじめに

セグメンテーションフォルト(セグメンテーション違反)は、中級以上のプログラマなら遭遇したことがある事象だと思います。今回は私が遭遇して、解決までに2週間ほどかかった非常に稀なセグメンテーションフォルトの実例を紹介します。

セグメンテーションフォルトとは

セグメンテーションフォルトは以下のとおり説明されています。

セグメンテーション違反英語: segmentation fault)はソフトウェアの実行時に起きる特定のエラー条件である。segfault(セグフォールト)と略される場合がある。

セグメンテーション違反はアクセスが許可されていないメモリ上の位置、もしくは許可されていない方法(例えばリードオンリーの位置へ書き込みをしようとする、もしくはオペレーティングシステムの部分を上書きしようとする)でメモリ上の位置にアクセスしようとするときに起こる。

アプリケーションプログラム(AP)が動作したときに、OS(WindowsやLinux)によってAPに割り当てされているメモリ領域以外に、APがアクセスしたときに「ダメだよ!」ということで、OSがAPを強制終了させる仕組みです。

セグメンテーション違反 – Wikipedia

Windowsの場合

 Windowsですと 0xc0000005 というエラーコードでアプリが終了したりBSOD(ブルースクリーン)になったりします。Windows XPのころは時々起きていましたが、最近は殆ど見ることありませんね。

Linuxの場合

Linux ですと command[pid]: segfault at address ip fault_address sp stack_address error error_code in libso.so[so_address+size] が出力されれAPが終了します。

ここで各変数の意味は以下の通りです。

  • command
    セグメンテーションフォルトが発生したコマンド名(プロセス名)
  • pid
    セグメンテーションフォルトが発生したコマンド(プロセス)のプロセスID
  • address
    セグメンテーションフォルトが発生したメモリアドレス(ここにAPがアクセスしてOSから怒られた)
  • fault_address
    セグメンテーションフォルトが発生した際の命令ポインタのアドレス(ここに格納されている命令を実行しようとしてOSから怒られた)
  • stack_address
    セグメンテーションフォルトが発生した際のスタックポインタのアドレス
  • error_code
    セグメンテーションフォルトの分類。
    エラーコードは10進数で出力されるので2進数に変換して、どの様な分類で発生したのかを調べます。
    例えば、error 6 となっていれば、00110と変換できますので、「ユーザーモードで書き込みをしようとしたがページが見つからない」という意味になります。
    bit 0 == 0: ページがみつからない 1: 保護違反 
    bit 1 == 0: 読み込み 1: 書き込み 
    bit 2 == 0: カーネルモードアクセス 1: ユーザーモードアクセス
    bit 3 == 1: 予約ビットの使用が検出された 
    bit 4 == 1: 違反は命令フェッチ
    bit 5 == 1: 保護キーブロックアクセス
  • libso.so
    セグメンテーションフォルトが発生したコマンド(プロセス)がリンクしていた共有オブジェクト(共有ライブラリ)。
    この共有オブジェクトの内部でセグメンテーションフォルトが発生している場合もあるし、外部で発生して居る場合もあるので、この共有オブジェクトが悪いとは一概に言えない。
  • so_address
    上記の共有オブジェクトのアドレス
  • size
    上記の共有オブジェクトのサイズ(16進数) 
Debug Hacks -デバッグを極めるテクニック&ツール

Debug Hacks -デバッグを極めるテクニック&ツール

吉岡 弘隆, 大和 一洋, 大岩 尚宏, 安部 東洋, 吉田 俊輔
8,096円(12/01 15:32時点)
Amazonの情報を掲載しています

セグメンテーションフォルトの対応の難しさ

セグメンテーションフォルトの発生状況はパターンがあります。

  1. 何を入力しても毎回発生する。(例:Aを入力しても、Bを入力しても必ず発生する。)
  2. 入力内容によって発生するときと発生しない時がある。(例:Aを入力すると発生するが、Bを入力すると発生しない。)
  3. 同じ入力をしても、発生する時と発生しない時がある。(例:Aを入力して100回実行すると1回発生した。)

1 と 2 については、それぞれ対応方針は明確です。デバッガを使って発生までのバックトレースを取っていけば比較的簡単に解決できます。

まつもとゆきひろ氏は以下のとおり述べており、私も同感です。

 バグにはバグの見つけ方があり、直し方があるのです。

「デバッグ」という言葉は「バグを直すこと」のような印象がありますが、実際にはどこにあるのか特定されたバグはまったく恐ろしいものではなく、たいていはすぐに直すことができるものです。

デバッグの真髄はバグの発見と特定にあるのです。

Debug Hacks 推薦の言葉より引用

難しいのは 3 の発生条件がよくわからない場合です。この場合の対応(デバッグ)方針は以下の通りと考えます。デバッガだけがデバッグのツールではないのです。

  1. STRACEやcoreダンプを仕掛けて再現させる。
  2. 取得したSTRACEやcoreダンプを解析し、発生状況を認識する。
  3. 仮説を立て発生状況との整合性を検証する。
  4. 検証結果より原因を特定する。
  5. 原因を取り除き、再現しないことを確認する。

メインプロセスの終了処理とサブスレッドの動作の間

私が実際に遭遇して難儀した実例を紹介します。

発生事象

Linuxで動作していたAPが約3ヶ月に一回ぐらいのタイミングでセグメンテーションフォルトしていました。同一のAPが1日あたり500回は動作しているので、約4.5万回に1回の割合でセグメンテーションフォルトが発生していることになります。開発環境で再現確認したところ、同様に数万回実行して1回セグメンテーションフォルトが発生しました。

こうなるとおそらくプログラムに問題がありますが、特定のタイミングでしか顕在化しない事象であるとわかります。このプログラムは、メインプロセス(メインスレッド)とメインから生成されたサブスレッドが動作しています。

原因

STRACEを取りながら再現させたところ、メインプロセスのexit()の処理で共有オブジェクトのアンロード(メモリ上からの解放)を行った直後に、サブスレッドでアンロードした共有オブジェクトの処理が動作していました。

そして、サブスレッドが解放されたメモリアドレスにアクセスした結果、セグメンテーションフォルトになっていました。正常終了するときのSTRACEでは、メインプロセスのexit()の処理(共有オブジェクトのアンロードmunmap(2)からexit_group(2))は、約0.0001秒で終了していました。つまり、この約0.0001秒の間にサブスレッドが動作したためにセグメンテーションフォルトになっていました。

対応

ここまでわかれば対応はできます。メインプロセスでは、サブスレッドを待つなり強制終了させるなりすればよかったのです。 

最後に

今回は非常に稀なセグメンテーションフォルトの実例について記載しました。

セグメンテーションフォルトで困っている方の助けになれば幸甚です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です