Использование регистра базового указателя в 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.
2 ответа
Смотрите в нижней части этого ответа коллекцию ссылок на другие вопросы и ответы inline-asm.
Что вы надеетесь научиться делать с помощью встроенного ассема? Если вы хотите изучить inline asm, научитесь использовать его для создания эффективного кода, а не таких ужасных вещей, как этот. Если вы хотите написать пролог функции и нажать / щелкнуть, чтобы сохранить / восстановить регистры, вы должны написать целые функции в asm. (Тогда вы можете легко использовать nasm или yasm вместо менее предпочтительного синтаксиса AT&T с директивами ассемблера GNU 1.)
Встроенный asm GNU сложен в использовании, но позволяет вам смешивать пользовательские фрагменты asm в C и C++, позволяя компилятору обрабатывать распределение регистров и любое сохранение / восстановление в случае необходимости. Иногда компилятор сможет избежать сохранения и восстановления, предоставив вам регистр, который может быть закрыт. Без volatile
, он может даже выводить операторы asm из циклов, когда ввод будет одинаковым. (т.е. если вы не используете volatile
предполагается, что выходы являются "чистой" функцией входов.)
Если вы просто пытаетесь изучать asm, GNU inline asm - ужасный выбор. Вы должны полностью понять почти все, что происходит с ассемблером, и понять, что должен знать компилятор, чтобы написать правильные ограничения ввода / вывода и получить все правильно. Ошибки приведут к разбиванию вещей и трудно отлаживаемым поломкам. Вызов функции ABI намного проще и проще отслеживать границы между вашим кодом и кодом компилятора.
Вы скомпилированы с -O0
, поэтому код GCC выливает параметр функции из %rdi
к месту в стеке. (Это может произойти в нетривиальной функции даже при -O3
). Поскольку целевой ABI является ABI SysV x86-64, он использует "красную зону" (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
,
Если это выглядит слишком сложно, не используйте встроенный asm. Либо приведите компилятор к требуемому asm с C, который подобен оптимальному asm, либо напишите целую функцию в asm.
При использовании встроенного asm, сохраняйте его как можно меньшим: в идеале это всего лишь одна или две инструкции, которые gcc не отправляет самостоятельно, с ограничениями ввода / вывода, указывающими, как вводить / выводить данные из оператора asm. Это то, для чего он предназначен.
Основное правило: если ваш встроенный ассемблер GNU C начинается или заканчивается mov
вы обычно делаете это неправильно и должны были использовать вместо этого ограничение.
Сноски:
- Вы можете использовать Intel-синтаксис GAS в inline-asm, создав
-masm=intel
(в этом случае ваш код будет работать только с этой опцией) или с использованием альтернатив диалекта, чтобы он работал с компилятором в синтаксисе вывода Intel или AT&T asm. Но это не меняет директив, и Intel-синтаксис GAS недостаточно документирован. (Это похоже на MASM, а не NASM.) Я действительно не рекомендую его, если вы действительно не ненавидите синтаксис AT&T.
Встроенные ссылки asm:
- x86 вики (Тег вики также ссылается на этот вопрос, для этой коллекции ссылок)
- Руководство. Прочитай это. Обратите внимание, что встроенный asm был разработан для переноса отдельных инструкций, которые компилятор обычно не генерирует. Вот почему сформулировано что-то вроде "инструкция", а не "блок кода".
- Учебник
- Зацикливание массивов с помощью встроенной сборки
r
ограничения для указателей / индексов и использования выбранного вами режима адресации по сравнению с использованиемm
ограничения, позволяющие gcc выбирать между инкрементными указателями и индексными массивами. - В GNU C inline asm, каковы модификаторы для xmm/ymm/zmm для одного операнда?, С помощью
%q0
получить%rax
против%w0
получить%ax
, С помощью%g[scalar]
получить%zmm0
вместо%xmm0
, - Эффективное 128-битное сложение с использованием флага переноса Ответ Стивена Кэнона объясняет случай, когда для операнда чтения + записи требуется объявление раннего клоббера. Также обратите внимание, что встроенный asm x86/x86-64 не должен объявлять
"cc"
клоббер (коды условий, иначе флаги); это неявно. (gcc6 вводит синтаксис для использования флаговых условий в качестве операндов ввода / вывода. Перед этим вы должныsetcc
регистр, в который gcc будет выдавать кодtest
, что явно хуже.) - Вопросы о производительности различных реализаций strlen: мой ответ на вопрос с каким-то плохо используемым встроенным asm, с ответом, похожим на этот.
- llvm сообщает: неподдерживаемый встроенный asm: ввод с типом 'void *', совпадающий с выводом с типом 'int': использование операндов смещаемой памяти (в x86 все действующие адреса являются смещаемыми: вы всегда можете добавить смещение).
- Когда не использовать встроенный asm, с примером
32b/32b => 32b
деление и остаток, что компилятор уже может сделать с однимdiv
, (Код в вопросе является примером того, как не использовать встроенный asm: множество инструкций по настройке и сохранению / восстановлению, которые следует оставить компилятору, написав соответствующие ограничения in/out.) - Встроенный asm MSVC против GNU C Встроенный asm для переноса одной инструкции с правильным примером встроенного asm для
64b/32b=>32bit
деление Конструкция и синтаксис MSVC требуют кругового обхода памяти для входов и выходов, что делает его ужасным для коротких функций. Это также "никогда не очень надежно" согласно комментарию Росса Риджа к этому ответу. - Использование x87 с плавающей точкой и коммутативные операнды. Не очень хороший пример, потому что я не нашел способ заставить gcc выдавать идеальный код.
Некоторые из них повторяют некоторые из тех вещей, которые я объяснил здесь. Я не перечитал их, чтобы избежать избыточности, извините.
В x86-64 указатель стека должен быть выровнен до 8 байтов.
Это:
subq $12, %rsp; // make room
должно быть:
subq $16, %rsp; // make room