Ответ 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
.