Сборка — Использование базового регистра указателя в C ++ inline asm

Я хочу иметь возможность использовать регистр указателя базы (%rbpв пределах встроенного ассм. Игрушечный пример этого выглядит так:

void Foo(int &x)
{
asm volatile ("pushq %%rbp;"         // 'prologue'
"movq %%rsp, %%rbp;"   // 'prologue'
"subq $12, %%rsp;"     // make room

"movl $5, -12(%%rbp);" // some asm instruction

"movq %%rbp, %%rsp;"  // 'epilogue'
"popq %%rbp;"         // 'epilogue'
: : : );
x = 5;
}

int main()
{
int x;
Foo(x);
return 0;
}

Я надеялся, что, так как я использую обычный метод вызова функции пролога / эпилога, толкая и выталкивая старый %rbpэто было бы хорошо. Однако при попытке доступа к нему возникают ошибки x после встроенного ассм.

Генерируемый GCC код сборки (слегка урезанный):

_Foo:
pushq   %rbp
movq    %rsp, %rbp
movq    %rdi, -8(%rbp)

# INLINEASM
pushq %rbp;          // prologue
movq %rsp, %rbp;     // prologue
subq $12, %rsp;      // make room
movl $5, -12(%rbp);  // some asm instruction
movq %rbp, %rsp;     // epilogue
popq %rbp;           // epilogue
# /INLINEASM

movq    -8(%rbp), %rax
movl    $5, (%rax)      // x=5;
popq    %rbp
ret

main:
pushq   %rbp
movq    %rsp, %rbp
subq    $16, %rsp
leaq    -4(%rbp), %rax
movq    %rax, %rdi
call    _Foo
movl    $0, %eax
leave
ret

Может кто-нибудь сказать мне, почему это ошибки сег? Кажется, я как-то развращен %rbp но я не вижу как. Заранее спасибо.

Я использую GCC 4.8.4 на 64-битной Ubuntu 14.04.

11

Решение

Смотрите в нижней части этого ответа коллекцию ссылок на другие inline-asm Q&Как.


Что вы надеетесь научиться делать с помощью встроенного ассема? Если вы хотите изучить inline asm, научитесь использовать его для создания эффективного кода, а не таких ужасных вещей, как этот. Если вы хотите написать пролог функции и нажать / поп, чтобы сохранить / восстановить регистры, вы должны написать целые функции в asm. (Тогда вы можете легко использовать Nasm или Yasm, а не менее предпочтительный по большинству AT&Синтаксис T с директивами ассемблера GNU1.)

Встроенный asm GNU сложен в использовании, но позволяет вам смешивать пользовательские фрагменты asm в C и C ++, позволяя компилятору обрабатывать распределение регистров и любое сохранение / восстановление в случае необходимости. Иногда компилятор сможет избежать сохранения и восстановления, предоставив вам регистр, который может быть закрыт. Без volatile, он может даже выводить операторы asm из циклов, когда ввод будет одинаковым. (т.е. если вы не используете volatileпредполагается, что выходы являются «чистой» функцией входов.)

Если вы просто пытаетесь изучать asm, GNU inline asm — ужасный выбор. Вы должны полностью понять почти все, что происходит с ассемблером, и понять, что должен знать компилятор, чтобы написать правильные ограничения ввода / вывода и получить все правильно. Ошибки приведут к разбиванию вещей и трудно отлаживаемым поломкам. Вызов функции ABI намного проще и проще отслеживать границы между вашим кодом и кодом компилятора.


Вы составлено с -O0, поэтому код GCC проливает параметр функции из %rdi к месту в стеке. (Это может произойти в нетривиальной функции даже при -O3). Поскольку целевой ABI является x86-64 SysV ABI, он использует «Красную зону» (128B ниже %rsp что даже асинхронные обработчики сигналов не имеют права на клоббер), вместо того, чтобы тратить инструкцию, уменьшающую указатель стека на резервное пространство.

Он хранит функцию указателя 8B arg в -8(rsp_at_function_entry), Тогда ваш встроенный асм толкает %rbp, который уменьшает% rsp на 8, а затем записывает туда, разбивая нижние 32b &x (указатель).

Когда ваш встроенный ассм закончен,

  • GCC перезагружается -8(%rbp) (который был перезаписан с %rbp) и использует его в качестве адреса для магазина 4B.
  • Foo возвращается к main с %rbp = (upper32)|5 (значение orig с низким 32, установленным в 5).
  • main работает leave: %rsp = (upper32)|5
  • main работает ret с %rsp = (upper32)|5, читая обратный адрес с виртуального адреса (void*)(upper32|5), который из вашего комментария 0x7fff0000000d,

Я не проверял с отладчиком; один из этих шагов может быть немного отключен, но проблема определенно в том, что вы загоняете красную зону, приводя к коду gcc, разбивающему стек.

Даже добавление «памяти» clobber не дает gcc избежать использования красной зоны, поэтому выглядит выделение собственной памяти стека из встроенного asm — просто плохая идея. (Запоминание означает, что вы могли записать память, в которую вам разрешено писать, а не то, что вы могли перезаписать то, что не должны были делать.)

Если вы хотите использовать пустое пространство из встроенного asm, вам, вероятно, следует объявить массив как локальную переменную и использовать его как операнд только для вывода (который вы никогда не читаете).


Вот что ты должен был сделать:

void Bar(int &x)
{
int tmp;
long tmplong;
asm ("lea  -16 + %[mem1], %%rbp\n\t""imul $10, %%rbp, %q[reg1]\n\t"  // q modifier: 64bit name.
"add  %k[reg1], %k[reg1]\n\t"    // k modifier: 32bit name
"movl $5, %[mem1]\n\t" // some asm instruction writing to mem
: [mem1] "=m" (tmp), [reg1] "=r" (tmplong)  // tmp vars -> tmp regs / mem for use inside asm
:
: "%rbp" // tell compiler it needs to save/restore %rbp.
// gcc refuses to let you clobber %rbp with -fno-omit-frame-pointer (the default at -O0)
// clang lets you, but memory operands still use an offset from %rbp, which will crash!
// gcc memory operands still reference %rsp, so don't modify it.  Declaring a clobber on %rsp does nothing
);
x = 5;
}

Обратите внимание на толчок / популярность %rbp в коде за пределами #APP / #NO_APP раздел, испускаемый gcc. Также обратите внимание, что чистая память, которую он вам дает, находится в красной зоне. Если вы компилируете с -O0, вы увидите, что он находится в другом месте, где он проливается &x,

Чтобы получить больше чистых регистров, лучше просто объявить больше выходных операндов, которые никогда не используются окружающим не-asm-кодом. Это оставляет распределение регистров для компилятора, поэтому оно может быть различным, если встроено в разные места. Выбор заблаговременно и объявление Clobber имеет смысл, только если вам нужно использовать определенный регистр (например, счетчик сдвига в %cl). Конечно, ограничение ввода, как "c" (count) получает gcc для установки счетчика в rcx / ecx / cx / cl, поэтому вы не создаете потенциально избыточный mov %[count], %%ecx,

Если это выглядит слишком сложно, не используйте встроенный ассм. Или приведи компилятор к асм, который ты хочешь с C это как оптимальный asm, или написать целую функцию в asm.

При использовании встроенного asm, сохраняйте его как можно меньшим: в идеале это всего лишь одна или две инструкции, которые gcc не отправляет самостоятельно, с ограничениями ввода / вывода, указывающими, как вводить / выводить данные из оператора asm. Это то, для чего он предназначен.

Основное правило: если ваш встроенный ассемблер GNU C начинается или заканчивается movвы обычно делаете это неправильно и должны были использовать вместо этого ограничение.


Сноски:

  1. Вы можете использовать Intel-синтаксис GAS в inline-asm, создав -masm=intel (в этом случае ваш код будет только работать с этой опцией), или используя диалектные альтернативы так что работает с компилятором в Intel или AT&T asm выходной синтаксис. Но это не меняет директив, и Intel-синтаксис GAS недостаточно документирован. (Это похоже на MASM, а не NASM.) Я действительно не рекомендую это, если вы действительно не ненавидите AT&T синтаксис.

Некоторые из них повторяют некоторые из тех вещей, которые я объяснил здесь. Я не перечитал их, чтобы избежать избыточности, извините.

17

Другие решения

В x86-64 указатель стека должен быть выровнен до 8 байтов.

Это:

subq $12, %rsp;      // make room

должно быть:

subq $16, %rsp;      // make room
3

По вопросам рекламы [email protected]