stdcall และ cdecl


92

มี (อื่น) ทั้งสองประเภทของการประชุมเรียกร้องให้มี - stdcallและcdecl ฉันมีคำถามสองสามข้อเกี่ยวกับพวกเขา:

  1. เมื่อเรียกใช้ฟังก์ชัน cdecl ผู้เรียกจะรู้ได้อย่างไรว่าควรเพิ่มสแต็กหรือไม่ ที่ไซต์การโทรผู้โทรทราบหรือไม่ว่าฟังก์ชันที่เรียกนั้นเป็นฟังก์ชัน cdecl หรือ stdcall มันทำงานอย่างไร? ผู้โทรจะรู้ได้อย่างไรว่าควรเพิ่มสแต็กหรือไม่? หรือเป็นความรับผิดชอบของผู้เชื่อมโยง?
  2. หากฟังก์ชันที่ประกาศเป็น stdcall เรียกใช้ฟังก์ชัน (ซึ่งมีหลักการเรียกเป็น cdecl) หรือในทางกลับกันสิ่งนี้จะไม่เหมาะสมหรือไม่?
  3. โดยทั่วไปเราสามารถพูดได้ว่าการโทรใดจะเร็วกว่า - cdecl หรือ stdcall?

9
มีรูปแบบการโทรหลายประเภทซึ่งมีเพียงสองแบบ en.wikipedia.org/wiki/X86_calling_conventions
Mooing Duck

1
กรุณาทำเครื่องหมายคำตอบที่ถูกต้อง
ceztko

คำตอบ:


79

เรย์มอนด์เฉินให้ภาพรวมที่ดีของสิ่งที่__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เป็นค่าเริ่มต้นสำหรับ C และ C ++ โปรแกรมตาม Visual C ++ คอมไพเลอร์และWinAPI ฟังก์ชั่นใช้__stdcallการประชุม

(3) ไม่ควรมีความแตกต่างด้านประสิทธิภาพที่แท้จริงระหว่างทั้งสอง


+1 สำหรับตัวอย่างที่ดีและโพสต์ของ Raymond Chen เกี่ยวกับการเรียกประวัติการประชุม สำหรับทุกคนที่สนใจส่วนอื่น ๆ ของที่อ่านดีเช่นกัน
OregonGhost

+1 สำหรับ Raymond Chen Btw (OT): ทำไมฉันไม่พบส่วนอื่น ๆ โดยใช้ช่องค้นหาบล็อก? Google พบพวกเขา แต่ไม่ใช่ MSDN Blogs?
Nordic Mainframe

44

ในอาร์กิวเมนต์ CDECL จะถูกผลักไปที่สแต็กในลำดับที่กลับกันผู้เรียกจะล้างสแต็กและผลลัพธ์จะถูกส่งกลับผ่านรีจิสตรีโปรเซสเซอร์ (ภายหลังฉันจะเรียกมันว่า "register A") ใน STDCALL มีข้อแตกต่างอย่างหนึ่งคือผู้โทรไม่ได้ล้างสแต็กผู้โทรทำ

คุณกำลังถามว่าอันไหนเร็วกว่า ไม่มีใคร. คุณควรใช้การเรียกแบบดั้งเดิมให้นานที่สุดเท่าที่จะทำได้ เปลี่ยนรูปแบบเฉพาะในกรณีที่ไม่มีทางออกเมื่อใช้ไลบรารีภายนอกที่ต้องใช้แบบแผนบางประการ

นอกจากนี้ยังมีอนุสัญญาอื่น ๆ ที่คอมไพเลอร์อาจเลือกเป็นค่าเริ่มต้นเช่นคอมไพเลอร์ 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จาก a __cdeclหรือวีซ่าในทางกลับกัน มัน.

เหตุผล: ด้วย __cdeclอาร์กิวเมนต์ที่ส่งผ่านไปยังฟังก์ชันที่เรียกว่าจะถูกลบออกจากสแต็กโดยฟังก์ชันการเรียกใช้ใน__stdcallอาร์กิวเมนต์จะถูกลบออกจากสแต็กโดยฟังก์ชันที่เรียกว่า หากคุณเรียกใช้__cdeclฟังก์ชันด้วย a __stdcallสแต็กจะไม่ถูกล้างเลยดังนั้นในที่สุดเมื่อ__cdeclใช้การอ้างอิงแบบเรียงซ้อนสำหรับอาร์กิวเมนต์หรือที่อยู่ที่ส่งคืนจะใช้ข้อมูลเก่าที่ตัวชี้สแต็กปัจจุบัน หากคุณเรียกใช้__stdcallฟังก์ชั่นจาก__cdeclที่__stdcallฟังก์ชั่นทำความสะอาดข้อโต้แย้งในกองแล้ว__cdeclฟังก์ชั่นไม่ได้อีกครั้งอาจจะเอาฟังก์ชั่นการโทรกลับข้อมูล

