stdcall và cdecl


89

Có (trong số những người khác) hai loại quy ước gọi - stdcallcdecl . Tôi có một số câu hỏi về chúng:

  1. Khi một hàm cdecl được gọi, làm thế nào người gọi biết liệu nó có nên giải phóng ngăn xếp hay không? Tại trang web cuộc gọi, người gọi có biết liệu hàm được gọi là cdecl hay hàm stdcall không? Làm thế nào nó hoạt động ? Làm thế nào để người gọi biết liệu nó có nên giải phóng ngăn xếp hay không? Hay đó là trách nhiệm của những người liên kết?
  2. Nếu một hàm được khai báo là stdcall gọi một hàm (có quy ước gọi là cdecl) hoặc ngược lại, thì điều này có phù hợp không?
  3. Nói chung, chúng ta có thể nói rằng cuộc gọi nào sẽ nhanh hơn - cdecl hay stdcall?

9
Có nhiều loại quy ước gọi, trong số đó chỉ có hai. vi.wikipedia.org/wiki/X86_calling_conventions
Mooing Duck

1
Xin vui lòng, đánh dấu một câu trả lời đúng
ceztko

Câu trả lời:


77

Raymond Chen đưa ra một cái nhìn tổng quan tốt đẹp về những gì __stdcallvà những việc __cdecllàm .

(1) Người gọi "biết" dọn dẹp ngăn xếp sau khi gọi một hàm vì trình biên dịch biết quy ước gọi của hàm đó và tạo ra mã cần thiết.

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

Có thể không phù hợp với quy ước gọi , như sau:

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

Vì vậy, nhiều mẫu mã bị sai, nó thậm chí còn không vui. Nó phải như thế này:

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

Tuy nhiên, giả sử lập trình viên không bỏ qua các lỗi trình biên dịch, trình biên dịch sẽ tạo ra mã cần thiết để dọn dẹp ngăn xếp đúng cách vì nó sẽ biết các quy ước gọi của các hàm liên quan.

(2) Cả hai cách đều hoạt động. Trên thực tế, điều này xảy ra khá thường xuyên, ít nhất là trong mã tương tác với Windows API, vì __cdeclnó là mặc định cho các chương trình C và C ++ theo trình biên dịch Visual C ++các hàm WinAPI sử dụng __stdcallquy ước .

(3) Không được có sự khác biệt về hiệu suất thực giữa cả hai.


+1 cho một ví dụ điển hình và bài đăng của Raymond Chen về lịch sử hội nghị gọi điện. Đối với bất kỳ ai quan tâm, các phần khác của đó cũng là một cuốn sách tốt.
OregonGhost

+1 cho Raymond Chen. Btw (OT): Tại sao tôi không thể tìm thấy các phần khác bằng cách sử dụng hộp tìm kiếm blog? Google tìm thấy chúng, nhưng không tìm thấy Blog MSDN?
Nordic Mainframe

44

Trong CDECL, các đối số được đẩy lên ngăn xếp theo thứ tự đảo ngược, trình gọi xóa ngăn xếp và kết quả được trả về thông qua sổ đăng ký bộ xử lý (sau này tôi sẽ gọi nó là "thanh ghi A"). Trong STDCALL có một sự khác biệt, người gọi không xóa ngăn xếp, người gọi thì làm.

Bạn đang hỏi cái nào nhanh hơn. Không một ai. Bạn nên sử dụng quy ước gọi bản địa miễn là bạn có thể. Chỉ thay đổi quy ước nếu không có lối thoát, khi sử dụng các thư viện bên ngoài yêu cầu sử dụng quy ước nhất định.

Bên cạnh đó, có những quy ước khác mà trình biên dịch có thể chọn làm mặc định, tức là trình biên dịch Visual C ++ sử dụng FASTCALL về mặt lý thuyết nhanh hơn vì sử dụng nhiều thanh ghi bộ xử lý hơn.

Thông thường, bạn phải cung cấp một chữ ký quy ước gọi thích hợp để gọi lại các hàm được truyền đến một số thư viện bên ngoài, tức là gọi lại qsorttừ thư viện C phải là CDECL (nếu trình biên dịch theo mặc định sử dụng quy ước khác thì chúng ta phải đánh dấu gọi lại là CDECL) hoặc các lệnh gọi lại WinAPI khác nhau phải là STDCALL (toàn bộ WinAPI là STDCALL).

