stdcall dan cdecl

92

Ada (antara lain) dua jenis konvensi pemanggilan - stdcall dan cdecl . Saya punya beberapa pertanyaan tentang mereka:

  1. Ketika fungsi cdecl dipanggil, bagaimana pemanggil tahu apakah itu harus mengosongkan tumpukan? Di situs panggilan, apakah pemanggil mengetahui jika fungsi yang dipanggil adalah fungsi cdecl atau stdcall? Bagaimana cara kerjanya ? Bagaimana penelepon tahu apakah ia harus membebaskan tumpukan atau tidak? Atau apakah itu tanggung jawab penaut?
  2. Jika sebuah fungsi yang dideklarasikan sebagai stdcall memanggil sebuah fungsi (yang memiliki konvensi pemanggilan sebagai cdecl), atau sebaliknya, apakah ini tidak pantas?
  3. Secara umum, dapatkah kita mengatakan bahwa panggilan mana yang akan lebih cepat - cdecl atau stdcall?
Rakesh Agarwal
sumber
9
Ada banyak jenis konvensi pemanggilan, di antaranya hanyalah dua. en.wikipedia.org/wiki/X86_calling_conventions
Mooing Duck
1
Tolong, tandai jawaban yang benar
ceztko

Jawaban:

79

Raymond Chen memberikan gambaran yang bagus tentang apa __stdcalldan apa yang __cdecldilakukannya .

(1) Pemanggil "tahu" untuk membersihkan tumpukan setelah memanggil suatu fungsi karena kompilator mengetahui konvensi pemanggilan fungsi itu dan menghasilkan kode yang diperlukan.

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();
}

Ada kemungkinan untuk tidak cocok dengan konvensi pemanggilan , seperti ini:

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);

Begitu banyak contoh kode yang salah ini bahkan tidak lucu. Seharusnya seperti ini:

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

Namun, dengan asumsi programmer tidak mengabaikan kesalahan kompilator, kompilator akan menghasilkan kode yang diperlukan untuk membersihkan tumpukan dengan benar karena ia akan mengetahui konvensi pemanggilan fungsi yang terlibat.

(2) Kedua cara harus berhasil. Sebenarnya, ini cukup sering terjadi setidaknya dalam kode yang berinteraksi dengan Windows API, karena __cdeclmerupakan default untuk program C dan C ++ menurut kompiler Visual C ++ dan fungsi WinAPI menggunakan __stdcallkonvensi .

(3) Seharusnya tidak ada perbedaan kinerja yang nyata di antara keduanya.

In silico
sumber
+1 untuk contoh yang baik dan postingan Raymond Chen tentang riwayat konvensi panggilan. Bagi siapa pun yang tertarik, bagian lain dari itu adalah bacaan yang bagus juga.
OregonGhost
1 untuk Raymond Chen. Btw (OT): Mengapa saya tidak dapat menemukan bagian lain menggunakan kotak pencarian blog? Google menemukannya, tetapi tidak MSDN Blogs?
Nordic Mainframe
44

Dalam argumen CDECL didorong ke tumpukan dalam urutan terbalik, pemanggil membersihkan tumpukan dan hasilnya dikembalikan melalui registri prosesor (nanti saya akan menyebutnya "register A"). Di STDCALL ada satu perbedaan, pemanggil tidak membersihkan tumpukan, panggilan yang melakukannya.

Anda bertanya mana yang lebih cepat. Tidak ada. Anda harus menggunakan konvensi pemanggilan asli selama Anda bisa. Ubah konvensi hanya jika tidak ada jalan keluar, saat menggunakan pustaka eksternal yang membutuhkan konvensi tertentu untuk digunakan.

Selain itu, terdapat ketentuan lain yang dapat dipilih oleh compiler sebagai default yaitu compiler Visual C ++ menggunakan FASTCALL yang secara teoritis lebih cepat karena penggunaan register prosesor yang lebih luas.

Biasanya Anda harus memberikan tanda tangan konvensi pemanggilan yang tepat untuk fungsi panggilan balik yang diteruskan ke beberapa pustaka eksternal yaitu panggilan balik ke qsortdari pustaka C harus CDECL (jika kompiler secara default menggunakan konvensi lain maka kita harus menandai callback sebagai CDECL) atau berbagai panggilan balik WinAPI harus STDCALL (seluruh WinAPI adalah STDCALL).

Kasus lain yang biasa terjadi ketika Anda menyimpan pointer ke beberapa fungsi eksternal yaitu untuk membuat pointer ke fungsi WinAPI, definisi jenisnya harus ditandai dengan STDCALL.

