Перейти / позвонить в другую функцию - PullRequest
0 голосов
/ 30 октября 2018

У меня есть две функции, похожие на C ++:

void f1(...);
void f2(...);

Я могу изменить тело f1, но f2 определено в другой библиотеке, которую я не могу изменить. Я абсолютно должен (хвост) вызвать f2 внутри f1, и я должен передать все аргументы f1 f2, но, насколько я знаю, это невозможно в чистом виде C или C ++. К сожалению, нет альтернативы f2, которая принимает va_list. Вызов f2 происходит последним в функции, поэтому мне нужна некоторая форма tailcall.

Я решил использовать ассемблер, чтобы вытолкнуть кадр стека текущей функции, а затем перейти к f2 (он фактически принимается как указатель на функцию и в переменную, поэтому я сначала сохраняю его в регистре):

__asm {
    mov eax, f2
    leave
    jmp eax
}

В MSVC ++, в Debug, он сначала кажется работающим, но он каким-то образом портится с возвращаемыми значениями других функций, а иногда происходит сбой. В Release всегда вылетает.

Является ли этот ассемблерный код неправильным или некоторые оптимизации компилятора каким-то образом нарушают этот код?

Ответы [ 2 ]

0 голосов
/ 31 октября 2018

Вы должны написать 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.

Если есть потенциально зарегистрированные аргументы, сохраните их в пространстве стека перед копируемыми вами аргументами и восстановите их перед вызовом хвоста.

0 голосов
/ 30 октября 2018

Компилятор не дает никаких гарантий в том месте, где вы копаетесь. Функция батута может сработать, но вы должны сохранить состояние между ними и много копаться.

Вот скелет, но вам нужно много знать о соглашениях о вызовах, вызове метода класса и т. Д. /

* argn, ..., arg0, retaddr */
trampoline:
    push < all volatile regs >
    call <get thread local storage >
    copy < volatile regs and ret addr > to < local storage >
    pop < volatile regs >
    remove ret addr
    call  f2
    call < get thread local storage >
    restore < volatile regs and ret addr>
    jmp f1
    ret
...