Если я не передаю достаточно параметров при вызове функции в DLL, что произойдет?

В проекте dll функция такова:

extern "C" __declspec(dllexport) void foo(const wchar_t* a, const wchar_t* b, const wchar_t* c)

В другом проекте я буду использовать функцию foo, но объявляю функцию foo в заголовочном файле с

extern "C" __declspec(dllimport) void foo(const wchar_t* a, const wchar_t* b)

и я называю его только двумя параметрами.

Результат - это успех, я думаю, что это о вызове __cdecl, но я хотел бы знать, как и почему это работает.

Ответы

Ответ 1

32-битный

Согласование вызовов по умолчанию - __cdecl, что означает, что вызывающий пользователь нажимает параметры на стек справа налево, затем очищает стек после вызова.

Итак, в вашем случае вызывающий:

  • Отбрасывает b
  • Отбрасывает
  • Выталкивает адрес возврата
  • Вызывает функцию.

В этот момент стек выглядит так (предположим, например, 4 байтовых указателя и помните, что указатель стека движется назад при нажатии на вещи):

+-----+ <--- this is where esp is after pushing stuff
| ret | [esp]
+-----+
|  a  | [esp+4]
+-----+
|  b  | [esp+8]
+-----+ <--- this is where esp was before we started
| ??? | [esp+12 and beyond]
+-----+

Хорошо, отлично. Теперь проблема возникает на стороне вызываемого абонента. Ожидается, что параметры будут находиться в определенных местах в стеке, поэтому:

  • a предполагается равным [esp+4]
  • b предполагается равным [esp+8]
  • c предполагается равным [esp+12]

И вот в чем проблема: мы понятия не имеем, что в [esp+12]. Таким образом, вызываемый будет видеть правильные значения a и b, но будет интерпретировать все неизвестные мусора, находящиеся в [esp+12] как c.

В этот момент это в значительной степени undefined, и зависит от того, что ваша функция действительно делает с c.

После того, как все закончится, и вызываемый возвращает, если ваша программа не сработала, вызывающий абонент восстановит esp, и указатель стека вернется туда, где он должен быть. Таким образом, из вызывающего POV все, вероятно, прекрасно, и указатель стека заканчивается там, где он должен быть, но вызывающий видит мусор для c.


64-битная

Механика на 64-битных машинах отличается, но конечный результат примерно такой же. Microsoft использует следующее соглашение о вызовах на 64-битных машинах независимо от __cdecl или что-то еще (любое соглашение, которое вы укажете, игнорируется, и все обрабатываются одинаково ):

  • Первые четыре целых числа или аргументы указателя, помещенные в регистры rcx, rdx, r8 и r9 в этом порядке слева направо.
  • Первые четыре аргумента с плавающей запятой помещаются в регистры xmm0, xmm1, xmm2 и xmm3 в этом порядке слева направо.
  • Все остальное помещается в стек, справа налево.
  • Ответчик несет ответственность за восстановление esp, а также восстановление значений всех изменчивых регистров после вызова.

Итак, в вашем случае вызывающий:

  • Помещает a в rcx.
  • Помещает b в rdx.
  • Выделяет дополнительные 32 байта "теневого пространства" в стеке (см. статью MS).
  • Отбрасывает адрес возврата.
  • Вызывает функцию.

Но ожидающий:

  • a предполагается, что он находится в rcx (проверьте!)
  • b предполагается, что он находится в rdx (проверьте!)
  • c предполагается в r8 (проблема)

Итак, как и в случае с 32-битным случаем, вызываемый интерпретирует все, что было в r8 как c, и появляются потенциальные hijinks, с конечным эффектом в зависимости от того, что делает вызываемый с c. Когда он возвращается, при условии, что программа не сработала, вызывающий восстанавливает все изменчивые регистры (rcx и rdx), а также обычно включает r8 и друзей) и восстанавливает esp.