web-dev-qa-db-ja.com

stdcallおよびcdecl

(他にも)2種類の呼び出し規約があります-stdcallcdecl。私はそれらについていくつか質問があります:

  1. Cdecl関数が呼び出されると、呼び出し元はスタックを解放する必要があるかどうかをどのように知るのですか?呼び出しサイトで、呼び出し元は、呼び出されている関数がcdecl関数かstdcall関数かを知っていますか?どのように機能しますか?呼び出し元は、スタックを解放する必要があるかどうかをどのように知るのですか?それともリンカーの責任ですか?
  2. Stdcallとして宣言されている関数が関数(cdeclとして呼び出し規約がある)を呼び出す場合、またはその逆の場合、これは不適切でしょうか?
  3. 一般に、cdeclまたはstdcallのどちらの呼び出しが高速になると言えますか?
80
Rakesh Agarwal

レイモンド・チェンは、__stdcallおよび__cdeclはありません

(1)呼び出し側は、関数を呼び出した後にスタックをクリーンアップすることを「認識」します。これは、コンパイラがその関数の呼び出し規則を知っており、必要なコードを生成するためです。

void __stdcall StdcallFunc() {}

void __cdecl CdeclFunc()
{
    // The compiler knows that StdcallFunc() uses the __stdcall
    // convention at this point, so it generates the proper binary
    // for stack cleanup.
    StdcallFunc();
}

呼び出し規約と一致しない可能性があります 、次のように:

LRESULT MyWndProc(HWND hwnd, UINT msg,
    WPARAM wParam, LPARAM lParam);
// ...
// Compiler usually complains but there's this cast here...
windowClass.lpfnWndProc = reinterpret_cast<WNDPROC>(&MyWndProc);

多くのコードサンプルがこれを間違えていますが、おかしくさえありません。次のようになるはずです。

// CALLBACK is #define'd as __stdcall
LRESULT CALLBACK MyWndProc(HWND hwnd, UINT msg
    WPARAM wParam, LPARAM lParam);
// ...
windowClass.lpfnWndProc = &MyWndProc;

ただし、プログラマがコンパイラエラーを無視しないと仮定すると、コンパイラは、関連する関数の呼び出し規約を知っているため、スタックを適切にクリーンアップするために必要なコードを生成します。

(2)両方の方法が機能するはずです。実際、これは少なくとも __cdeclは、Visual C++コンパイラーによるCおよびC++プログラムのデフォルトです および WinAPI関数は__stdcall表記法

(3)2つの間に実際のパフォーマンスの違いはありません。

72
In silico

CDECLの引数が逆順にスタックにプッシュされると、呼び出し元はスタックをクリアし、プロセッサレジストリを介して結果が返されます(後で「レジスタA」と呼びます)。 STDCALLには1つの違いがあります。呼び出し側はスタックをクリアせず、呼び出し側はクリアします。

どちらが速いかを尋ねています。誰も。できる限りネイティブの呼び出し規約を使用する必要があります。使用する特定の規則を必要とする外部ライブラリを使用する場合、出口がない場合にのみ規則を変更します。

その上、コンパイラがデフォルトとして選択するかもしれない他の規則があります。つまり、Visual C++コンパイラは、プロセッサレジスタのより広範な使用のために理論的に高速なFASTCALLを使用します。

通常、外部ライブラリに渡されるコールバック関数に適切な呼び出し規約シグネチャを与える必要があります。つまり、CライブラリからのqsortへのコールバックはCDECLでなければなりません(コンパイラーがデフォルトで他の規約を使用する場合、コールバックをCDECLとしてマークする必要があります)または、さまざまなWinAPIコールバックはSTDCALLである必要があります(WinAPIはすべてSTDCALLです)。

他の通常の場合は、いくつかの外部関数へのポインターを保存する場合、つまりWinAPI関数へのポインターを作成する場合、その型定義はSTDCALLでマークする必要があります。

そして、以下はコンパイラがそれをどのように行うかを示す例です:

/* 1. calling function in C++ */
i = Function(x, y, z);

/* 2. function body in C++ */
int Function(int a, int b, int c) { return a + b + c; }

CDECL:

/* 1. calling CDECL 'Function' in pseudo-assembler (similar to what the compiler outputs) */
Push on the stack a copy of 'z', then a copy of 'y', then a copy of 'x'
call (jump to function body, after function is finished it will jump back here, the address where to jump back is in registers)
move contents of register A to 'i' variable
pop all from the stack that we have pushed (copy of x, y and z)

/* 2. CDECL 'Function' body in pseudo-assembler */
/* Now copies of 'a', 'b' and 'c' variables are pushed onto the stack */
copy 'a' (from stack) to register A
copy 'b' (from stack) to register B
add A and B, store result in A
copy 'c' (from stack) to register B
add A and B, store result in A
jump back to caller code (a, b and c still on the stack, the result is in register A)

STDCALL:

/* 1. calling STDCALL in pseudo-assembler (similar to what the compiler outputs) */
Push on the stack a copy of 'z', then a copy of 'y', then a copy of 'x'
call
move contents of register A to 'i' variable

/* 2. STDCALL 'Function' body in pseaudo-assembler */
pop 'a' from stack to register A
pop 'b' from stack to register B
add A and B, store result in A
pop 'c' from stack to register B
add A and B, store result in A
jump back to caller code (a, b and c are no more on the stack, result in register A)
41
adf88

__stdcallから__cdeclを呼び出すか、またはその逆を行うかは問題ではないという投稿に気付きました。します。

