Вы должны написать f1
в чистом asm, чтобы оно было гарантированно безопасным.
Во всех основных соглашениях о вызовах x86 вызываемый объект "владеет" аргументами и может изменять пространство стека, в котором они хранятся. (Изменяет ли их источник C или нет, объявлены ли они const
).
например. void foo(int x) { x += 1; bar(x); }
может изменить пространство стека над адресом возврата, который содержит x
, если скомпилировано с отключенной оптимизацией. Для повторного вызова с теми же аргументами требуется их сохранение, если только вы не знаете, что вызываемый не наступил на них. Тот же аргумент применяется для вызова хвоста с конца одной функции.
Я проверил в проводнике компилятора Godbolt ; MSVC и gcc на самом деле изменяют x
в стеке в отладочных сборках. gcc использует add DWORD PTR [ebp+8], 1
перед нажатием [ebp+8]
.
На практике компиляторы могут на самом деле не использовать это преимущество для функций с переменным числом, хотя, поэтому, в зависимости от определений ваших функций, вам может это не сойти с рук, если вы сможете убедить их сделать хвостовой вызов.
Обратите внимание, что void bar(...);
это , а не действительный прототип в C, хотя:
# gcc -xc on Godbolt to force compiling as C, not C++
<source>:1:10: error: ISO C requires a named argument before '...'
Это допустимо в C ++, или, по крайней мере, g ++ принимает его, а gcc - нет. MSVC принимает его в режиме C ++, , но не в режиме C . (У Godbolt есть целый отдельный режим C с другим набором компиляторов, который вы можете использовать, чтобы MSVC компилировал код как C вместо C ++. Я не знаю параметра командной строки, чтобы перевести его в режим C, как gcc имеет -xc
и -xc++
)
В любом случае, В оптимизированных сборках может сработать запись f2();
в конце f1
, но это неприятно и полностью лжет компилятору о том, какие аргументы передаются. И, конечно, работает только для соглашения о вызовах без аргументов регистра. (Но вы показывали 32-битный asm, так что вы вполне могли бы использовать соглашение о вызовах без аргументов регистра.)
Любой приличный компилятор будет использовать jmp f2
для выполнения оптимизированного хвостового вызова в этом случае, потому что они оба возвращают void
. (Для не пустых, вы бы return f2();
)
Кстати, если mov eax, f2
работает, то jmp f2
также будет работать.
Однако ваш код не может работать в оптимизированной сборке, потому что вы предполагаете, что компилятор создал унаследованный стековый фрейм и что функция нигде не будет встроена.
Это небезопасно даже в отладочной сборке, потому что компилятор может push
редактировать некоторые регистры, сохраняемые при вызове, которые нужно вытолкнуть перед выходом из функции (и перед запуском leave
для уничтожения фрейма стека).
Идея батута, показанная @mevets, может быть упрощена: если есть разумный фиксированный верхний предел размера для аргументов, вы можете скопировать, возможно, 64 или 128 байтов потенциальных аргументов из ваших входящих аргументов в аргументы для f1
. Несколько SIMD векторов сделают это. Затем вы можете позвонить f1
в обычном режиме, а затем завершить вызов f2
из вашей оболочки asm.
Если есть потенциально зарегистрированные аргументы, сохраните их в пространстве стека перед копируемыми вами аргументами и восстановите их перед вызовом хвоста.