Dan di bawah ini adalah contoh yang menunjukkan bagaimana kompilator melakukannya:

/* 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)
adf88
sumber
Catatan: __fastcall lebih cepat dari __cdecl dan STDCALL adalah konvensi panggilan default untuk Windows 64-bit
dns
ohh; sehingga harus memunculkan alamat pengirim, menambahkan ukuran blok argumen, lalu melompat ke alamat pengirim yang muncul sebelumnya? (setelah Anda memanggil ret, Anda tidak dapat lagi membersihkan tumpukan, tetapi setelah Anda membersihkan tumpukan, Anda tidak dapat lagi memanggil ret (karena Anda perlu mengubur diri Anda kembali ke ret pada tumpukan, membawa Anda kembali ke masalah bahwa Anda belum membersihkan tumpukan).
Dmitry
alternatifnya, pop kembali ke reg1, setel penunjuk tumpukan ke penunjuk dasar, lalu lompat ke reg1
Dmitry
sebagai alternatif, pindahkan nilai penunjuk tumpukan dari atas tumpukan ke bawah, bersihkan, lalu panggil ret
Dmitry
15

Saya melihat sebuah posting yang mengatakan bahwa tidak masalah jika Anda memanggil a __stdcalldari a__cdecl atau sebaliknya. Memang.

Alasan: dengan __cdeclargumen yang diteruskan ke fungsi yang dipanggil dihapus dari tumpukan oleh fungsi pemanggil, di __stdcall, argumen dihapus dari tumpukan oleh fungsi yang dipanggil. Jika Anda memanggil __cdeclfungsi dengan a __stdcall, tumpukan tidak dibersihkan sama sekali, jadi pada akhirnya ketika __cdeclmenggunakan referensi berbasis tumpukan untuk argumen atau alamat kembalian akan menggunakan data lama di penunjuk tumpukan saat ini. Jika Anda memanggil __stdcallfungsi dari a __cdecl, __stdcallfungsi tersebut membersihkan argumen pada stack, lalu __cdeclfungsi tersebut melakukannya lagi, kemungkinan menghapus informasi yang dikembalikan oleh fungsi pemanggil.

Konvensi Microsoft untuk C mencoba mengelak ini dengan mengacaukan nama. Sebuah __cdeclfungsi diawali dengan garis bawah. Sebuah __stdcallawalan fungsi dengan garis bawah dan suffixed dengan tanda “@” dan jumlah byte yang akan dihapus. Misalnya __cdeclf (x) ditautkan sebagai _f, __stdcall f(int x)ditautkan sebagai _f@4tempatsizeof(int) adalah 4 byte)

Jika Anda berhasil melewati linker, nikmati kekacauan debugging.

Lee Hamilton
sumber
3

Saya ingin memperbaiki jawaban @ adf88. Saya merasa kodesemu untuk STDCALL tidak mencerminkan bagaimana hal itu terjadi dalam kenyataan. 'a', 'b', dan 'c' tidak muncul dari tumpukan di badan fungsi. Sebaliknya mereka muncul oleh retinstruksi (ret 12 akan digunakan dalam kasus ini) yang dalam satu gerakan melompat kembali ke pemanggil dan pada saat yang sama muncul 'a', 'b', dan 'c' dari tumpukan.

Ini versi saya dikoreksi menurut pemahaman saya:

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)

golem
sumber
2

Ini ditentukan dalam tipe fungsi. Ketika Anda memiliki sebuah function pointer, itu diasumsikan sebagai cdecl jika tidak secara eksplisit stdcall. Ini berarti bahwa jika Anda mendapatkan penunjuk stdcall dan penunjuk cdecl, Anda tidak dapat menukarnya. Kedua jenis fungsi dapat memanggil satu sama lain tanpa masalah, hanya mendapatkan satu jenis saat Anda mengharapkan jenis lainnya. Soal kecepatan, mereka berdua memainkan peran yang sama, hanya di tempat yang sedikit berbeda, itu sangat tidak relevan.

Anak anjing
sumber
1

Penelepon dan penerima harus menggunakan konvensi yang sama pada saat pemanggilan - itulah satu-satunya cara agar dapat berfungsi dengan andal. Baik pemanggil dan penerima mengikuti protokol yang telah ditentukan - misalnya, siapa yang perlu membersihkan tumpukan. Jika konvensi tidak cocok, program Anda mengalami perilaku tidak terdefinisi - kemungkinan besar akan crash secara spektakuler.

Ini hanya diperlukan per situs pemanggilan - kode panggilan itu sendiri dapat menjadi fungsi dengan konvensi pemanggilan apa pun.

Anda seharusnya tidak melihat perbedaan nyata dalam kinerja antara konvensi tersebut. Jika itu menjadi masalah, Anda biasanya perlu mengurangi panggilan - misalnya, ubah algoritme.

gigi tajam
sumber
1

Hal-hal tersebut spesifik untuk Compiler dan Platform. Baik standar C maupun C ++ tidak mengatakan apa pun tentang konvensi pemanggilan kecuali untuk extern "C"C ++.

bagaimana penelepon tahu apakah ia harus mengosongkan tumpukan?

Pemanggil mengetahui konvensi pemanggilan fungsi tersebut dan menangani panggilan tersebut sesuai dengan itu.

Di situs panggilan, apakah pemanggil mengetahui jika fungsi yang dipanggil adalah fungsi cdecl atau stdcall?

Iya.

Bagaimana cara kerjanya ?

Ini adalah bagian dari deklarasi fungsi.

Bagaimana penelepon tahu apakah ia harus membebaskan tumpukan atau tidak?

Penelepon mengetahui konvensi panggilan dan dapat bertindak sesuai dengan itu.

Atau apakah itu tanggung jawab penaut?

Tidak, konvensi pemanggilan adalah bagian dari deklarasi fungsi sehingga compiler mengetahui semua yang perlu diketahui.

Jika sebuah fungsi yang dideklarasikan sebagai stdcall memanggil sebuah fungsi (yang memiliki konvensi pemanggilan sebagai cdecl), atau sebaliknya, apakah ini tidak pantas?

Tidak. Kenapa harus?

Secara umum, dapatkah kita mengatakan bahwa panggilan mana yang akan lebih cepat - cdecl atau stdcall?

Saya tidak tahu. Menguji.

menjual
sumber
0

a) Ketika fungsi cdecl dipanggil oleh pemanggil, bagaimana pemanggil tahu apakah itu harus membebaskan tumpukan?

The cdeclmodifikator merupakan bagian dari fungsi prototipe (atau fungsi tipe pointer dll) sehingga pemanggil mendapatkan info dari sana dan bertindak sesuai.

b) Jika suatu fungsi yang dideklarasikan sebagai stdcall memanggil suatu fungsi (yang memiliki konvensi pemanggilan sebagai cdecl), atau sebaliknya, apakah ini tidak pantas?

Tidak apa-apa.

c) Secara umum, dapatkah kita mengatakan panggilan mana yang akan lebih cepat - cdecl atau stdcall?

Secara umum, saya akan menahan diri dari pernyataan semacam itu. Perbedaan itu penting misalnya. ketika Anda ingin menggunakan fungsi va_arg. Secara teori, bisa jadi itu stdcalllebih cepat dan menghasilkan kode yang lebih kecil karena memungkinkan untuk menggabungkan argumen yang muncul dengan memunculkan penduduk setempat, tetapi OTOH dengancdecl , Anda dapat melakukan hal yang sama, juga, jika Anda pintar.

Konvensi pemanggilan yang bertujuan untuk lebih cepat biasanya melakukan beberapa pengoperan register.

jpalecek.dll
sumber
-1

Konvensi pemanggilan tidak ada hubungannya dengan bahasa pemrograman C / C ++ dan lebih spesifik tentang bagaimana kompilator mengimplementasikan bahasa yang diberikan. Jika Anda secara konsisten menggunakan kompilator yang sama, Anda tidak perlu khawatir tentang konvensi pemanggilan.

Namun, terkadang kita ingin kode biner yang dikompilasi oleh kompiler yang berbeda untuk beroperasi dengan benar. Ketika kita melakukannya, kita perlu mendefinisikan sesuatu yang disebut Application Binary Interface (ABI). ABI mendefinisikan bagaimana kompilator mengubah sumber C / C ++ menjadi kode mesin. Ini akan mencakup konvensi pemanggilan, pengubahan nama, dan tata letak v-table. cdelc dan stdcall adalah dua konvensi pemanggilan berbeda yang biasa digunakan pada platform x86.

Dengan menempatkan informasi pada konvensi pemanggilan ke dalam header sumber, kompilator akan mengetahui kode apa yang perlu dibuat untuk beroperasi secara benar dengan executable yang diberikan.

Doron
sumber