Create  Edit  Diff  FrontPage  Index  Search  Changes  Login

__stdcallと__cdecl

__stdcallと__cdecl

__stdcallは、WIN32 APIの呼び出し規約で、スタックの解放を呼ばれた側が行い(したがって、varargは使用不可)、__cdeclは、スタックの解放を呼ぶ側が行う(したがって、varargを使用可能)。

テストコード

#include <stdio.h>
int __stdcall scall(int a, int b)
{
    printf("a=%d,b=%d\n", a, b);
    return a * b;
}
int __cdecl ccall(int a, int b)
{
    printf("a=%d,b=%d\n", a, b);
    return a * b;
}
int main(int argc, char* argv[])
{
    scall(1, 2);
    ccall(1, 2);
    return 0;
}

アセンブラリスト

__stdcall

?scall@@YGHHH@Z PROC NEAR				; scall, COMDAT
; 4    : {
 00000	56		 push	 esi
; 5    : 	printf("a=%d,b=%d\n", a, b);
 00001	8b 74 24 0c	 mov	 esi, DWORD PTR _b$[esp]
 00005	57		 push	 edi
 00006	8b 7c 24 0c	 mov	 edi, DWORD PTR _a$[esp+4]
 0000a	56		 push	 esi
 0000b	57		 push	 edi
 0000c	68 00 00 00 00	 push	 OFFSET FLAT:??_C@_0L@IIPB@a?$DN?$CFd?0b?$DN?$CFd?6?$AA@ ; `string'
 00011	e8 00 00 00 00	 call	 _printf
 00016	83 c4 0c	 add	 esp, 12			; 0000000cH
; 6    : 	return a * b;
 00019	8b c7		 mov	 eax, edi
 0001b	0f af c6	 imul	 eax, esi
 0001e	5f		 pop	 edi
 0001f	5e		 pop	 esi
; 7    : }
 00020	c2 08 00	 ret	 8

ret 8は、引数の分スタックポインタを余分に戻している。8はsizeof(int)×2

__cdecl

?ccall@@YAHHH@Z PROC NEAR				; ccall, COMDAT
; 10   : {
 00000	56		 push	 esi
; 11   : 	printf("a=%d,b=%d\n", a, b);
 00001	8b 74 24 0c	 mov	 esi, DWORD PTR _b$[esp]
 00005	57		 push	 edi
 00006	8b 7c 24 0c	 mov	 edi, DWORD PTR _a$[esp+4]
 0000a	56		 push	 esi
 0000b	57		 push	 edi
 0000c	68 00 00 00 00	 push	 OFFSET FLAT:??_C@_0L@IIPB@a?$DN?$CFd?0b?$DN?$CFd?6?$AA@ ; `string'
 00011	e8 00 00 00 00	 call	 _printf
 00016	83 c4 0c	 add	 esp, 12			; 0000000cH
; 12   : 	return a * b;
 00019	8b c7		 mov	 eax, edi
 0001b	0f af c6	 imul	 eax, esi
 0001e	5f		 pop	 edi
 0001f	5e		 pop	 esi
; 13   : }
 00020	c3		 ret	 0

素直なretの呼び出し。

呼び出し側

; 17   : 	scall(1, 2);
 00018	6a 02		 push	 2
 0001a	6a 01		 push	 1
 0001c	e8 00 00 00 00	 call	 ?scall@@YGHHH@Z		; scall
; 18   : 	ccall(1, 2);
 00021	6a 02		 push	 2
 00023	6a 01		 push	 1
 00025	e8 00 00 00 00	 call	 ?ccall@@YAHHH@Z		; ccall
 0002a	83 c4 08	 add	 esp, 8

見てのとおり、__cdeclを呼んだ場合、最後にespに8(引数のint × 2)を加えている。

dl.soの呼び出し個所をチェック

以下、VC++6SP5を利用しているASRのdl.soを利用して実際の動作をチェックしてみよう。

問題となっているのは、dl.soを利用してWIN32 APIを呼ぶと、__stdcallな関数を__cdeclとして呼び出すことだ。

この結果、APIから復帰すると、APIの引数のサイズだけespが余分に加算され、スタックトップが下にずれる。

