__cdecl lub __stdcall w systemie Windows?

84

Obecnie pracuję nad biblioteką C ++ dla systemu Windows, która będzie dystrybuowana jako biblioteka DLL. Moim celem jest maksymalizacja interoperacyjności binarnej; dokładniej, funkcje w mojej bibliotece DLL muszą być użyteczne z kodu skompilowanego z wieloma wersjami MSVC ++ i MinGW bez konieczności ponownej kompilacji biblioteki DLL. Jednak nie wiem, która konwencja dzwonienia jest najlepsza cdecllub stdcall.

Czasami słyszę takie stwierdzenia, jak „Konwencja wywoływania w C jest jedyną, która gwarantuje, że będą takie same między kompilatorami”, co kontrastuje ze stwierdzeniami typu „ Istnieją pewne różnice w interpretacji cdecl, szczególnie w zwracaniu wartości ”. Wydaje się, że nie powstrzymuje to niektórych deweloperów bibliotek (takich jak libsndfile ) przed używaniem konwencji wywoływania C w dystrybuowanych przez nich bibliotekach DLL, bez żadnych widocznych problemów.

Z drugiej strony stdcallkonwencja wywoływania wydaje się być dobrze zdefiniowana. Z tego, co mi powiedziano, wszystkie kompilatory Windows są zasadniczo zobowiązane do przestrzegania tego, ponieważ jest to konwencja używana dla Win32 i COM. Opiera się to na założeniu, że kompilator systemu Windows bez obsługi Win32 / COM nie byłby zbyt przydatny. Wiele fragmentów kodu opublikowanych na forach deklaruje funkcje, stdcallale nie mogę znaleźć ani jednego wpisu, który jasno wyjaśnia dlaczego .

Jest tam zbyt wiele sprzecznych informacji, a każde wyszukiwanie, które przeprowadzam, daje mi różne odpowiedzi, co tak naprawdę nie pomaga mi zdecydować między nimi. Szukam jasnego, szczegółowego, uzasadnionego wyjaśnienia, dlaczego powinienem wybrać jedno z nich (lub dlaczego oba są równoważne).

Zwróć uwagę, że to pytanie dotyczy nie tylko „klasycznych” funkcji, ale także wywołań funkcji wirtualnych, ponieważ większość kodu klienta będzie się łączyć z moją biblioteką DLL za pośrednictwem „interfejsów”, czystych klas wirtualnych (według wzorców opisanych np. Tu i tam ).

Etienne Dechamps
źródło
4
„Istnieją pewne różnice w interpretacji cdecl, szczególnie w sposobie zwracania wartości” - oznacza to w przybliżeniu, że różne systemy operacyjne używające cdecl na x86 mogą używać różnych wariantów cdecl, czyli różnych ABI, które oba nazywają „cdecl”. Windows przypina różne warianty, każda implementacja, która działa w systemie Windows i nie respektuje wyborów systemu Windows, nie będzie w stanie wywołać funkcji cdecl systemu Windows, więc jak mówisz w przypadku stdcall, jest bezużyteczna do programowania w systemie Windows i istnieje kilka standardowych C funkcje, które byłyby trudne do wdrożenia.
Steve Jessop
1
Konwencja wywoływania __stdcall służy do wywoływania funkcji API Win32. docs.microsoft.com/en-us/cpp/cpp/stdcall?view=vs-2017
Zanna,

Odpowiedzi:

79

Właśnie przeprowadziłem kilka testów w świecie rzeczywistym (kompilowałem biblioteki DLL i aplikacje z MSVC ++ i MinGW, a następnie mieszałem je). Jak się okazuje, miałem lepsze wyniki z cdeclkonwencją wywoływania.

Dokładniej: problem stdcallpolega na tym, że MSVC ++ zmienia nazwy w tabeli eksportu DLL, nawet jeśli jest używany extern "C". Na przykład foostaje się _foo@4. Dzieje się tak tylko podczas używania __declspec(dllexport), a nie podczas używania pliku DEF; Jednak moim zdaniem pliki DEF są kłopotliwe w utrzymaniu i nie chcę ich używać.

Zniekształcenie nazwy MSVC ++ stwarza dwa problemy:

  • Korzystanie GetProcAddressz biblioteki DLL staje się nieco bardziej skomplikowane;
  • MinGW domyślnie nie dodaje znaku podkreślenia do dekorowanych nazw (np. MinGW użyje foo@4 zamiast _foo@4), co komplikuje linkowanie. Wprowadza również ryzyko zobaczenia „wersji bez podkreślenia” bibliotek DLL i aplikacji, które są niekompatybilne z „wersjami z podkreśleniem”.

Wypróbowałem cdeclkonwencję: interoperacyjność między MSVC ++ i MinGW działa doskonale, zaraz po wyjęciu z pudełka, a nazwy pozostają niezdekorowane w tabeli eksportu DLL. Działa nawet w przypadku metod wirtualnych.

Z tych powodów cdecljest dla mnie wyraźnym zwycięzcą.

Etienne Dechamps
źródło
W szczególności nie dotyczy to 64-bitowych bibliotek DLL, ponieważ __stdcall i __cdecl są zwykle ignorowane - więc w eksportowanych funkcjach języka C nie występuje zmiana nazwy.
jtbr
@jtbr A co z wyeksportowanymi funkcjami C ++ (tj. nie extern "C")?
Ela782
@ Ela782 C ++ zawsze będzie mieć jakieś zniekształcenia nazw, aby obsłużyć przeciążenie funkcji. Ale to nie jest znormalizowane i dlatego może się różnić w zależności od kompilatora.
jtbr
@jtbr Przepraszam, nie miałem na myśli zniekształcenia nazwy. Miałem na myśli, czy __stdcall i __cdecl są również ignorowane w 64-bitowych bibliotekach DLL z wyeksportowanymi funkcjami C ++.
Ela782
1
@ Ela782 Yes; Uważam, że __stdcall i __cdecl są ignorowane we wszystkich kompilacjach x86-64. Zobacz wikipedię .
jtbr
20

Największą różnicą w tych dwóch konwencjach wywoływania jest to, że " _ _cdecl" nakłada ciężar równoważenia stosu po wywołaniu funkcji na obiekt wywołujący, co pozwala na funkcje ze zmienną ilością argumentów. Konwencja „__stdcall” ma „prostszy” charakter, jednak jest mniej elastyczna pod tym względem.

Uważam również, że języki zarządzane domyślnie używają konwencji stdcall, więc każdy, kto używa na nim P / Invoke, musiałby jawnie określić konwencję wywoływania, jeśli korzystasz z cdecl.

Tak więc, jeśli wszystkie sygnatury funkcji mają być zdefiniowane statycznie, prawdopodobnie skłaniałbym się w stronę stdcall, jeśli nie cdecl.

Brandon Moretz
źródło
11

Z punktu widzenia bezpieczeństwa, __cdeclkonwencja jest „bezpieczniejsza”, ponieważ to wywołujący musi zwolnić przydział stosu. W __stdcallbibliotece może się zdarzyć, że programista mógł zapomnieć o poprawnym zwolnieniu przydziału stosu lub osoba atakująca może wstrzyknąć jakiś kod, uszkadzając stos DLL (np. Przez przechwycenie API), który następnie nie jest sprawdzany przez wywołującego. Nie mam żadnych przykładów bezpieczeństwa CVS, które pokazują, że moja intuicja jest poprawna.

user2471214
źródło