FIELD NOTES: 書を持って街へ出よう

合同会社フィールドワークス プログラマ兼代表のブログ

OCamlで作成した拡張モジュールでセグメンテーション違反発生

以前のブログでも書いたとおり,弊社ではOCamlを使ってLL言語用の拡張ライブラリを作成しています。
この構成でこれまでは特に問題なく稼動していたのですが,機能拡張していくうちに突然セグメンテーション違反が発生するようになってしまい困りました。
現在のところ根本的な原因は判明していないのですが,これまでの調査結果と見つかった回避策をいったんまとめておきます。

現象

セグメンテーション違反は,64ビット版Linuxのみで発生します。32ビット版LinuxMac OS Xでは発生しません。使用するLL言語によっても違いがあり,Pythonでは発生せず,PerlRubyで発生します(PHPは未確認)。
動作環境は,CentOS 5.5 です。

gdbによる解析

OCamlで作成した共有ライブラリをfoo.soとすると,その共有ライブラリを各LL言語用の拡張ライブラリfoo_py.so,foo_rb.so, foo_pl.soが利用する構造になっています。foo.soは共通なので,呼び出し方によってセグメンテーション違反が発生したりしなかったりすることになります。
違いがどこにあるのか動作をデバッガで追ってみました。

まず,セグメンテーション例外が発生する直接の原因は範囲外のメモリにアクセスしたためでした。rsiレジスタにはデータ格納領域のアドレスが格納されているはずなのですが,でたらめな値になっています。

(gdb) info reg
rax            0x930000002a95f07e     -7854277749419675522
rbx            0x2a95f05798     182904182680
rcx            0x2a95f05002     182904180738
rdx            0x7bb     1979
rsi            0x930000002a95f07e     -7854277749419675522
rdi            0x2a95c13b70     182901095280
rbp            0x2a95f057f8     0x2a95f057f8
rsp            0x7fbfffe810     0x7fbfffe810
r8             0x1     1
r9             0x1     1
r10            0x0     0
r11            0x2a95ebd630     182903887408
r12            0x2a95f06180     182904185216
r13            0x2a95f06078     182904184952
r14            0x7fbfffe9d0     548682066384
r15            0x2a95f05768     182904182632
rip            0x2a95c13b77     0x2a95c13b77 <camlField__Pdfreport__set_field_value_3874+7>
eflags         0x10202     66050
cs             0x33     51
ss             0x2b     43
ds             0x0     0
es             0x0     0
fs             0x0     0
gs             0x0     0

後は,レジスタが破壊される場所を絞り込んでいけば原因が推測できそうですが,これが一筋縄では行きませんでした。
この手のローレベルなデバッグではありがちなことですが,ブレークポイントを設定することでプログラムの動作が影響を受けてしまうようなのです。ブレークポイントを細かく設定して発生場所を絞り込んでいくと,捜査範囲外で例外が発生するなどしてなかなかうまくいきません。

ただ,試行錯誤を続けるうちになんとなく傾向が見えてきました。以下のような“XXX@plt”という形式のシンボルへサブルーチンコールした直後にレジスタが破壊されることが多いようなのです。

callq  0x2a95b2e780 <camlField__Extlib__ExtList__loop_1124@plt>

“XXX@plt”というのは,動的リンクを行う際に遅延リンクを実現するための仕組みです。PLTと呼ばれるテーブルにサブルーチンのアドレスが並んでいて,サブルーチンコールはPLTを介して間接的に行うことになります。共有ライブラリロードした直後にはPLTには実際のアドレスは格納されておらず,最初のサブルーチンコールを行った際に動的にシンボルを解決します。

動的リンクの仕組みの詳細については,参考文献を参照してください。特に「リンカ・ローダ実線開発テクニック」が非常に詳しくてお勧めです。

共有ライブラリ呼び出し側の違い

一方,Pythonで動いてRubyPerlで動かないということは,共有ライブラリの呼び出し方に何か違いがあるのかもしれません。

Rubyのソースを調べたところ,RTLD_LAZYフラグを指定して共有ライブラリをオープンしていました。

        /* Load file */
        if ((handle = (void*)dlopen(file, RTLD_LAZY|RTLD_GLOBAL)) == NULL) {
            error = dln_strerror();
            goto failed;
        }

Pythonでは,フラグの値が変数に格納されているので動作時に実際にどのモードで動くのかをソースから解析するのは困難でしたが,少なくともdlopenflagsの初期値としてはRTLD_NOWが設定されているようでした。

#if !(defined(PYOS_OS2) && defined(PYCC_GCC))
    dlopenflags = PyThreadState_GET()->interp->dlopenflags;
#endif

    handle = dlopen(pathname, dlopenflags);

RTLD_LAZYモードでは,シンボル解決が最初の呼び出し時まで遅延されます。RTLD_NOWモードでは,共有ライブラリのロード時にシンボル解決が行われます。

実際にデバッガでステップ実行すると,Rubyから起動した場合は“XXX@plt”のコール後にシンボル解決ルーチンへジャンプしますが,Pythonから起動した場合はPLTテーブルに既に有効なアドレスが格納されていてシンボル解決ルーチンへのジャンプは発生しません。

考察

遅延シンボル解決ルーチンは決して実行環境に影響を与えないように設計されているはずですが,何らかの理由により実行環境に影響を与えてしまっているのではないでしょうか?

例えば,以下のような理由が考えられます。

  • OCamlが出力するコードが,遅延シンボル解決利用時の規約に違反している。
  • 共有ライブラリが,シンボル解決ルーチンの想定を超える構造になっている(例えば,シンボル数が多すぎるなど)。

ただ,遅延シンボル解決を利用する際の規約のようなものが存在するのかどうか分かりませんし,私自身のマシン語の知識が8ビット時代で止まっているので,マシン語コードがなにをやっているのか深く理解するのが困難な状況です。これ以上の深追いができなくて,調査が中断しているというのが実情です。

ちなみに,64ビット版のみで問題が発生する理由は,OCamlが出力するアセンブラコードの性質が異なるためです。32ビット版では遅延シンボル解決に対応した“XXX@plt”形式のシンボルを出力しません。

回避策

共有ライブラリロード時にRTLD_NOWモードで開かせることができれば,回避することができます。
幸い,環境変数LD_BIND_NOWにより遅延シンボル解決を抑制することができます。

$ export LD_BIND_NOW=1

この方法では他の共有ライブラリにも影響を与えてしまいますが,共有ライブラリを作成する際にリンカに“-z now”オプションを与えれば,遅延シンボル解決を抑制する対象を絞り込むことができます。

$ gcc -shared -Wl,-z,now -o foo.so foo.o

“-z now”オプションを付ける方法で,しばらく様子を見てみようと思います。

参考文献

  • 坂井弘亮 著, 「リンカ・ローダ実線開発テクニック」, CQ出版, 2010年
  • 高林哲 他著, 「Binary Hacks ―ハッカー秘伝のテクニック100選」, オーム社, 2006年
  • John R. Levine 著, 榊原一矢 監訳, 「Linkers & Loaders」, オーム社, 2001年
  • 青木峰郎 著, 「ふつうのコンパイラをつくろう」, ソフトバンク クリエイティブ, 2009年