__dyld_stub_binding_helper_interface で落ちてた件

状況としては、「yajit がコンパイルしたコードが yajit 内の関数を呼んだ後で libruby の関数を呼んだ場合にのみ」に落ちるというもので、 gdb で落としてみると、

Program received signal EXC_BAD_INSTRUCTION, Illegal instruction/operand.
0x8fe13184 in __dyld_stub_binding_helper_interface ()

とか言っていた。いかにも late binding 関係の問題っぽいので、 yajit が呼ぶ関数を、

typeof(rb_ary_new2)* RB_ARY_NEW2 = &rb_ary_new2;
typeof(rb_funcall2)* RB_FUNCALL2 = &rb_funcall2;
...

とかいう感じで強制的に bind してしまう、っていう雑な解決でとりあえずゴマかしたのだった。でもまぁ「yajit がコンパイルしたコードが yajit 内の関数を呼んだ後で libruby の関数を呼んでその関数がまた libruby のコードを呼んだ」ような落ちるケースがでてきてしまって、マジメに追うことになった。

とりあえず dyld のコードは Apple が配ってくれてる。とてもとてもありがたい。ただなんか OSX 10.5 のヤツと 10.4 のヤツのコードはだいぶ違うみたいだった。 10.5 では別の理由で動かなかったりするかもなー…という。

http://www.opensource.apple.com/darwinsource/

とりあえず落ち方はよく見ると bad instruction とか言ってるので相当ヘンだ。逆アセしてみたところ、

(gdb) disassemble $eip $eip+1
Dump of assembler code from 0x8fe13184 to 0x8fe13185:
0x8fe13184 <__dyld_stub_binding_helper_interface+18>:   movdqa %xmm0,32(%esp)

SSEのアラインメントかよ! という。該当個所のコードを読んでみると…

    .globl _stub_binding_helper_interface
_stub_binding_helper_interface:
	pushl		%ebp
	movl		%esp,%ebp
	subl		$STACK_SIZE,%esp		# at this point stack is 16-byte aligned because two meta-parameters where pushed
	movl		%eax,EAX_SAVE(%esp)		# save registers that might be used as parameters
	movl		%ecx,ECX_SAVE(%esp)
	movl		%edx,EDX_SAVE(%esp)
	movdqa		%xmm0,XMMM0_SAVE(%esp)    # ここで落ちた
	movdqa		%xmm1,XMMM1_SAVE(%esp)
	movdqa		%xmm2,XMMM2_SAVE(%esp)
	movdqa		%xmm3,XMMM3_SAVE(%esp)

まぁ要はとりあえずレジスタの値保存しておいてから bind する関数を呼ぼうって腹で、スタックにレジスタを保存してるんだと思う。んで stack pointer を見ると、

(gdb) p $esp
$1 = (void *) 0xbfffddfc

当然ながらアラインされてない。これはまぁ、 yajit が好き勝手にスタックを使ってて関数呼ぶ時のスタックの状態なんか気にしてないのがまずくて、たぶんアラインしなきゃいけないんだろう。

とりあえずてっとり早い解決としては export DYLD_BIND_AT_LAUNCH=1 とかしてから起動する手があって、これをしてやれば最初に全シンボルが bind されるので問題ない…んだけど、そんなことを強要するのは負けた気がする。

スタックの位置調整ってインラインアセンブリとかでは書きにくいしいちいち全関数に調整そのコード埋めるのもめんどくさいし、 __attribute__( (aligned(?))) をつけた変数配置するってのはスタック変数の計算がもっとめんどくさいし…と、どうしたもんかなーと思いました。でまぁなんか GCC の __attribute__ にあるんじゃないかと探してみたら、 __attribute__( (force_align_arg_pointer)) というのがありました。これを JIT コードが呼び出す yajit 内の関数につけてみたところ、解決したみたいです。

予想通りちょっと面白い感じでした。

ちなみにこの attribute をつけるとこんなコードを関数の頭につけるみたいです。

        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)

たぶん ECX に ESP つっこんで SP 変わりに使ってるんだと思います。で ESP はアラインしとく、と。

なんか適当にぐぐってみると、 force_align_arg_pointer は汎用性無いし遅いしよくないとかで、改善の提案があるみたいでした。

http://archives.free.net.ph/message/20080117.230310.aa614225.ja.html

あと force_align_arg_pointer 無しで同じことをやるハックをされてる方がいました。面白いなー。

http://kikyou.info/diary/?200610

って見たことあると思ったら吉里吉里の方か…どうりで。

Yajit (3)

つーわけで yajit アップデートしときました。

http://shinh.skr.jp/tmp/yajit.tgz

やったことは

  • 上に書いてある OSX の件
  • putself をいい加減に修正して const をいいかげんに実装したので benchmark モジュールが動くようにした
  • expandarray 実装したから a, *b = 3 とか動く
  • Cygwin/Mingw32 で #define DEBUG 無しでは落ちてたのを修正
  • RubyGC をほんの少し学んだので mark しなければならないものを mark するようにしたので大きいコードで落ちてたのがなおった
  • 毎度 mprotect するのはアホなので mmap した空間に置くようにしたので速くなった

とりあえず速くなったのはめでたいと思う。くだらないループのベンチマークとかだと ruby1.9 より速い。でも send とか getconstant は遅いんじゃないかなと思います。

ていうか Cygwin/Mingw32/MacOSX だとくだらないループは軽く5倍は速くなるんだよな。というか Ruby1.9 が遅い。 ktrace とかした感じだと、ブロック呼び出しのたびにシグナルハンドラいじったりしてるからかなぁという感じでした。

あとインラインアセンブリ内でメモリ壊したりメモリ参照とかする場合は "memory" ってつけとくと良いと知った。

次は

  • for が OSX で落ちる(というか OSX 以外で動いてるのが不思議。 ってなに)
  • 他の命令をもっと増やす
  • 可変長引数
  • throw の実装
  • 関数呼び出しのたびに #ifdef してるのをもうちょい抽象化
  • 最適化

あたりかね。

for は

each に化けてて引数が一旦入る場所が IFUNC とかになるだけなので現状で問題ないみたいだ。

OSX で落ちるのは GC が走って GC の中でまた SSE 命令があるとかみたいだ。 __attribute__ とかじゃなくて、ちゃんとスタック調整せんといかんね。となると push pop でゴマかすのはやめないといけないね…まぁ最適化とかやるならいずれやるべきだったんだろうけど。

ちなみに動作確認してる環境一覧

x86_64 の Windows, Mac OSX とか、 VC の Rub とか、OSX 10.5 とか *BSD あたりやってみたいところです。なんかもし動かした人とかいたら教えてください。とりあえず NetBSD はさっくりやるか…と思ったら Xen 入ってるカーネルが古いからやめてたんだった…

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h