Ответ 1
Смотрите в нижней части этого ответа коллекцию ссылок на другие вопросы и ответы inline-asm.
Ваш код не работает, потому что вы наступаете на красную зону ниже RSP (с push
), где GCC сохранял значение.
Что вы надеетесь научиться делать с помощью встроенного ассема? Если вы хотите изучить встроенный 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, он использует "красную зону" (128 байт ниже %rsp
что даже асинхронным обработчикам сигналов не разрешено блокировать) вместо того, чтобы тратить инструкцию, уменьшающую указатель стека на резервное пространство,
Он хранит функцию указателя 8B arg в -8(rsp_at_function_entry)
. Затем ваш встроенный asm выдвигает %rbp
, который уменьшает значение% rsp на 8, а затем записывает туда, что приводит к засорению младшего 32b символа &x
(указатель).
Когда ваш встроенный ассм закончен,
- gcc перезагружает
-8(%rbp)
(который был перезаписан%rbp
) и использует его в качестве адреса для хранилища 4B. -
Foo
возвращается кmain
с%rbp = (upper32)|5
(значение orig с минимальным 32, установленным в5
). -
leave
main
трассы:%rsp = (upper32)|5
-
main
запускаетret
с%rsp = (upper32)|5
, считывая адрес возврата с виртуального адреса(void*)(upper32|5)
, который из вашего комментария равен0x7fff0000000d
.
Я не проверял с отладчиком; один из этих шагов может быть слегка отключен, но проблема определенно заключается в том, что вы затираете красную зону, что приводит к тому, что код gcc разрушает стек.
Даже добавление "памяти" clobber не дает gcc избежать использования красной зоны, поэтому похоже, что выделение собственной памяти стека из встроенного asm - просто плохая идея. (Подстановка памяти означает, что вы, возможно, записали некоторую память, в которую вам разрешено писать, например, глобальную переменную или что-то, на которое указывает глобальная переменная, а не то, что вы могли перезаписать то, что не должны были делать.)
Если вы хотите использовать пустое пространство из встроенного asm, вам, вероятно, следует объявить массив как локальную переменную и использовать его как операнд только для вывода (который вы никогда не читаете).
AFAIK, нет синтаксиса для объявления, что вы изменяете красную зону, поэтому ваши единственные варианты:
- используйте выходной операнд
"=m"
(возможно, массив) для пустого пространства; компилятор, вероятно, заполнит этот операнд режимом адресации относительно RBP или RSP. Вы можете индексировать его с помощью констант, таких как4 + %[tmp]
или чего-либо еще. Вы можете получить предупреждение ассемблера от4 + (%rsp)
но не ошибку. - пропустите красную зону с
add $-128, %rsp
/sub $-128, %rsp
вокруг вашего кода. (Необходим, если вы хотите использовать неизвестное количество дополнительного стекового пространства, например, вставить цикл или сделать вызов функции. Еще одна причина, чтобы разыменовать указатель функции в чистом C, а не в встроенном asm.) - компилировать с
-mno-red-zone
(я не думаю, что вы можете включить это для каждой функции, только для каждого файла) - Во-первых, не используйте пустое место. Сообщите компилятору, какие регистры вы записываете, и дайте ему сохранить их.
Вот что вы должны были сделать:
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;
}
Обратите внимание на push/pop %rbp
в коде вне #APP
/#NO_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 вики. (Тег вики также ссылается на этот вопрос, для этой коллекции ссылок)
- Тег inline-assembly вики
- Руководство. Прочитай это. Обратите внимание, что встроенный asm был разработан для переноса отдельных инструкций, которые компилятор обычно не генерирует. Вот почему он сформулировал слова "инструкция", а не "блок кода".
- Учебник
-
Зацикливание массивов с помощью встроенной сборки. Использование ограничений
r
для указателей/индексов и использование выбранного вами режима адресации, а также использование ограниченийm
чтобы позволить gcc выбирать между инкрементными указателями и индексными массивами. -
Как я могу указать, что память, на которую указывает * встроенный аргумент ASM, может использоваться? (Входные данные указателя в регистрах не подразумевают, что указанная память читается и/или записывается, поэтому она может не синхронизироваться, если вы не сообщите компилятору).
-
В GNU C inline asm, каковы модификаторы для xmm/ymm/zmm для одного операнда? , Использование
%q0
для получения%rax
против%w0
для получения%ax
. Используя%g[scalar]
чтобы получить%zmm0
вместо%xmm0
. - Эффективное 128-битное сложение с использованием флага переноса Ответ Стивена Кэнона объясняет случай, когда для операнда чтения + записи требуется объявление раннего клоббера. Также обратите внимание, что встроенный asm x86/x86-64 не должен объявлять
"cc"
clobber (коды условий, или флаги); это неявное. (gcc6 вводит синтаксис для использования флаговых условий в качестве операндов ввода/вывода. Перед этим вы должныsetcc
регистр, который gcc будетsetcc
для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 выдавать идеальный код.
Некоторые из них повторяют некоторые из тех вещей, которые я объяснил здесь. Я не перечитывал их, чтобы избежать избыточности, извините.