Почему не происходит сбой PInvoke в случае нарушения соглашения о вызовах (в .NET 3.5)?

В моем решении есть неуправляемая C++ DLL, которая экспортирует функцию, и управляемое приложение, которое PInvokes эту функцию.

Я только что преобразовал решение из .NET 3.5 в .NET 4.0 и получил исключение PInvokeStackImbalance "Вызов функции PInvoke [...] привел к дисбалансу стека". Как оказалось, я вызывал функцию __cdecl'ed, так как это было __stdcall:

Часть С++ (вызываемый):

__declspec(dllexport) double TestFunction(int param1, int param2); // by default is __cdecl

Часть С# (вызывающий):

[DllImport("TestLib.dll")] // by default is CallingConvention.StdCall
private static extern double TestFunction(int param1, int param2);

Итак, я исправил ошибку, но теперь мне интересно, как это работало в .NET 3.5? Почему (многократно повторяющаяся) ситуация, когда никто (ни callee, ни caller) не очищает стек, не вызывала переполнения стека или каких-либо других нарушений, а просто работала нормально? Есть ли какая-то проверка в PInvoke, как упоминал Рэймонд Чен в его статья? Также интересно, почему противоположный тип нарушения соглашения (когда __stdcall вызывается PInvoked как __cdecl) вообще не работает, вызывая только EntryPointNotFoundException.


person Alex Che    schedule 17.02.2011    source источник


Ответы (4)


PInvokeStackImbalance не является исключением. Это предупреждение MDA, реализованное управляемым помощником по отладке. Наличие этого MDA не является обязательным, вы можете настроить его в диалоговом окне «Отладка + исключения». Он никогда не будет активен при запуске без отладчика.

Несбалансированность стека может вызвать довольно неприятные проблемы, начиная от странного повреждения данных и заканчивая получением SOE или AVE. Тоже очень сложно диагностировать. Но это также может не вызвать никаких проблем, указатель стека восстанавливается, когда метод возвращается.

Код, скомпилированный в 64-разрядную систему, имеет тенденцию быть устойчивым, гораздо больше аргументов функции передаются через регистры, а не через стек. Он выйдет из строя, если будет принудительно работать на x86, новом стандарте по умолчанию для VS2010.

person Hans Passant    schedule 17.02.2011
comment
Спасибо за разъяснения по поводу MDA. - person Alex Che; 18.02.2011

После некоторого расследования:

Помощником, спасающим ситуацию от сбоя, является еще один регистр - EBP, базовый указатель, указывающий на начало кадра стека. Весь доступ к локальным переменным функции осуществляется через этот указатель (кроме оптимизированного кода, см. редактирование ниже). Перед возвратом функции указатель стека сбрасывается до значения базового указателя.

Прежде чем функция (скажем, PInvoke) вызовет другую функцию (импортированную функцию DLL), указатель стека указывает на конец локальных переменных вызывающей функции. Затем вызывающий объект помещает параметры в стек и вызывает эту другую функцию.

В описанной ситуации, когда функция вызывает другую функцию как __stdcall, хотя на самом деле это __cdecl, никто не очищает стек от этих параметров. Таким образом, после возврата из вызываемого объекта указатель стека указывает на конец переданного блока параметров. Это похоже на то, что вызывающая функция (PInvoke) только что получила еще несколько локальных переменных.

Поскольку доступ к локальным переменным вызывающей стороны осуществляется через базовый указатель, это ничего не ломает. Единственная плохая вещь, которая может случиться, это если вызываемая функция будет вызываться много раз одновременно. В этом случае стек будет расти и может переполниться. Но поскольку PInvoke вызывает функцию DLL только один раз, а затем возвращается, указатель стека просто сбрасывается до базового указателя, и все в порядке. Изменить: Как отмечено здесь, код также может быть оптимизирован для хранения локальных переменных в регистрах ЦП. Только. В этом случае EBP не используется, поэтому неверный ESP может привести к возврату на неверный адрес.

person Alex Che    schedule 18.02.2011
comment
Единственная плохая вещь, которая может случиться, это достаточно оптимистичная с сегодняшней точки зрения формулировка. См. этот вопрос для другого плохого сценария (все локальные переменные в регистрах, EBP вообще уходит со сцены). .NET 3.5 по-прежнему исправляет это за вас (за счет производительности), но .NET 4 по умолчанию этого не делает, и вы будете падать. - person Jirka Hanika; 21.11.2014

Стоит отметить, что причина, по которой это изменилось между версиями 3.5 и 4, заключается в том, что поведение по умолчанию для PInvoke изменилось. В версии 3.5 и ранее он проверял такие вещи, как описал Алекс, и исправлял их. Это вызывает некоторые накладные расходы, поскольку проверку необходимо выполнять при каждом вызове PInvoke. В .NET 4 поведение изменилось на не выполнять эту проверку, чтобы устранить падение производительности при правильных вызовах. Вместо этого было добавлено предупреждение MDA.

Старое поведение можно повторно включить с помощью параметра app.config NetFx40_PInvokeStackResilience (http://msdn.microsoft.com/en-us/library/ff361650.aspx).

person Martin Wilkerson    schedule 07.09.2011

При использовании DllImport по умолчанию фактически используется WinApi, а не StdCall. WinApi на самом деле не является соглашением, а представляет собой соглашение по умолчанию в системе. Возможно, в .Net 3.5 WinApi представлял _cdecl, а сейчас — __stdcall.

Я действительно не думаю, что это так, поскольку я всегда должен был указывать __stdcall (или, скорее, WINAPI) при использовании P/Invoke. Я не совсем понимаю, почему это сработало в .Net 3.5. (Возможно, тогда DllImport был ленив и просто «проглядел» соглашение о вызовах — это было бы странно)

person Ken Wayne VanderLinde    schedule 17.02.2011
comment
Да, я должен был уточнить, что мое приложение работает в Windows, где WinApi означает StdCall. - person Alex Che; 18.02.2011
comment
Как я писал в своем ответе, похоже, что функция PInvoking CDecl, поскольку StdCall является «безопасной». Это дополнительная проверка MDA, добавленная в .NET 4.0, которая приводит к сбою приложения. - person Alex Che; 18.02.2011