Почему PInvoke не падает в случае нарушения соглашения о вызовах (в .NET 3.5)? - PullRequest
4 голосов
/ 18 февраля 2011

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

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

C ++ part (вызываемый):

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

C # part (вызывающий):

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

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

Ответы [ 4 ]

7 голосов
/ 18 февраля 2011

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

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

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

5 голосов
/ 18 февраля 2011

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

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

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

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

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

4 голосов
/ 07 сентября 2011

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

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

0 голосов
/ 18 февраля 2011

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

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

...