Я хочу иметь возможность использовать регистр указателя базы (%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.
Смотрите в нижней части этого ответа коллекцию ссылок на другие 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
(указатель).
Когда ваш встроенный ассм закончен,
-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
вы обычно делаете это неправильно и должны были использовать вместо этого ограничение.
Сноски:
-masm=intel
(в этом случае ваш код будет только работать с этой опцией), или используя диалектные альтернативы так что работает с компилятором в Intel или AT&T asm выходной синтаксис. Но это не меняет директив, и Intel-синтаксис GAS недостаточно документирован. (Это похоже на MASM, а не NASM.) Я действительно не рекомендую это, если вы действительно не ненавидите AT&T синтаксис.r
ограничения для указателей / индексов и использования выбранного вами режима адресации по сравнению с использованием m
ограничения, позволяющие gcc выбирать между инкрементными указателями и индексными массивами.%q0
получить %rax
против %w0
получить %ax
, С помощью %g[scalar]
получить %zmm0
вместо %xmm0
,"cc"
клоббер (коды условий, иначе флаги); это неявно. (GCC6 вводит синтаксис использования флаговых условий в качестве операндов ввода / вывода. До этого вы должны setcc
регистр, в который gcc будет выдавать код test
, что явно хуже.)32b/32b => 32b
деление и остаток, что компилятор уже может сделать с одним div
, (Код в вопросе является примером того, как не использовать встроенный asm: множество инструкций по настройке и сохранению / восстановлению, которые следует оставить компилятору, написав соответствующие ограничения in / out.)64b/32b=>32bit
деление. Конструкция и синтаксис MSVC требуют кругового обхода памяти для входов и выходов, что делает его ужасным для коротких функций. Это также «никогда не очень надежно» согласно комментарию Росса Риджа к этому ответу.Некоторые из них повторяют некоторые из тех вещей, которые я объяснил здесь. Я не перечитал их, чтобы избежать избыточности, извините.
В x86-64 указатель стека должен быть выровнен до 8 байтов.
Это:
subq $12, %rsp; // make room
должно быть:
subq $16, %rsp; // make room