การประชุมของ Microsoft สำหรับ C พยายามหลีกเลี่ยงสิ่งนี้โดยการบิดเบือนชื่อ __cdeclฟังก์ชั่นจะนำหน้าด้วยการขีดเส้นใต้ __stdcallคำนำหน้าฟังก์ชั่นที่มีการขีดเส้นใต้และ suffixed กับที่เครื่องหมาย“@” และจำนวนไบต์ที่ถูกลบออก เช่น__cdeclf (x) มีการเชื่อมโยงเป็น_f, __stdcall f(int x)มีการเชื่อมโยงเป็น_f@4ที่sizeof(int)เป็น 4 ไบต์)

หากคุณจัดการเพื่อผ่านตัวเชื่อมโยงได้ให้สนุกกับการแก้ไขข้อบกพร่อง


3

ฉันต้องการปรับปรุงคำตอบของ @ adf88 ฉันรู้สึกว่า pseudocode สำหรับ 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)


2

ระบุไว้ในประเภทฟังก์ชัน เมื่อคุณมีตัวชี้ฟังก์ชันจะถือว่าเป็น cdecl ถ้าไม่ใช่ stdcall อย่างชัดเจน ซึ่งหมายความว่าหากคุณได้รับตัวชี้ stdcall และตัวชี้ cdecl คุณจะไม่สามารถแลกเปลี่ยนได้ ฟังก์ชันทั้งสองประเภทสามารถโทรหากันได้โดยไม่มีปัญหาเพียงแค่ได้รับประเภทเดียวเมื่อคุณคาดหวังอีกประเภทหนึ่ง สำหรับความเร็วนั้นทั้งคู่แสดงบทบาทเดียวกันเพียงแค่ในสถานที่ที่แตกต่างกันเล็กน้อยมันไม่เกี่ยวข้องกันเลย


1

ผู้โทรและผู้เรียกจำเป็นต้องใช้แบบแผนเดียวกัน ณ จุดที่เรียกใช้นั่นเป็นวิธีเดียวที่สามารถทำงานได้อย่างน่าเชื่อถือ ทั้งผู้โทรและผู้รับสายจะปฏิบัติตามโปรโตคอลที่กำหนดไว้ล่วงหน้าตัวอย่างเช่นผู้ที่ต้องการทำความสะอาดสแต็ก หากข้อตกลงที่ไม่ตรงกับโปรแกรมของคุณเกิดพฤติกรรมที่ไม่ได้กำหนด - อาจเกิดปัญหาอย่างไม่น่าเชื่อ

สิ่งนี้จำเป็นสำหรับไซต์การเรียกใช้เท่านั้น - รหัสการโทรเองสามารถเป็นฟังก์ชันที่มีรูปแบบการโทรใด ๆ

คุณไม่ควรสังเกตเห็นความแตกต่างอย่างแท้จริงในประสิทธิภาพระหว่างอนุสัญญาเหล่านั้น หากสิ่งนั้นกลายเป็นปัญหาคุณมักจะต้องโทรน้อยลงเช่นเปลี่ยนอัลกอริทึม


1

สิ่งเหล่านี้เป็นคอมไพเลอร์และเฉพาะแพลตฟอร์ม ทั้ง C และมาตรฐาน C ++ ไม่ได้กล่าวอะไรเกี่ยวกับการเรียกแบบประชุมยกเว้นextern "C"ใน C ++

ผู้โทรจะรู้ได้อย่างไรว่าควรเพิ่มสแต็กหรือไม่

ผู้โทรรู้หลักการโทรของฟังก์ชันและจัดการการโทรตามนั้น

ที่ไซต์การโทรผู้โทรทราบหรือไม่ว่าฟังก์ชันที่เรียกนั้นเป็นฟังก์ชัน cdecl หรือ stdcall

ใช่.

มันทำงานอย่างไร?

มันเป็นส่วนหนึ่งของการประกาศฟังก์ชัน

