プロフィール

kosaki

Author:kosaki
連絡先はコチラ

ブログ検索
最近の記事
最近のコメント
最近のトラックバック
リンク
カテゴリー
月別アーカイブ
RSSフィード
FC2ブログランキング

スポンサーサイト このエントリーをはてなブックマークに追加

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。


スポンサー広告 | 【--------(--) --:--:--】 | Trackback(-) | Comments(-)

Rubyで学ぶx86_64 ABI このエントリーをはてなブックマークに追加

ひさしぶりにプログラミングの話題でも。
現在のRubyのtrunkをx86_64上のFedora12でmake test-allすると8個ぐらいテストが失敗するのだが、その中の1つにこういうエラーがある

  1) Failure:
test_sin(DL::TestDL) [/home/kosaki/linux/ruby/test/dl/test_dl2.rb:95]:
<1.0> expected but was
<1.38523885234213e-309>.


test/dl/test_dl2.rbというのが、なにをしているテストかというと、ようするに以下のようにdlモジュールを使ってlibmのsin関数を呼んでいるわけだ

module DL
class TestDL < TestBase
# TODO: refactor test repetition
def test_sin()
pi_2 = Math::PI/2
cfunc = CFunc.new(@libm['sin'], TYPE_DOUBLE, 'sin')
x = cfunc.call([pi_2].pack("d").unpack("l!*"))
assert_equal(Math.sin(pi_2), x)
end
end
end # module DL


dlモジュールというのは(僕は知らなかったのだが)RubyからCのコードを直接呼ぶためのしくみで、引数をpackとunpackを使ってlongの配列にしておくと、なぜか関数の型に関わらず正しい結果がかえって来るという魔法のライブラリである。

まあ sin(π/2)が1.0にならなかったら、バグだよね。


では、その実装をみていこう


