stdcallおよびcdecl


89

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

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

9
呼び出し規約には多くの種類があり、そのうち2つだけです。 en.wikipedia.org/wiki/X86_calling_conventions
Mooing Duck

1
正解をマークしてください
ceztko 2018

回答:


77

Raymond Chenが何__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)両方の方法が機能するはずです。実際、これは少なくともWindows APIとやり取りするコードでかなり頻繁に発生します。これ__cdeclは、Visual C ++コンパイラによるCおよびC ++プログラムのデフォルトであり、WinAPI関数はこの__stdcall規則を使用しているためです。

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


+1は良い例であり、Raymond Chenが会議の歴史を呼ぶことについて投稿しています。興味のある人にとっては、他の部分も読むのに適しています。
OregonGhost 2010

レイモンドチェンの+1。ところで(OT):ブログの検索ボックスを使用しても他のパーツが見つからないのはなぜですか?グーグルはそれらを見つけますが、MSDNブログは見つけませんか?
ノルディックメインフレーム2010

44

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

あなたはどちらがより速いかを求めています。誰も。可能な限り、ネイティブの呼び出し規約を使用する必要があります。特定の規則を使用する必要がある外部ライブラリを使用する場合に、方法がない場合にのみ規則を変更します。

さらに、コンパイラーがデフォルトの規則として選択する可能性のある他の規則があります。つまり、Visual C ++コンパイラーは、プロセッサー・レジスターをより広範囲に使用するため、理論的には速いFASTCALLを使用します。

通常、いくつかの外部ライブラリに渡されるコールバック関数に適切な呼び出し規約の署名を与える必要があります。つまりqsort、Cライブラリからのコールバックは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)

注:__fastcallは__cdeclよりも高速で、STDCALLはWindows 64ビットのデフォルトの呼び出し規約です
dns

ああ したがって、戻りアドレスをポップし、引数ブロックサイズを追加して、先にポップした戻りアドレスにジャンプする必要がありますか?(retを呼び出すと、スタックをクリーンアップできなくなりますが、スタックをクリーンすると、ret(スタックに戻り、スタックをクリーンアップしていない問題に戻るには、自分自身を埋める必要があります)
Dmitry

または、reg1に戻り、スタックポインターをベースポインターに設定してから、reg1にジャンプします
Dmitry

または、スタックポインターの値をスタックの一番上から一番下に移動してクリーンにし、retを呼び出します
Dmitry

15

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

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

マイクロソフトのC規約では、名前をマングルすることでこれを回避しようとしています。__cdecl関数は、アンダースコアが付いています。__stdcall関数下線付きプレフィックス及び符号に接尾辞「@」と除去されるべきバイトの数。たとえば、__cdeclf(x)はとしてリンクされ_f、where は4バイト__stdcall f(int x)としてリンクされます)_f@4sizeof(int)

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


3

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

これは私の理解に従って修正された私のバージョンです:

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)


2

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


1

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

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

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


1

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

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

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

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

はい。

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

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

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

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

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

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

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

いいえ。なぜそれが必要なのですか。

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

知りません。試して。


0

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

cdecl修飾子は、関数プロトタイプの一部(または関数ポインタ型等)、発信者がそこから情報を取得し、それに応じて作用するようです。

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

大丈夫です。

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

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

高速化を目的とした呼び出し規約では、通常、レジスタの受け渡しが行われます。


-1

呼び出し規約は、C / C ++プログラミング言語とは何の関係もなく、コンパイラーが特定の言語を実装する方法をかなり具体化したものです。同じコンパイラを一貫して使用する場合は、呼び出し規約について心配する必要はありません。

ただし、適切に相互運用するために、異なるコンパイラーによってコンパイルされたバイナリコードが必要な場合があります。その場合、Application Binary Interface(ABI)と呼ばれるものを定義する必要があります。ABIは、コンパイラーがC / C ++ソースをマシンコードに変換する方法を定義します。これには、呼び出し規約、名前のマングリング、およびvテーブルのレイアウトが含まれます。cdelcとstdcallは、x86プラットフォームで一般的に使用される2つの異なる呼び出し規約です。

呼び出し規約に関する情報をソースヘッダーに配置することにより、コンパイラーは、指定された実行可能ファイルと正しく相互運用するために生成する必要があるコードを認識します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.