Trường hợp thông thường khác có thể là khi bạn đang lưu trữ con trỏ đến một số hàm bên ngoài, tức là để tạo một con trỏ tới hàm WinAPI, định nghĩa kiểu của nó phải được đánh dấu bằng STDCALL.

Và dưới đây là một ví dụ cho thấy cách trình biên dịch làm điều đó:

/* 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)

CÀI ĐẶT:

/* 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)

Lưu ý: __fastcall nhanh hơn __cdecl và STDCALL là quy ước gọi mặc định cho Windows 64-bit
dns

ồ; vì vậy nó phải bật địa chỉ trả về, thêm kích thước khối đối số, sau đó chuyển đến địa chỉ trả về đã xuất hiện trước đó? bạn sẽ cần phải chôn mình trở lại để kiểm tra lại ngăn xếp, đưa bạn trở lại vấn đề mà bạn chưa làm sạch ngăn xếp).
Dmitry

cách khác, bật trở lại reg1, bộ con trỏ ngăn xếp để con trỏ cơ sở, sau đó nhảy đến reg1
Dmitry

cách khác, di chuyển giá trị con trỏ ngăn xếp từ đỉnh ngăn xếp xuống dưới, sạch sẽ, sau đó gọi ret
Dmitry

15

Tôi nhận thấy một bài đăng nói rằng nó không quan trọng nếu bạn gọi một __stdcalltừ một __cdeclhoặc ngược lại. Nó có.

Lý do: với __cdeclcác đối số được truyền cho các hàm được gọi sẽ bị loại bỏ khỏi ngăn xếp bởi hàm đang gọi, trong đó __stdcall, các đối số được xóa khỏi ngăn xếp bởi hàm được gọi. Nếu bạn gọi một __cdeclhàm với a __stdcall, thì ngăn xếp sẽ không được dọn sạch, vì vậy cuối cùng khi hàm __cdeclsử dụng tham chiếu dựa trên xếp chồng cho các đối số hoặc địa chỉ trả về sẽ sử dụng dữ liệu cũ tại con trỏ ngăn xếp hiện tại. Nếu bạn gọi một __stdcallhàm từ a __cdecl, __stdcallhàm sẽ xóa các đối số trên ngăn xếp và sau đó __cdeclhàm thực hiện lại, có thể xóa thông tin trả về các hàm đang gọi.

Quy ước của Microsoft cho C cố gắng phá vỡ điều này bằng cách xáo trộn tên. Một __cdeclhàm được bắt đầu bằng dấu gạch dưới. Một __stdcallchức năng tiền tố với một dấu gạch dưới và hậu tố với một ít dấu hiệu “@” và số byte phải được loại bỏ. Ví dụ như __cdeclf (x) được liên kết như _f, __stdcall f(int x)được liên kết như _f@4nơi sizeof(int)là 4 byte)

Nếu bạn vượt qua được trình liên kết, hãy tận hưởng mớ hỗn độn gỡ lỗi.


3

Tôi muốn cải thiện câu trả lời của @ adf88. Tôi cảm thấy rằng mã giả cho STDCALL không phản ánh cách nó xảy ra trong thực tế. 'a', 'b' và 'c' không xuất hiện từ ngăn xếp trong thân hàm. Thay vào đó, chúng được xuất hiện bởi retlệnh ( ret 12sẽ được sử dụng trong trường hợp này) mà trong một lần lướt sẽ nhảy trở lại người gọi và đồng thời bật ra 'a', 'b' và 'c' từ ngăn xếp.

Đây là phiên bản của tôi được sửa chữa theo sự hiểu biết của tôi:

CÀI ĐẶT:

/* 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

Nó được chỉ định trong loại hàm. Khi bạn có một con trỏ hàm, nó được giả định là cdecl nếu không phải là stdcall rõ ràng. Điều này có nghĩa là nếu bạn nhận được một con trỏ stdcall và một con trỏ cdecl, bạn không thể trao đổi chúng. Hai loại hàm có thể gọi lẫn nhau mà không có vấn đề gì, nó chỉ nhận được một loại khi bạn mong đợi loại còn lại. Về tốc độ, cả hai đều thực hiện hai vai trò như nhau, chỉ là ở chỗ hơi khác một chút, thật sự không liên quan.


1

Người gọi và người gọi cần sử dụng cùng một quy ước tại điểm gọi - đó là cách duy nhất mà nó có thể hoạt động một cách đáng tin cậy. Cả người gọi và người gọi đều tuân theo một giao thức được xác định trước - ví dụ: ai cần dọn dẹp ngăn xếp. Nếu các quy ước không phù hợp, chương trình của bạn sẽ gặp phải hành vi không xác định - rất có thể chỉ gặp sự cố một cách ngoạn mục.

Điều này chỉ được yêu cầu cho mỗi trang web gọi - bản thân mã gọi có thể là một hàm với bất kỳ quy ước gọi nào.

Bạn sẽ không nhận thấy bất kỳ sự khác biệt thực sự nào về hiệu suất giữa các quy ước đó. Nếu điều đó trở thành vấn đề, bạn thường cần thực hiện ít cuộc gọi hơn - ví dụ: thay đổi thuật toán.


1

Những thứ đó thuộc về Trình biên dịch và Nền tảng cụ thể. Cả tiêu chuẩn C và C ++ đều không nói gì về việc gọi các quy ước ngoại trừ extern "C"trong C ++.

Làm thế nào để người gọi biết liệu nó có nên giải phóng ngăn xếp hay không?

Người gọi biết quy ước gọi của hàm và xử lý cuộc gọi tương ứng.

Tại trang web cuộc gọi, người gọi có biết hàm được gọi là cdecl hay hàm stdcall không?

Đúng.

Làm thế nào nó hoạt động ?

Nó là một phần của khai báo hàm.

Làm thế nào để người gọi biết liệu nó có nên giải phóng ngăn xếp hay không?

Người gọi biết các quy ước gọi và có thể hành động theo đó.

Hay đó là trách nhiệm của những người liên kết?

Không, quy ước gọi là một phần trong khai báo của một hàm nên trình biên dịch biết mọi thứ nó cần biết.

Nếu một hàm được khai báo là stdcall gọi một hàm (có quy ước gọi là cdecl) hoặc ngược lại, thì điều này có phù hợp không?

Không, tại sao phải làm vậy?

Nói chung, chúng ta có thể nói rằng cuộc gọi nào sẽ nhanh hơn - cdecl hay stdcall?

Tôi không biết. Kiểm tra nó.


0

a) Khi một hàm cdecl được gọi bởi người gọi, làm thế nào để người gọi biết nó có nên giải phóng ngăn xếp hay không?

Công cụ cdeclsửa đổi là một phần của nguyên mẫu hàm (hoặc loại con trỏ hàm, v.v.) để người gọi lấy thông tin từ đó và hành động tương ứng.

b) Nếu một hàm được khai báo là stdcall gọi một hàm (có quy ước gọi là cdecl) hoặc ngược lại, điều này có phù hợp không?

Không, không sao đâu.

c) Nói chung, chúng ta có thể nói rằng cuộc gọi nào sẽ nhanh hơn - cdecl hay stdcall?

Nói chung, tôi sẽ hạn chế bất kỳ tuyên bố nào như vậy. Vấn đề phân biệt ví dụ. khi bạn muốn sử dụng các hàm va_arg. Về lý thuyết, điều đó có thể stdcallnhanh hơn và tạo ra mã nhỏ hơn vì nó cho phép kết hợp việc bật các đối số với việc bật các cục bộ, nhưng với OTOH cdecl, bạn cũng có thể làm điều tương tự, nếu bạn thông minh.

Các quy ước gọi nhằm mục đích nhanh hơn thường thực hiện một số chuyển đăng ký.


-1

Các quy ước gọi không liên quan gì đến ngôn ngữ lập trình C / C ++ và là các chi tiết cụ thể về cách trình biên dịch thực hiện ngôn ngữ đã cho. Nếu bạn thường xuyên sử dụng cùng một trình biên dịch, bạn không bao giờ cần phải lo lắng về việc gọi các quy ước.

Tuy nhiên, đôi khi chúng ta muốn mã nhị phân được biên dịch bởi các trình biên dịch khác nhau để hoạt động chính xác với nhau. Khi làm như vậy, chúng ta cần xác định một thứ gọi là Giao diện nhị phân ứng dụng (ABI). ABI xác định cách trình biên dịch chuyển đổi nguồn C / C ++ thành mã máy. Điều này sẽ bao gồm các quy ước gọi, sắp xếp tên và bố cục bảng v. cdelc và stdcall là hai quy ước gọi khác nhau thường được sử dụng trên nền tảng x86.

Bằng cách đặt thông tin về quy ước gọi vào tiêu đề nguồn, trình biên dịch sẽ biết mã nào cần được tạo để liên hoạt động chính xác với tệp thực thi đã cho.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.