static VALUE
rb_dlcfunc_call(VALUE self, VALUE ary)
{
struct cfunc_data *cfunc;
int i;
DLSTACK_TYPE stack[DLSTACK_SIZE];
VALUE result = Qnil;

rb_secure_update(self);

memset(stack, 0, sizeof(DLSTACK_TYPE) * DLSTACK_SIZE);
Check_Type(ary, T_ARRAY);

TypedData_Get_Struct(self, struct cfunc_data, &dlcfunc_data_type, cfunc);

if( cfunc->ptr == 0 ){
rb_raise(rb_eDLError, "can't call null-function");
return Qnil;
}

for( i = 0; i < RARRAY_LEN(ary); i++ ){
if( i >= DLSTACK_SIZE ){
rb_raise(rb_eDLError, "too many arguments (stack overflow)");
}
rb_check_safe_obj(RARRAY_PTR(ary)[i]);
stack[i] = NUM2LONG(RARRAY_PTR(ary)[i]);
}


まず、RubyのArray(ary)をCのArray(stack[])に変換する。


switch( cfunc->type ){
case DLTYPE_VOID:
#define CASE(n) case n: { \
DECL_FUNC_CDECL(f,void,DLSTACK_PROTO##n) = cfunc->ptr; \
f(DLSTACK_ARGS##n(stack)); \
result = Qnil; \
}
CALL_CASE;
#undef CASE
break;
・・・

case DLTYPE_DOUBLE:
#define CASE(n) case n: { \
DECL_FUNC_CDECL(f,double,DLSTACK_PROTO##n) = cfunc->ptr; \
double ret; \
ret = f(DLSTACK_ARGS##n(stack)); \
result = rb_float_new(ret); \
}
CALL_CASE;
#undef CASE


んで、CFunc.newで渡したtypeに応じてクソ長い switch caseが始まる。ここでさらに引数の数に応じて場合分けしたいが最大引数数がrubyのdlモジュールの仕様上20個もあるため、いちいち書くのがめんどくさいのでCASE, CALLCASEマクロで量産している。CALL_CASEマクロの定義は以下


#define CALL_CASE switch( RARRAY_LEN(ary) ){ \
CASE(0); break; \
CASE(1); break; CASE(2); break; CASE(3); break; CASE(4); break; CASE(5); break; \
CASE(6); break; CASE(7); break; CASE(8); break; CASE(9); break; CASE(10);break; \
CASE(11);break; CASE(12);break; CASE(13);break; CASE(14);break; CASE(15);break; \
CASE(16);break; CASE(17);break; CASE(18);break; CASE(19);break; CASE(20);break; \
default: rb_raise(rb_eArgError, "too many arguments"); \
}


というわけで、cfunc.call()すると


DECL_FUNC_CDECL(f,double,DLSTACK_PROTO##n) = cfunc->ptr; \


はCFunc.newでlookupした関数ポインタcfunc->ptr(この場合はsin())をfに代入して、


double ret; \
ret = f(DLSTACK_ARGS##n(stack)); \


fをcallして


result = rb_float_new(ret); \


それをrubyのfloatオブジェクトに変換して返す。ということをしているのだと分かる。実装の詳細を見るために
DLSTACK_PROTO1
DECL_FUNC_CDECL
DLSTACK_ARGS1
のマクロ定義も見ていこう。(あ、今回は引数1つなので##nは1になる。CALL_CASEマクロをもう一度みると納得できると思います。)

まず、DLSTACK_PROTO1とDECL_FUNC_CDECLから見る

#define DLSTACK_TYPE long
#define DLSTACK_PROTO1_ DLSTACK_TYPE
#define DLSTACK_PROTO1 DLSTACK_PROTO1_, ...

#if !defined(FUNC_CDECL)
# define FUNC_CDECL(x) x
#endif
# define DECL_FUNC_CDECL(f,ret,args) ret (FUNC_CDECL(*f))(args)


ようするに DLSTACK_PROTO1 → "long, ..." で結果として

DECL_FUNC_CDECL(f,double,DLSTACK_PROTO##n) = cfunc->ptr; \

という行は

double (*f)(long, ...) = cfunc->ptr;

となる。

わざわざ可変長引数にしている理由は、すぐそばに以下のコメントががが

/*
* Add ",..." as the last argument.
* This is required for variable argument functions such
* as fprintf() on x86_64-linux.
*
* http://refspecs.linuxfoundation.org/elf/x86_64-abi-0.95.pdf
* page 19:
*
* For calls that may call functions that use varargs or stdargs
* (prototype-less calls or calls to functions containing ellipsis
* (...) in the declaration) %al is used as hidden argument to
* specify the number of SSE registers used.
*/


つまり、x86_64では...を特別扱いするから、これでうまくいくんだよ。と言いたいようだ。

でもって、DLSTACK_ARGS1は


#define DLSTACK_ARGS1(stack) stack[0]


なので、

f(DLSTACK_ARGS##n(stack)); \



f(stack[0]);

という普通の関数呼び出しになる。

あれ?なんかx86_64対策も入ってるのになんでうまくいかんのだ?という話になるので、参照されているx86_64 ABI
の原典を読む



3.2.3 Parameter Passing

After the argument values have been computed, they are placed in registers, or
pushed on the stack. The way how values are passed is described in the following
sections.

Definitions We first define a number of classes to classify arguments. The
classes are corresponding to AMD64 register classes and defined as:

INTEGER This class consists of integral types that fit into one of the general
purpose registers.
SSE The class consists of types that fits into a SSE register.
SSEUP The class consists of types that fit into a SSE register and can be passed
and returned in the most significant half of it.
X87, X87UP These classes consists of types that will be returned via the x87 FPU.
COMPLEX_X87 This class consists of types that will be returned via the x87 FPU.
NO_CLASS This class is used as initializer in the algorithms. It will be used for
padding and empty structures and unions.
MEMORY This class consists of types that will be passed and returned in memory
via the stack.


超訳。ABI的にはINTEGER, SSE, SSEUP, X87, X87UP, COMPLEX_X87, NO_CLASSという
引数のタイプがあるよ。



Classification The size of each argument gets rounded up to eightbytes.
The basic types are assigned their natural classes:

• Arguments of types (signed and unsigned) _Bool, char, short, int,
long, long long, and pointers are in the INTEGER class.
• Arguments of types float, double and __m64 are in class SSE.
(しばらく略)


超訳
(signed and unsigned) _Bool, char, short, int, long, long long, およびポインタは INTEGERだよ
float, double and __m64 はSSEだよ

(この後、__float128とか構造体とかの説明がつづくが割愛)



Passing Once arguments are classified, the registers get assigned (in left-to-right
order) for passing as follows:

1. If the class is MEMORY, pass the argument on the stack.
2. If the class is INTEGER, the next available register of the sequence %rdi,
%rsi, %rdx, %rcx, %r8 and %r9 is used.
3. If the class is SSE, the next available SSE register is used, the registers are
taken in the order from %xmm0 to %xmm7.
4. (略)
5. (略)


超訳。
MEMORYならスタック渡し確定
INTEGERならrdi, rsi, rdx, rcx, r8, r9の順に使うよ
SSEならSSEレジスタをxmm0からxmm7の順につかうよ


If there is no register available anymore for any eightbyte of an argument, the
whole argument is passed on the stack. If registers have already been assigned for
some eightbytes of this argument, those assignments get reverted.

Once registers are assigned, the arguments passed in memory are pushed on
the stack in reversed (right-to-left) order.


超訳
レジスタが足りなくなったらスタックを使って引数を渡すよ。
スタックには逆順(右から左)でpushするよ


で、ここで問題のセンテンスが現れる

For calls that may call functions that use varargs or stdargs (prototype-less
calls or calls to functions containing ellipsis (. . . ) in the declaration) %al is used
as hidden argument to specify the number of SSE registers used. The contents of
%al do not need to match exactly the number of registers, but must be an upper


超訳
varargsまたはstdargsな関数(プロトタイプがなかったり、宣言で...を使っていたり)を呼ぶときは
%al に何個SSEレジスタを使ったか隠し引数で入れるよ。


つまり、ここではprintf()的なまっとうな可変長引数の事を語っているだけであって、呼び出し元と呼び出し先で
関数の型が適合しない場合については何も語っていない。

今回の場合で言うとsin関数はlibmの中では
double sin(double)
という型でコンパイルされていて、呼び出し側では
double f(long, ...)
という型だと思ってcallしている。

じゃあ、実際に何が起こるかを示すためにテストプログラムを書いてみた。

main.c

#define _GNU_SOURCE
#include
#include

void f1(void) {
double (*func)(long, ...);
double d = 57.0;
long l = *(long*)&d;
double ret;

func = dlsym(RTLD_DEFAULT, "func");
ret = func(l);
printf("%f\n",ret);
}

int main(void)
{
f1();
return 0;
}


f.c

double func(double d)
{
return d*2;
}


実行結果

% gcc *.c -ldl -Wall -g -rdynamic; ./a.out
0.000000


はい、57*2で114 が返ってくるはずが0が返ってきてしまいました。

んではdisassemble,


0000000000400834 :
400834: 55 push %rbp
400835: 48 89 e5 mov %rsp,%rbp
400838: f2 0f 11 45 f8 movsd %xmm0,-0x8(%rbp)
40083d: f2 0f 10 45 f8 movsd -0x8(%rbp),%xmm0
400842: f2 0f 58 c0 addsd %xmm0,%xmm0
400846: c9 leaveq
400847: c3 retq


funcでは素直にxmm0を引数だと思って二倍しています。


00000000004008e0 :
4008e0: 55 push %rbp
4008e1: 48 89 e5 mov %rsp,%rbp
4008e4: 48 83 ec 20 sub $0x20,%rsp
4008e8: 48 b8 00 00 00 00 00 mov $0x404c800000000000,%rax
4008ef: 80 4c 40
4008f2: 48 89 45 e0 mov %rax,-0x20(%rbp)
4008f6: 48 8d 45 e0 lea -0x20(%rbp),%rax
4008fa: 48 8b 00 mov (%rax),%rax
4008fd: 48 89 45 f0 mov %rax,-0x10(%rbp)
400901: b8 48 0b 40 00 mov $0x400b48,%eax
400906: 48 89 c6 mov %rax,%rsi
400909: bf 00 00 00 00 mov $0x0,%edi
40090e: e8 2d fe ff ff callq 400740
400913: 48 89 45 e8 mov %rax,-0x18(%rbp)
400917: 48 8b 45 f0 mov -0x10(%rbp),%rax
40091b: 48 8b 55 e8 mov -0x18(%rbp),%rdx
40091f: 48 89 c7 mov %rax,%rdi
400922: b8 00 00 00 00 mov $0x0,%eax
400927: ff d2 callq *%rdx
400929: f2 0f 11 45 f8 movsd %xmm0,-0x8(%rbp)
40092e: b8 4d 0b 40 00 mov $0x400b4d,%eax
400933: f2 0f 10 45 f8 movsd -0x8(%rbp),%xmm0
400938: 48 89 c7 mov %rax,%rdi
40093b: b8 01 00 00 00 mov $0x1,%eax
400940: e8 db fd ff ff callq 400720
400945: c9 leaveq
400946: c3


でもf1ではrdi(INTEGERの第一引数)に l を、eaxに0を入れて関数コールしています。

つまり、これが起きていることのすべてです。
呼び出し側がrdiに引数を詰んでいるのに、呼び出され側はxmm0を呼んでいるのですから
正しい答えが返るはずがありません。

というわけで、これはdlモジュール制作者のABIの誤読ですね。
なお、Cの規格的には呼び出し元と呼び出し先で関数型が適合しない場合は未定義。たぶん、dlモジュール作者は規格を元に考えたのではなく

1)全CPUアーキテクチャでlongで無理矢理callしても安心と考えた。または
2)x86_32 だけで動けばいいや。と割り切った

のどちらかだと思いますが、イマドキx86_64は普通ですからねぇ。。。

現在のインターフェースだと直しようがないので、RubyのCFuncの仕様を大きく変えないと x86_64対応は出来ないでしょう。
というわけで、RubyユーザのみなさんはCFuncでfloat, doubleを使わないようにしておくと将来幸せになれるかもしれないというお話でした。

ではでは


追記: なぜこの問題がx86_64だけで発生するのかを、つらつらと考えてみたのだが、やはり歴史的な理由だろう。昔は関数宣言を忘れるとか当たり前だったので、昔からあるアーキだと、こういう関数型が不適合な呼び出しに対してABIを寛容に作ることが多かったのではないか?

追記2: 関数の型の適合性とか安易に書いた気がする。でもCの規格を読み直す気がおきない。めどい
簡単にいうと
1)呼び出し元と呼び出し先が両方とも関数宣言有り: 引数、戻り値の型が全部一致していた場合のみOK
2)どちらかが宣言なし: 戻り値の型は厳密一致が必要だけど、引数は型がshortとintみたいに十分近ければOK
みたいなノリだったと思う。今回はケース1だけど、2の基準で反転してもアウトという論外さ

関連記事


プログラミング | 【2010-01-19(Tue) 23:47:20】 | Trackback:(0) | Comments:(5)
コメント
DLは長い間メンテされていなくて、yuguiさんはじめコミッタの方々は代替を探しています。
ということで、何も考えず gem install ffi しましょう。
2010-01-19 火 10:17:20 | URL | 斎藤ただし #9cg9ikIo [ 編集]

実のところ、dlを使いたいという要求はまったくなくて、たんにtest-allが通らないのが悔しいので深追いしてみただけなのでした。
なので、代替案を提示されても、あんまり御利益がなかったり。すまぬ
2010-01-19 火 10:36:04 | URL | kosaki #- [ 編集]

x86-64のABIはきちんと決まっているよね。仕様をみる限り、できるだけレジスタ渡しにしておけばスタックの消費も少ないし、速いよ、というのと、浮動小数点レジスタがない可能性があるx86-32と違って、初めから浮動小数点を渡す専用レジスタがあるんだからそれを使おう、という考えなのではないかと。

あれ、そうすると、x86-32でも-mregparm=3でコンパイルしたライブラリはどうなんだろうか?exportされるときにスタック渡しに戻されるのかな?
2010-01-19 火 13:32:41 | URL | mhiramat #- [ 編集]

gccが *.o を作るフェイズではライブラリエクスポートされるかどうかの知識はまったくないので、普通に呼んだら同じ事が起きるのではないかと。
2010-01-19 火 19:22:07 | URL | kosaki #- [ 編集]

ちょうど今日、DLの実装が割とごっそり(libffiを利用するように)入れ替わった
らしいので、再度test-allしてみると、x86_64でなにか進展があるかもしれません。
# ついでにIA-64のレポートとか…(ボソッ
2010-02-03 水 04:46:41 | URL | 斎藤ただし #9cg9ikIo [ 編集]
  1. 無料アクセス解析
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。