Линейная сборка 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)
Другие вопросы по тегам