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年

現在契約できるプロバイダフリーのIP電話

ナンバー 電話 ビジネス  - 写真素材
(c) nakusa123画像素材 PIXTA

現在,050 plusのようなスマートフォンから利用できるIP電話サービスが開始され好評の様ですが,使用する端末やアプリが限定されてしまうのがちょっと不満に思えます。一方,各ISPがひかり電話を始めとするIP電話サービスを提供していますが,この場合は使用する回線が限定されてしまいます。

好みのIP電話アプリや,自宅サーバにインストールしたAsteriskなどのSIPサーバに自由に登録して使用することはできないものでしょうか?

このような制限のないIP電話サービスは「プロバイダフリーのIP電話サービス」と呼ばれ,海外にはたくさん存在します。日本でも2005年前後にいくつか存在したのですが,規制か何かの影響ですっかり廃れてしまいました。

それから数年経って,またボチボチとサービスが立ち上がってきたようですので,現在契約可能なプロバイダフリーのIP電話を調べて見ました。

選定の条件は,以下のとおりです。

  • 使用回線が限定されない(プロバイダフリー)。
  • 標準的なIP電話プロトコル(SIP等)に準拠していて,専用アプリケーションを必要としない。
  • 日本の電話番号が取得できる。
  • 個人で契約できる。

どこでもIPフォン

URL:http://www.ip-denwa.com/
電話番号:03-xxxx-yyyy
月額基本料金:1,000円
通話料金:(固定電話)8.4円/3分,(携帯電話)18.27円/分
その他の特長:内線通話,転送電話設定可能

FleaLine Light

URL:http://www.covia.jp/net/flealine01.html
電話番号:050-xxxx-7777 または 03-xxxx-yyyy(オプション)
月額基本料金:400円
通話料金:(固定電話)8.4円/3分,(携帯電話)16.8円/分
その他の特長:転送電話設定可能(オプション)

Phytter

URL:http://www.phytter.com/
電話番号:050-xxxx-7777 または 03-xxxx-eye
月額基本料金:5ドル/月
通話料金:(固定電話)3セント/分,(携帯電話)15セント/分
その他の特長:PayPalでドル決済

まとめ

以上,条件に合ったIP電話サービスが3件見つかりました。

試しにPhytterに加入しiPhone上のAcrobits SoftPhoneに登録してみましたが,問題なく発信できました。

VMware ESXi 5.0でVNC接続を利用する方法

最近,VMware ESXi を5.0に移行したところ,今まで使えていたVNCでの接続ができなくなってしまいました。

VMWareサーバにログインしてログファイルを調べてみたところ,以下のようなメッセージを発見。

vmauthd.log:2011-09-05T14:47:33Z vmauthd[11156]: vthread-3| VMAuthdSocketWrite: writing: 220 VMware Authentication Daemon Version 1.10: SSL Required, ServerDaemonProtocol:SOAP, MKSDisplayProtocol:VNC , VMXARGS supported

どうやら,SSH経由でVNC接続する必要があるようです。

VMware ESXiでのVNC接続を有効にする方法

以下の記事などを参考にVNC接続を有効にする設定を行ってください。

なお,*.vmxを直接編集しなくても,VMware vSphere Clientの「仮想マシンのプロパティ/オプション/詳細/全般/構成パラメータ…」からパラメータを変更することができます。

SSHのポートフォワーディング機能を使う方法

ターミナルから以下のコマンドを実行します。

$ ssh -L <ローカルホストポート>:localhost:<リモートホストポート> <ユーザ名>@<VMwareサーバ名/アドレス>

ここで,リモートホストポートは,各仮想マシンに割り当てたポート番号です(通常,5900以上)。
ローカルホストポートは任意のポート番号で良いのですが,私はリモートホストポートと同じ番号にしています。
ユーザ名は,VMwareサバーへログインできるユーザ名です(通常“root”?)。

SSH Tunel Managerを使う方法(Mac OS X専用)

SSH Tunel Managerをインストールして,上記のSSHと同様の設定を行います。
f:id:fet:20110905154513p:image

設定が終わったらウインドウを閉じて「再生」ボタンを押せば,SSHトンネリングが確立します。

VNCでの接続

これで,お好みのVNCクライアントを使って接続できるはずですが,接続先ホスト名が“localhost”になることに注意してください。

ちなみに,Mac OS Xの場合,OS標準の「画面共有」を使ってVNC接続できます。
Finderから「移動/サーバへ接続…」を選択して,「サーバアドレス:」に以下のように入力してください。

vnc://localhost:<ポート番号>/

iCalとiPhoneをTODO込みで同期をとる方法

iCalとiPhoneを連携させるには Google カレンダーを媒介として同期を取る方法が定番ですが,残念なことに TODO(リマインダー)の同期が取れません。
GoTasksなど,iPhoneからGoogle の「ToDo リスト」を参照できるようにするAppは存在するようなのですが,iCalとGoogleを連携させる方法がどうしても見つかりませんでした。

そこで発想を変えて Yahoo! Calendar を媒介とする方法を実験してみたところ,うまくいきましたので報告します。

Yahoo!(US)のアカウントを取得する

まず,Yahoo!本家のアカウントを取得します。

Calendarアプリを選択して,試しにいくつかスケジュールとTODOを入力してみました。
f:id:fet:20110902174232p:image:w640

iCalにYahoo!アカウントの情報を登録する

次に,iCalにYahoo!アカウントの情報を登録します。

「環境設定/アカウント」ダイアログを開いて,アカウントを追加します。
f:id:fet:20110902174442p:image:w480


どうやら,CalDAVを使って同期をとっているようです。
f:id:fet:20110902174443p:image:w480


これで,iCal と Yahoo! Calendar の同期が取れるようになりました。
f:id:fet:20110902174444p:image:w640

iPhoneにYahoo!アカウントの情報を登録する

つぎは,iPhone に Yahoo! アカウントの情報を登録します。

「設定」 → 「メール/連絡先/カレンダー」 と進んで「アカウントを追加…」を選択し,「Yahoo!」のロゴを選びます。
f:id:fet:20110902180903p:image:w320


次の画面では,「カレンダー」の同期を「オン」にします。
f:id:fet:20110902175937p:image:w320


これで,iPhone と Yahoo! Calendar の同期が取れるようになりました。
「カレンダー」アプリを起動すると,スケジュールが確認できます。
f:id:fet:20110902175938p:image:w320


ただし,iOSの標準アプリだけでは TODO を参照することができません。
ここまでうまく行ったのに残念!

BusyToDoをインストールして,Yahoo!アカウントの情報を登録する

そこで,Yahoo! Calendar のクライアントとなるアプリがないか色々探したのですが見つかりません。
最後に,iCalがCalDAVを使っているのを思い出して,CalDAVに対応しているタスク管理アプリを試してみたところ,BusyToDo(850円)で成功しました!!

BusyToDoは,本来MobileMeを想定してCalDAVに対応しているようですが,「ユーザ名」にyahooのメールアドレスを入力したところあっさり接続できました。
f:id:fet:20110902175939p:image:w320


機能は,非常にシンプルです。
f:id:fet:20110902175940p:image:w320


他には,2Do(600円)がMobileMe(CalDAV)に対応しているようだったので試してみましたが,こちらはうまく接続できませんでした。

まとめ

Yahoo! Calendar を媒介とすることで,iCalとiPhoneのスケジュールとタスクを同期させることに成功しました。主に使用するiCalでスケジュールとタスクの管理が一元化できるようになったので,とてもすっきりしました。

ただ,iCloud のサービスが開始されれば,このようなテクニックは不要になるんでしょうね。