Ada (antara lain) dua jenis konvensi pemanggilan - stdcall dan cdecl . Saya punya beberapa pertanyaan tentang mereka:
- 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?
- Jika sebuah fungsi yang dideklarasikan sebagai stdcall memanggil sebuah fungsi (yang memiliki konvensi pemanggilan sebagai cdecl), atau sebaliknya, apakah ini tidak pantas?
- Secara umum, dapatkah kita mengatakan bahwa panggilan mana yang akan lebih cepat - cdecl atau stdcall?
Jawaban:
Raymond Chen memberikan gambaran yang bagus tentang apa
__stdcall
dan apa yang__cdecl
dilakukannya .(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
__cdecl
merupakan default untuk program C dan C ++ menurut kompiler Visual C ++ dan fungsi WinAPI menggunakan__stdcall
konvensi .(3) Seharusnya tidak ada perbedaan kinerja yang nyata di antara keduanya.
sumber
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
qsort
dari 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)
sumber
Saya melihat sebuah posting yang mengatakan bahwa tidak masalah jika Anda memanggil a
__stdcall
dari a__cdecl
atau sebaliknya. Memang.Alasan: dengan
__cdecl
argumen 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__cdecl
fungsi dengan a__stdcall
, tumpukan tidak dibersihkan sama sekali, jadi pada akhirnya ketika__cdecl
menggunakan referensi berbasis tumpukan untuk argumen atau alamat kembalian akan menggunakan data lama di penunjuk tumpukan saat ini. Jika Anda memanggil__stdcall
fungsi dari a__cdecl
,__stdcall
fungsi tersebut membersihkan argumen pada stack, lalu__cdecl
fungsi tersebut melakukannya lagi, kemungkinan menghapus informasi yang dikembalikan oleh fungsi pemanggil.Konvensi Microsoft untuk C mencoba mengelak ini dengan mengacaukan nama. Sebuah
__cdecl
fungsi diawali dengan garis bawah. Sebuah__stdcall
awalan fungsi dengan garis bawah dan suffixed dengan tanda “@” dan jumlah byte yang akan dihapus. Misalnya__cdecl
f (x) ditautkan sebagai_f
,__stdcall f(int x)
ditautkan sebagai_f@4
tempatsizeof(int)
adalah 4 byte)Jika Anda berhasil melewati linker, nikmati kekacauan debugging.
sumber
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
ret
instruksi (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)
sumber
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.
sumber
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.
sumber
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 ++.Pemanggil mengetahui konvensi pemanggilan fungsi tersebut dan menangani panggilan tersebut sesuai dengan itu.
Iya.
Ini adalah bagian dari deklarasi fungsi.
Penelepon mengetahui konvensi panggilan dan dapat bertindak sesuai dengan itu.
Tidak, konvensi pemanggilan adalah bagian dari deklarasi fungsi sehingga compiler mengetahui semua yang perlu diketahui.
Tidak. Kenapa harus?
Saya tidak tahu. Menguji.
sumber
The
cdecl
modifikator merupakan bagian dari fungsi prototipe (atau fungsi tipe pointer dll) sehingga pemanggil mendapatkan info dari sana dan bertindak sesuai.Tidak apa-apa.
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
stdcall
lebih 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.
sumber
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.
sumber