__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を呼び出すのはまずい、というのが本当の結論だ。
Keyword(s):
References:[ruby 1.8.0 dl.so パッチ]