スタックトップが下にずれれば、本来使用すべき自動変数が破壊されて問題が起きそうに見える。しかし、実際にはext/dl/sample/msgbox.rb などは正常に動作する。

この理由を見てみよう。

sym.cの

f(DLSTACK_ARGS);

は、

ff		 mov	 eax, DWORD PTR _stack$56496[ebp+56]
 011b2	50		 push	 eax
...
ff		 mov	 edx, DWORD PTR _stack$56496[ebp]
 01214	52		 push	 edx
 01215	ff 95 b8 fe ff
ff		 call	 DWORD PTR _f$56589[ebp]
 0121b	83 c4 3c	 add	 esp, 60			; 0000003cH
 0121e	89 45 e0	 mov	 DWORD PTR _ret$[ebp], eax

という呼び出しになる。見てのとおり、arg0からarg14までの15 × sizeof(long) = 60がespに加算されている。

この後、sym.cでは、689行目からの

 switch( sym->type[0] ){
 case '0':
   val = Qnil;
   break;

という、戻り値をrubyのオブジェクトに変換する処理となる。ここでespは、本来スタックポインタがさすべき位置よりWIN32 APIの実引数のサイズ分、スタックの上位位置をさしていることになる。これ以降、rb_dlptr_newなどの関数を呼び出せば、使用中のスタックを破壊することになる。また、仮にval = Qnil;であっても、最終的には

 dvals = rb_ary_new();

に到達するため、いずれにしろスタックは破壊される。

では、破壊される自動変数はなんだろうか?

それが、破壊されても影響がないものであれば、呼び出されたWIN32 APIの引数の数によってOKであったりNGであったりするだろう。

;	COMDAT _rb_dlsym_call
_TEXT	SEGMENT
_str$56432 = -56
_str$56442 = -60
_stk_size$56494 = -308
_stack$56496 = -304
_sp$56497 = -64
_v$56517 = -312
_v$56523 = -316
_v$56529 = -320
_f$56572 = -324
_f$56589 = -328
_f$56606 = -332
_f$56623 = -336
_f$56640 = -340
_f$56657 = -344
_f$56674 = -348
_f$56691 = -352
_f$56708 = -356
_c$56778 = -360
$T56977 = -364
$T56978 = -368
$T56988 = -372
$T56989 = -376
$T56999 = -380
$T57000 = -384
$T57010 = -388
$T57011 = -392
_argc$ = 8
_argv$ = 12
_self$ = 16
_sym$ = -12
_args$ = -16
_dargs$ = -8
_ret$ = -32
_dtypes$ = -20
_val$ = -4
_dvals$ = -40
_i$ = -36
_ftype$ = -24
_func$ = -44
_data$56272 = -52
_pval$56273 = -48
_rb_dlsym_call PROC NEAR				; COMDAT

どうやら$Txxxxといった変数群だ。これは何か? これらの$Txxxxs(9個ある)が破壊されても影響がない変数ならば、結構、マージンがあることになる。

実際に使用されている個所をチェックすると350行目以降のrubyのオブジェクトをネイティブな型に変換している個所だとわかる。使用方法は、完全に使い捨ての、ソース上にはあらわれない変数としてだ。

; 406  :       ANY2H(dargs[i]) = DLSHORT(NUM2CHR(argv[i]));
 006a3	8b 45 dc	 mov	 eax, DWORD PTR _i$[ebp]
 006a6	8b 4d 0c	 mov	 ecx, DWORD PTR _argv$[ebp]
 006a9	8b 14 81	 mov	 edx, DWORD PTR [ecx+eax*4]
 006ac	89 95 7c fe ff
ff		 mov	 DWORD PTR $T57010[ebp], edx
 006b2	8b 85 7c fe ff
ff		 mov	 eax, DWORD PTR $T57010[ebp]

この処理はターゲットとなるAPIの呼び出し前に実行されるため、呼出し後に破壊されても影響はない。

したがって、APIの引数が9個程度(ほとんどのAPI)については、結果オーライとなる。

しかし、このままだとespがずれているために、retで正しい復帰アドレスがcpに設定されないように見える。

ここのコードを見てみよう。

; 800  : #undef FREE_ARGS
; 801  :   return rb_assoc_new(val,dvals);
 01a15	8b 4d d8	 mov	 ecx, DWORD PTR _dvals$[ebp]
 01a18	51		 push	 ecx
 01a19	8b 55 fc	 mov	 edx, DWORD PTR _val$[ebp]
 01a1c	52		 push	 edx
 01a1d	e8 00 00 00 00	 call	 _rb_assoc_new
 01a22	83 c4 08	 add	 esp, 8
$L56238:
; 802  : }
 01a25	5f		 pop	 edi
 01a26	5e		 pop	 esi
 01a27	8b e5		 mov	 esp, ebp
 01a29	5d		 pop	 ebp
 01a2a	c3		 ret	 0