理由:__cdeclでは、呼び出された関数に渡された引数は呼び出し元の関数によってスタックから削除され、__stdcallでは、引数は呼び出された関数によってスタックから削除されます。 __cdecl__stdcall関数を呼び出すと、スタックはまったくクリーンアップされないため、最終的に__cdeclが引数または戻りアドレスにスタックベースの参照を使用すると、古い現在のスタックポインターのデータ。 __stdcallから__cdecl関数を呼び出すと、__stdcall関数はスタックの引数をクリーンアップし、__cdecl関数は再度呼び出しを削除します。関数は情報を返します。

CのMicrosoft規則は、名前をマングルすることでこれを回避しようとします。 __cdecl関数の先頭にはアンダースコアが付きます。 __stdcall関数のプレフィックスにはアンダースコアが付き、サフィックスにはアットマーク「@」と削除するバイト数が付きます。たとえば、__cdecl f(x)は_fとしてリンクされ、__stdcall f(int x)_f@4としてリンクされます。ここでsizeof(int)は4バイト)

リンカを通り抜けることができたら、デバッグの混乱を楽しんでください。

14
Lee Hamilton

@ adf88の答えを改善したい。 STDCALLの擬似コードは、実際にどのように発生するかを反映していないと感じています。 「a」、「b」、および「c」は、関数本体のスタックからポップされません。代わりに、ret命令(ret 12は、この場合に使用されます)一度にジャンプして呼び出し元に戻り、同時にスタックから 'a'、 'b'、および 'c'をポップします。

私の理解に従って修正された私のバージョンは次のとおりです。

STDCALL:

/* 1. calling STDCALL in pseudo-assembler (similar to what the compiler outputs) */
Push on the stack a copy of 'z', then copy of 'y', then copy of 'x'
call
move contents of register A to 'i' variable


/* 2. STDCALL 'Function' body in pseaudo-assembler */ copy 'a' (from stack) to register A copy 'b' (from stack) to register B add A and B, store result in A copy 'c' (from stack) to register B add A and B, store result in A jump back to caller code and at the same time pop 'a', 'b' and 'c' off the stack (a, b and c are removed from the stack in this step, result in register A)
3
golem

関数タイプで指定されます。関数ポインターがある場合、明示的にstdcallでない場合、cdeclであると想定されます。つまり、stdcallポインターとcdeclポインターを取得した場合、それらを交換することはできません。 2つの関数タイプは問題なくお互いを呼び出すことができます。他のタイプを期待するときに1つのタイプを取得するだけです。速度に関しては、どちらも同じ役割を果たしますが、ほんのわずかに異なる場所で、それは本当に無関係です。

2
Puppy

呼び出し元と呼び出し先は、呼び出しの時点で同じ規則を使用する必要があります-それが確実に機能する唯一の方法です。呼び出し元と呼び出し先の両方が事前定義されたプロトコルに従います。たとえば、誰がスタックをクリーンアップする必要があるかなどです。規則が一致しない場合、プログラムは未定義の動作に陥ります-おそらく見事にクラッシュします。

これは、呼び出しサイトごとにのみ必要です。呼び出しコード自体は、任意の呼び出し規約を持つ関数にすることができます。

これらの規則間のパフォーマンスの実際の違いに気付かないでください。それが問題になる場合は、通常、呼び出し回数を減らす必要があります。たとえば、アルゴリズムを変更します。

1
sharptooth

これらはコンパイラ固有およびプラットフォーム固有です。 CとC++のどちらの標準も、C++のextern "C"以外の呼び出し規約については何も言及していません。

呼び出し元は、スタックを解放する必要があるかどうかをどのように知るのですか?

呼び出し元は、関数の呼び出し規則を知っており、それに応じて呼び出しを処理します。

呼び出しサイトで、呼び出し元は、呼び出されている関数がcdecl関数かstdcall関数かを知っていますか?

はい。

どのように機能しますか?

これは、関数宣言の一部です。

呼び出し元は、スタックを解放する必要があるかどうかをどのように知るのですか?

呼び出し元は呼び出し規則を知っており、それに応じて行動できます。

それともリンカーの責任ですか?

いいえ、呼び出し規約は関数の宣言の一部であるため、コンパイラは知る必要のあるすべてのことを知っています。

Stdcallとして宣言されている関数が関数(cdeclとして呼び出し規約がある)を呼び出す場合、またはその逆の場合、これは不適切でしょうか?

いいえ。なぜそうする必要がありますか?

一般に、cdeclまたはstdcallのどちらの呼び出しが高速になると言えますか?

知りません。試して。

1
sellibitze

a)cdecl関数が呼び出し元によって呼び出された場合、呼び出し元はスタックを解放する必要があるかどうかをどのように知るのですか?

cdecl修飾子は関数プロトタイプ(または関数ポインター型など)の一部であるため、呼び出し元はそこから情報を取得し、それに応じて動作します。

b)stdcallとして宣言された関数が関数(cdeclとして呼び出し規約を持つ)を呼び出す場合、またはその逆の場合、これは不適切でしょうか?

大丈夫です。

c)一般に、cdeclまたはstdcallのどちらの呼び出しが高速になると言えますか?

一般的に、私はそのような声明を控えます。区別が重要です。 va_arg関数を使用する場合。理論的には、stdcallはより高速で、より小さなコードを生成します。これは、引数のポップとローカルのポップを組み合わせることができるためですが、OTOHはcdeclと同じこともできます。 、あなたが賢いなら。

より高速にすることを目的とする呼び出し規約は、通常、いくつかのレジスタ渡しを行います。

0
jpalecek