ผู้โทรจะรู้ได้อย่างไรว่าควรเพิ่มสแต็กหรือไม่?

ผู้โทรรู้หลักการโทรและสามารถดำเนินการตามนั้นได้

หรือเป็นความรับผิดชอบของผู้เชื่อมโยง?

ไม่แบบแผนการเรียกเป็นส่วนหนึ่งของการประกาศฟังก์ชันดังนั้นคอมไพเลอร์จึงรู้ทุกสิ่งที่จำเป็นต้องรู้

หากฟังก์ชันที่ประกาศเป็น stdcall เรียกใช้ฟังก์ชัน (ซึ่งมีหลักการเรียกเป็น cdecl) หรือในทางกลับกันสิ่งนี้จะไม่เหมาะสมหรือไม่?

ไม่ทำไมต้อง?

โดยทั่วไปเราสามารถพูดได้ว่าการโทรใดจะเร็วกว่า - cdecl หรือ stdcall?

ฉันไม่รู้ ทดสอบ


0

ก) เมื่อผู้เรียกเรียกใช้ฟังก์ชัน cdecl ผู้โทรจะทราบได้อย่างไรว่าควรเพิ่มสแต็กหรือไม่

cdeclปรับปรุงเป็นส่วนหนึ่งของต้นแบบฟังก์ชั่น (หรือฟังก์ชั่นตัวชี้ประเภทอื่น ๆ ) เพื่อให้ผู้ที่โทรมาได้รับข้อมูลจากที่นั่นและทำหน้าที่ตาม

b) หากฟังก์ชันที่ประกาศเป็น stdcall เรียกใช้ฟังก์ชัน (ซึ่งมีรูปแบบการเรียกเป็น cdecl) หรือในทางกลับกันสิ่งนี้จะไม่เหมาะสมหรือไม่?

ไม่เป็นไร.

c) โดยทั่วไปเราสามารถพูดได้ว่าการโทรใดจะเร็วกว่า - cdecl หรือ stdcall?

โดยทั่วไปฉันจะละเว้นจากข้อความดังกล่าว ความแตกต่างมีความสำคัญเช่น เมื่อคุณต้องการใช้ฟังก์ชัน va_arg ในทางทฤษฎีอาจเป็นไปได้ว่าstdcallเร็วกว่าและสร้างโค้ดที่เล็กลงเนื่องจากช่วยให้สามารถรวมการโต้เถียงกับคนท้องถิ่นได้ แต่ OTOH กับcdeclคุณสามารถทำสิ่งเดียวกันได้เช่นกันหากคุณฉลาด

การประชุมทางโทรศัพท์ที่มีเป้าหมายให้เร็วขึ้นมักจะมีการลงทะเบียนบางส่วน


-1

รูปแบบการเรียกใช้ไม่มีส่วนเกี่ยวข้องกับภาษาโปรแกรม C / C ++ และค่อนข้างเฉพาะเจาะจงเกี่ยวกับวิธีที่คอมไพเลอร์ใช้ภาษาที่กำหนด หากคุณใช้คอมไพเลอร์เดียวกันอย่างสม่ำเสมอคุณก็ไม่ต้องกังวลเกี่ยวกับการเรียกประชุม

อย่างไรก็ตามบางครั้งเราต้องการให้รหัสไบนารีที่รวบรวมโดยคอมไพเลอร์ต่างกันเพื่อให้ทำงานร่วมกันได้อย่างถูกต้อง เมื่อเราทำเช่นนั้นเราจำเป็นต้องกำหนดสิ่งที่เรียกว่า Application Binary Interface (ABI) ABI กำหนดวิธีที่คอมไพลเลอร์แปลงซอร์ส C / C ++ เป็นรหัสเครื่อง ซึ่งจะรวมถึงรูปแบบการเรียกการโกงชื่อและรูปแบบตารางวี cdelc และ stdcall เป็นรูปแบบการโทรที่แตกต่างกันสองแบบที่ใช้กันทั่วไปบนแพลตฟอร์ม x86

ด้วยการวางข้อมูลเกี่ยวกับรูปแบบการเรียกลงในส่วนหัวของแหล่งที่มาคอมไพเลอร์จะรู้ว่าต้องสร้างรหัสใดเพื่อให้ทำงานร่วมกันได้อย่างถูกต้องกับไฟล์ปฏิบัติการที่กำหนด

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.