VC++は、move esp, ebpによりespの最終的な復帰をベースポインタを元に行っていることがわかる。このため、ret呼び出しおよびその前の直前のフレームの戻し(pop ebp)は正しいespの位置によって処理されていることがわかる。

結論

たいていのWIN32 APIの呼び出しは問題なし。しかし、コンパイラ依存なので、ASRは安全でも他のバイナリーではわからない。

すなわち、完全にバイナリ依存ということになる。

補足

MSDNには

__cdecl 呼び出し規約では、関数の呼び出しごとにスタック クリア用コードが必要なので、__stdcall より大きな実行コードが作成されます。

と書いてある。

これを検証すると、

ret 8

c2 08 00

の3バイト。

それに対して

ret 0

c3

の1バイト。しかし、add esp, 8は、

83 c4 08

の3バイトとなるため、3 < (1 + 3) で、__cdeclのほうが1バイト余分に必要になる。

補足の補足

もちろん、上の文章は「やっぱりMSですね」と繋がるわけですが、まったくフェアではないので背景も書いておきましょう。

ある1つの関数をプログラム内の1000箇所から呼び出すとした場合、__cdeclならば1000箇所にadd esp, xの3バイトが必要となるため3000バイトが使われます。一方の、__stdcallであれば1つの関数のret命令だけが3バイトになるだけです。さすがに1000箇所から呼び出すというのは極端にしても、最終的なプログラムにとってはキロバイト単位の差になることは想像がつきます。もちろん、このサイズは、user32やkernel32といったシステム提供のプログラムにとっても影響します。

と512MB単位でメモリーを、40GB単位でHDを買える2003年に書いても全然、実感が湧きませんね。

補足2

ちなみに、上で使用しているdl/sym.cのアセンブラリストの出し方であるが、/ruby-a.8.0/win32/ext/dl/Makefileに

CFLAGS   =  -MD -Zi -O2b2xg- -G6 -Fadl.cod -FAsc
                                 ~~~~~~~~~~~~~~~

を追加すれば良い。

-FA<オプション>
アセンブラリストを出力する。オプションは's'でソースコードの添付、'c'で機械語の添付。オプションを付けなければアセンブリコードのみが出力される。
-Fa<ファイル名>
アセンブラリストを<ファイル名>で出力する。

もちろん、ruby全体のアセンブラリストが必要であれば、あらかじめCFLAGSに上記設定をすれば良いが、ここでは後からdl.soのみのリストを出したため、直接dl.soのMakefileを手で編集した。

補足3

上では、自動変数に影響がないから大丈夫であろうと結論しているが、もちろん、呼び出しの前後で

push bx
...
call ..
add  esp, 60
pop  bx

のように、スタックにレジスターを一時退避していたら、お話にならない。

この場合は、上記のようなスタックの使い方をしていないから問題ないのであって、自動変数の使われ方だけをチェックしてOKと判断することはできない。

結果論として、rb_dlsym_callが長めの関数で、自動変数を多数使用していたため、

  • スタックフレームが形成され(ebpが保存され)
  • テンポラリなスタックの使用がターゲットの呼び出し前後に無い

ため、問題なかったということである。

また、$Txxxxxの使い方は、アセンブラリストを見てのとおりあまりうまくは無い。1個生成すれば十分だからだ。もし、より最適化されていれば、後から参照する自動変数が破壊される可能性も十分にある。

したがって、正論としては、dlでWIN32 APIを呼び出すのはまずい、というのが本当の結論だ。

Last modified:2010/07/30 01:21:35
Keyword(s):
References:[ruby 1.8.0 dl.so パッチ]