Линейная сборка GCC со стеком
Мне нужен такой встроенный код сборки:
- У меня есть пара (так, это сбалансировано) операции push/pop внутри сборки
- У меня также есть переменная в памяти (так, не регистр) в качестве ввода
как это:
__asm__ __volatile__ ("push %%eax\n\t"
// ... some operations that use ECX as a temporary
"mov %0, %%ecx\n\t"
// ... some other operation
"pop %%eax"
: : "m"(foo));
// foo is my local variable, that is to say, on stack
При разборке скомпилированного кода компилятор дает адрес памяти как 0xc(%esp)
это относительно esp
следовательно, этот фрагмент кода не будет работать правильно, так как у меня есть push
операция до mov
, Поэтому, как я могу сказать, компиляция мне не нравится foo
относительно esp
, но любая вещь, как -8(%ebp)
по отношению к Ebp.
PS Можете предложить что я могу поставить eax
внутри Clobbers, но это всего лишь пример кода. Я не люблю показывать причину, по которой я не принимаю это решение.
3 ответа
Как правило, следует избегать изменения ESP внутри inline-asm, когда у вас есть какие-либо входы / выходы памяти, поэтому вам не нужно отключать оптимизацию или заставлять компилятор создавать стековый фрейм с EBP другим способом. Одним из основных преимуществ является то, что вы (или компилятор) можете использовать EBP в качестве дополнительного бесплатного регистра; потенциально значительное ускорение, если вам уже приходится разливать / перезаряжать вещи. Если вы пишете встроенный asm, вероятно, это горячая точка, поэтому стоит потратить лишний размер кода, чтобы использовать режимы ESP-относительной адресации.
В коде x86-64 есть дополнительное препятствие для безопасного использования push/pop, потому что вы не можете сказать компилятору, что вы хотите заглушить красную зону под RSP. (Вы можете скомпилировать с -mno-red-zone
, но нет способа отключить его из источника C.) Подобные проблемы могут возникнуть, когда вы забьете данные компилятора в стек. Ни у одного 32-битного x86 ABI нет красной зоны, поэтому это применимо только к x86-64 System V. (Или к не-x86 ISA с красной зоной).
Вам нужно только отключить -fomit-frame-pointer
для этой функции, если вы хотите делать такие вещи только для asm, как push
как структура данных стека, так что есть переменная величина толчка. Или, может быть, если оптимизировать для размера кода.
Вы всегда можете написать целую не встроенную функцию в asm и поместить ее в отдельный файл, тогда у вас будет полный контроль. Но делайте это только в том случае, если ваша функция включает в себя весь цикл; не делайте компилятор call
короткая нецикличная функция внутри внутреннего цикла C.
Кажется, вы используете push
/ pop
внутри встроенного asm, потому что у вас недостаточно регистров, и вам нужно что-то сохранить / перезагрузить. Вам не нужно использовать push/pop для сохранения / восстановления. Вместо этого используйте фиктивные выходные операнды с "=m"
ограничения, чтобы заставить компилятор выделять место в стеке для вас, и использовать mov
в / из этих слотов. (Конечно, вы не ограничены mov
; использование операнда источника памяти для инструкции ALU может быть выигрышным, если вам нужно только значение один или два раза.)
Это может быть немного хуже для размера кода, но обычно не хуже для производительности (и может быть лучше). Если этого недостаточно, напишите всю функцию (или весь цикл) в asm, чтобы вам не пришлось бороться с компилятором.
int foo(char *p, int a, int b) {
int t1,t2; // dummy output spill slots
int r1,r2; // dummy output tmp registers
int res;
asm ("# operands: %0 %1 %2 %3 %4 %5 %6 %7 %8\n\t"
"imull $123, %[b], %[res]\n\t"
"mov %[res], %[spill1]\n\t"
"mov %[a], %%ecx\n\t"
"mov %[b], %[tmp1]\n\t" // let the compiler allocate tmp regs, unless you need specific regs e.g. for a shift count
"mov %[spill1], %[res]\n\t"
: [res] "=&r" (res),
[tmp1] "=&r" (r1), [tmp2] "=&r" (r2), // early-clobber
[spill1] "=m" (t1), [spill2] "=&rm" (t2) // allow spilling to a register if there are spare regs
, [p] "+&r" (p)
, "+m" (*(char (*)[]) p) // dummy in/output instead of memory clobber
: [a] "rmi" (a), [b] "rm" (b) // a can be an immediate, but b can't
: "ecx"
);
return res;
// p unused in the rest of the function
// so it's really just an input to the asm,
// which the asm is allowed to destroy
}
Это компилируется в следующий asm с gcc7.3 -O3 -m32
на проводнике компилятора Godbolt. Обратите внимание на asm-комментарий, показывающий, что компилятор выбрал для всех операндов шаблона: он выбрал 12(%esp)
за %[spill1]
и% edi for
% [Spill2] (because I used
"= & Гт" for that operand, so the compiler saved/restore
% edi` вне asm, и дал его нам для этого фиктивного операнда).
foo(char*, int, int):
pushl %ebp
pushl %edi
pushl %esi
pushl %ebx
subl $16, %esp
movl 36(%esp), %edx
movl %edx, %ebp
#APP
# 19 "/tmp/compiler-explorer-compiler118120-55-w92ge8.v797i/example.cpp" 1
# operands: %eax %ebx %esi 12(%esp) %edi %ebp (%edx) 40(%esp) 44(%esp)
imull $123, 44(%esp), %eax
mov %eax, 12(%esp)
mov 40(%esp), %ecx
mov 44(%esp), %ebx
mov 12(%esp), %eax
# 0 "" 2
#NO_APP
addl $16, %esp
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
Хм, фиктивный операнд памяти, сообщающий компилятору, какую память мы модифицируем, похоже, привел к тому, что для этого был выделен регистр, так как p
Операнд ранний клоббер, поэтому он не может использовать тот же регистр. Я полагаю, вы могли бы рискнуть оставить ранний клоббер, если уверены, что ни один из других входов не будет использовать тот же регистр, что и p
, (то есть, что они не имеют одинаковое значение).
Прямое использование указателя стека для ссылки на локальные переменные, вероятно, вызвано использованием оптимизации компилятора. Я думаю, что вы можете решить эту проблему несколькими способами:
- Отключение оптимизации указателя кадра (
-fno-omit-frame-pointer
в GCC); - Вставка
esp
в Clobbers, так что компилятор будет знать, что его значение изменяется (проверьте совместимость вашего компилятора).
Вместо того, чтобы помещать движение в ecx в коде сборки, поместите операнд в ecx напрямую:
: : "c"(foo)