Ваша текущая версия с pusha
/ popa
выглядит правильно (медленно, но безопасно), если ваше соглашение о вызовах не зависит от поддержания выравнивания стека в 16 байтов.
Если происходит сбой, ваша настоящая проблема в другом месте, поэтому вы должны использовать отладчик и выяснить , где он падает .
Если объявить клобберы на eax
/ ecx
/ edx
или запросить указатели в двух из этих регистров и перекрыть третий, это позволит вам избежать pusha
/ popa
. (Или независимо от того, что reg-clobbered'ы относятся к соглашению о вызовах, которое вы используете.)
Вы должны удалить .intel_syntax noprefix
. Вы уже зависите от компиляции с -masm=intel
, потому что вы не восстанавливаете предыдущий режим, если это был AT & T. (Я не думаю, что есть способ сохранить / восстановить старый режим, к сожалению, но есть механизм диалекта-альтернативы для использования различных шаблонов для различных режимов синтаксиса.)
Для этого вам не нужно и не следует использовать встроенный ассемблер
компиляторы уже знают, как выполнять вызовы функций, когда вы используете стандартное соглашение о вызовах (в этом случае: стек аргументов в 32-битном режиме, который обычно используется по умолчанию).
Это допустимый C ++ для приведения целого числа к указателю функции , и это даже не неопределенное поведение, если там действительно есть функция по этому адресу.
void function(const char* text) {
typedef void (*char_func_t)(const char *);
char_func_t func = (char_func_t)0x004169E0;
func(text);
}
В качестве бонуса он более эффективно компилируется с MSVC, чем ваша версия asm.
Вы можете использовать атрибуты функции GCC в указателях функций, чтобы явно указать соглашение о вызовах , если вы компилируете с другим значением по умолчанию. Например, __attribute__((cdecl))
для явного указания аргументов стека и вызовов для вызовов с использованием этого указателя функции. Эквивалент MSVC составляет всего __cdecl
.
#ifdef __GNUC__
#define CDECL __attribute__((cdecl))
#define STDCALL __attribute__((stdcall))
#elif defined(_MSC_VER)
#define CDECL __cdecl
#define STDCALL __stdcall
#else
#define CDECL /*empty*/
#define STDCALL /*empty*/
#endif
// With STDCALL instead of CDECL, this function has to translate from one calling convention to another
// so it can't compile to just a jmp tailcall
void function(const char* text) {
typedef void (CDECL *char_func_t)(const char *);
char_func_t func = (char_func_t)0x004169E0;
func(text);
}
Чтобы увидеть вывод asm компилятора , я поместил это в проводник компилятора Godbolt . Я использовал опцию «intel-syntax», поэтому вывод gcc приходит с gcc -S -masm=intel
# gcc8.1 -O3 -m32 (the 32-bit Linux calling convention is close enough to Windows)
# except it requires maintaing 16-byte stack alignment.
function(char const*):
mov eax, 4286944
jmp eax # tail-call with the args still where we got them
Этот вызывающий тест заставляет компилятор устанавливать аргументы, а не просто хвостовой вызов , но function
может встроить его в него.
int caller() {
function("hello world");
return 0;
}
.LC0:
.string "hello world"
caller():
sub esp, 24 # reserve way more stack than it needs to reach 16-byte alignment, IDK why.
mov eax, 4286944 # your function pointer
push OFFSET FLAT:.LC0 # addr becomes an immediate
call eax
xor eax, eax # return 0
add esp, 28 # add esp, 4 folded into this
ret
Вывод
MSVC -Ox
для caller
практически одинаков:
caller PROC
push OFFSET $SG2661
mov eax, 4286944 ; 004169e0H
call eax
add esp, 4
xor eax, eax
ret 0
Но версия с вашим встроенным ассемблером намного хуже :
;; MSVC -Ox on a caller() that uses your asm implementation of function()
caller_asm PROC
push ebp
mov ebp, esp
sub esp, 8
; store inline asm inputs to the stack
mov DWORD PTR _addr$2[ebp], OFFSET $SG2671
mov DWORD PTR _fncAddr$1[ebp], 4286944 ; 004169e0H
push DWORD PTR _addr$2[ebp] ; then reload as memory operands
call DWORD PTR _fncAddr$1[ebp]
add esp, 4
xor eax, eax
mov esp, ebp ; makes the add esp,4 redundant in this case
pop ebp
ret 0
Встроенный синтаксис MSVC asm в основном отстой, потому что в отличие от синтаксиса GNU C asm входные данные всегда должны находиться в памяти, а не в регистрах или непосредственно. Таким образом, вы могли бы добиться большего успеха с GNU C, но не так хорошо, как вы могли бы, вообще избегая inline asm. https://gcc.gnu.org/wiki/DontUseInlineAsm.
Как правило, следует избегать вызовов функций из встроенного asm; гораздо безопаснее и эффективнее, когда компилятор знает, что происходит.