Почему эта встроенная сборка не работает с отдельным оператором asm volatile для каждой инструкции?
Для следующего кода:
long buf[64];
register long rrax asm ("rax");
register long rrbx asm ("rbx");
register long rrsi asm ("rsi");
rrax = 0x34;
rrbx = 0x39;
__asm__ __volatile__ ("movq $buf,%rsi");
__asm__ __volatile__ ("movq %rax, 0(%rsi);");
__asm__ __volatile__ ("movq %rbx, 8(%rsi);");
printf( "buf[0] = %lx, buf[1] = %lx!\n", buf[0], buf[1] );
Я получаю следующий вывод:
buf[0] = 0, buf[1] = 346161cbc0!
пока он должен был быть:
buf[0] = 34, buf[1] = 39!
Любые идеи, почему он не работает должным образом и как его решить?
Ответы
Ответ 1
Вы clobber-память, но не сообщаете GCC об этом, поэтому GCC может кэшировать значения в buf
для вызовов сборки. Если вы хотите использовать входы и выходы, расскажите GCC обо всем.
__asm__ (
"movq %1, 0(%0)\n\t"
"movq %2, 8(%0)"
: /* Outputs (none) */
: "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */
: "memory"); /* Clobbered */
Вы также обычно хотите, чтобы GCC обрабатывал большую часть mov
, выбор регистров и т.д. - даже если вы явно ограничиваете регистры (rrax is stil %rax
), пусть информация проходит через GCC или вы получите неожиданный результаты.
__volatile__
неверно.
Причина __volatile__
существует, поэтому вы можете гарантировать, что компилятор размещает ваш код точно там, где он есть..., что является абсолютно ненужной гарантией для этого кода. Это необходимо для реализации расширенных функций, таких как барьеры памяти, но почти полностью бесполезных, если вы только изменяете память и регистры.
GCC уже знает, что он не может переместить эту сборку после printf
, потому что вызов printf
обращается к buf
, а buf
может быть сбит сборкой. GCC уже знает, что он не может перемещать сборку до rrax=0x39;
, потому что rax
является входом в код сборки. Так что же вы __volatile__
получаете? Ничего.
Если ваш код не работает без __volatile__
, тогда в коде есть ошибка, которая должна быть исправлена вместо добавления __volatile__
и надеется, что все будет лучше. Ключевое слово __volatile__
не является магии и не должно рассматриваться как таковое.
Альтернативное исправление:
Является ли __volatile__
необходимым для вашего исходного кода? Нет. Просто пометьте входы и значения clobber правильно.
/* The "S" constraint means %rsi, "b" means %rbx, and "a" means %rax
The inputs and clobbered values are specified. There is no output
so that section is blank. */
rsi = (long) buf;
__asm__ ("movq %%rax, 0(%%rsi)" : : "a"(rrax), "S"(rssi) : "memory");
__asm__ ("movq %%rbx, 0(%%rsi)" : : "b"(rrbx), "S"(rrsi) : "memory");
Почему __volatile__
не помогает вам здесь:
rrax = 0x34; /* Dead code */
GCC вполне может полностью удалить указанную выше строку, так как код в вышеприведенном вопросе утверждает, что он никогда не использует rrax
.
Более четкий пример
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)");
}
Разборка более или менее, как вы ожидаете, в -O0
,
movl $5, %rax
movq %rax, (global)
Но с оптимизацией вы можете быть довольно неряшливы в сборке. Попробуйте -O2
:
movq %rax, (global)
Упс! Куда пошел rax = 5;
? Это мертвый код, так как %rax
никогда не используется в функции - по крайней мере, насколько GCC знает. GCC не заглядывает внутрь сборки. Что происходит, когда мы удаляем __volatile__
?
; empty
Ну, вы можете подумать, что __volatile__
делает вам сервис, не позволяя GCC отказаться от вашей драгоценной сборки, но это просто маскирует тот факт, что GCC думает, что ваша сборка ничего не делает. GCC считает, что ваша сборка не принимает никаких входов, не производит никаких выходов и не сжимает память. Вам лучше разобраться:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)" : : : "memory");
}
Теперь мы получаем следующий результат:
movq %rax, (global)
Лучше. Но если вы сообщите GCC о входах, то убедитесь, что %rax
правильно инициализирован первым:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory");
}
Выход с оптимизацией:
movl $5, %eax
movq %rax, (global)
Правильно! И нам даже не нужно использовать __volatile__
.
Почему существует __volatile__
?
Первичным правильным использованием для __volatile__
является то, что ваш код сборки делает что-то еще, кроме ввода, вывода или сбивания памяти. Возможно, это связано со специальными регистрами, о которых GCC не знает, или влияет на IO. Вы многого видите в ядре Linux, но очень часто пользовались им в пространстве пользователя.
Ключевое слово __volatile__
очень заманчиво, потому что программистам C часто нравится думать, что мы уже почти программируем на ассемблере. Не были. Компиляторы C делают много анализа потока данных, поэтому вам нужно объяснить поток данных для компилятора для вашего кода сборки. Таким образом, компилятор может безопасно манипулировать вашим блоком сборки так же, как он манипулирует сборкой, которую он создает.
Если вы часто используете __volatile__
, в качестве альтернативы вы можете написать целую функцию или модуль в файле сборки.
Ответ 2
Компилятор использует регистры, и он может писать над значениями, которые вы вложили в них.
В этом случае компилятор, вероятно, использует регистр rbx
после назначения rrbx
и перед секцией встроенной сборки.
В общем, вы не должны ожидать, чтобы регистры сохраняли свои значения после и между последовательностями последовательных сборок.
Ответ 3
Немного не по теме, но я хотел бы немного следить за сборкой gcc inline.
Необходимость (не) для __volatile__
исходит из того, что GCC оптимизирует встроенную сборку. GCC проверяет инструкцию сборки для побочных эффектов/предварительных условий, и если она считает, что они не существуют, она может выбрать перемещение инструкции сборки или даже решить удалить ее. Все __volatile__
заключается в том, чтобы сообщить компилятору "прекратить заботу и поместить это прямо там".
Это обычно не то, что вы действительно хотите.
Здесь возникает необходимость в ограничениях. Имя перегружено и фактически используется для разных вещей в встроенной сборке GCC:
- ограничения определяют операнды ввода/вывода, используемые в блоке
asm()
Ограничения - определяют "список clobber", в котором указано, на что "состояние" (регистры, коды условий, память) влияет на
asm()
.
Ограничения - определяют классы операндов (регистры, адреса, смещения, константы,...)
- объявляет ассоциации/привязки между объектами ассемблера и переменными/выражениями C/С++
Во многих случаях разработчики злоупотребляют __volatile__
, потому что они замечают, что их код либо перемещается, либо даже исчезает без него. Если это произойдет, это скорее скорее признак того, что разработчик попытался не сообщать GCC о побочных эффектах/предпосылках сборки. Например, этот багги-код:
register int foo __asm__("rax") = 1234;
register int bar __adm__("rbx") = 4321;
asm("add %rax, %rbx");
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
У него появилось несколько ошибок:
- для одного, он только компилируется из-за ошибки gcc (!). Как правило, для записи имен регистров в встроенной сборке необходимы двойные
%%
, но в приведенном выше примере, если вы действительно указываете их, вы получаете ошибку компилятора/ассемблера, /tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'
.
- во-вторых, он не сообщает компилятору, когда и где вам нужно/использовать переменные. Вместо этого он предполагает, что компилятор отличает
asm()
буквально. Это может быть справедливо для Microsoft Visual С++, но это не относится к gcc.
Если вы скомпилируете его без оптимизации, он создает:
0000000000400524 <main>:
[ ... ]
400534: b8 d2 04 00 00 mov $0x4d2,%eax
400539: bb e1 10 00 00 mov $0x10e1,%ebx
40053e: 48 01 c3 add %rax,%rbx
400541: 48 89 da mov %rbx,%rdx
400544: b8 5c 06 40 00 mov $0x40065c,%eax
400549: 48 89 d6 mov %rdx,%rsi
40054c: 48 89 c7 mov %rax,%rdi
40054f: b8 00 00 00 00 mov $0x0,%eax
400554: e8 d7 fe ff ff callq 400430 <[email protected]>
[...]
Вы можете найти инструкцию add
и инициализировать два регистра, и она напечатает ожидаемое. Если, с другой стороны, вы оптимизируете оптимизацию, происходит что-то еще: 0000000000400530 <main>:
400530: 48 83 ec 08 sub $0x8,%rsp
400534: 48 01 c3 add %rax,%rbx
400537: be e1 10 00 00 mov $0x10e1,%esi
40053c: bf 3c 06 40 00 mov $0x40063c,%edi
400541: 31 c0 xor %eax,%eax
400543: e8 e8 fe ff ff callq 400430 <[email protected]>
[ ... ]
Инициализация обоих "используемых" регистров уже отсутствует. Компилятор отбросил их, потому что ничего, что он мог видеть, не использовал их, и, хотя он сохранил инструкцию сборки, он поставил его перед любым использованием этих двух переменных. Он там, но ничего не делает (к счастью, на самом деле... если rax
/rbx
был в использовании, кто может сказать, что произошло...).
И причина в том, что вы на самом деле не сказали GCC, что сборка использует эти регистры/эти значения операнда. Это не имеет ничего общего с volatile
, но все с факт, что вы используете выражение asm()
без ограничений.
Способ сделать это правильно - это ограничения, т.е. вы используете:
int foo = 1234;
int bar = 4321;
asm("add %1, %0" : "+r"(bar) : "r"(foo));
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
Это сообщает компилятору, что сборка:
- имеет один аргумент в регистре
"+r"(...)
, который должен быть инициализирован перед оператором сборки и модифицирован оператором сборки, и связать с ним переменную bar
.
- имеет второй аргумент в регистре
"r"(...)
, который должен быть инициализирован перед оператором сборки и обрабатывается как readonly/not modified. Здесь сопоставьте foo
с этим.
Обратите внимание, что не задано назначение регистров - компилятор выбирает это в зависимости от переменных/состояния компиляции. (Оптимизированный) вывод выше:
0000000000400530 <main>:
400530: 48 83 ec 08 sub $0x8,%rsp
400534: b8 d2 04 00 00 mov $0x4d2,%eax
400539: be e1 10 00 00 mov $0x10e1,%esi
40053e: bf 4c 06 40 00 mov $0x40064c,%edi
400543: 01 c6 add %eax,%esi
400545: 31 c0 xor %eax,%eax
400547: e8 e4 fe ff ff callq 400430 <[email protected]>
[ ... ]
Встроенные сборочные ограничения GCC почти всегда необходимы в той или иной форме, но могут быть несколько возможных способов описания одних и тех же требований к компилятору; вместо вышесказанного вы также можете написать:
asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));
Это говорит gcc:
- оператор имеет выходной операнд, переменную
bar
, которая после того, как оператор будет найден в регистре, "=r"(...)
- оператор имеет входной операнд, переменную
foo
, которая должна быть помещена в регистр, "r"(...)
- Опорный ноль также является входным операндом и должен быть инициализирован с помощью
bar
Или снова альтернатива:
asm("add %1, %0" : "+r"(bar) : "g"(foo));
который сообщает gcc:
- bla (yawn - то же, что и раньше,
bar
оба входа/выхода)
- оператор имеет входной операнд, переменную
foo
, которую оператор не заботится о том, находится ли он в регистре, в памяти или константе времени компиляции (что ограничение "g"(...)
)
Результат отличается от предыдущего:
0000000000400530 <main>:
400530: 48 83 ec 08 sub $0x8,%rsp
400534: bf 4c 06 40 00 mov $0x40064c,%edi
400539: 31 c0 xor %eax,%eax
40053b: be e1 10 00 00 mov $0x10e1,%esi
400540: 81 c6 d2 04 00 00 add $0x4d2,%esi
400546: e8 e5 fe ff ff callq 400430 <[email protected]>
[ ... ]
, потому что теперь GCC фактически понял, что foo
является константой времени компиляции и просто встраивает значение в инструкцию add
! Разве это не так?
По общему признанию, это сложно и требует привыкания. Преимущество заключается в том, что позволить компилятору выбрать, какие регистры использовать для того, какие операнды позволяют оптимизировать код в целом; если, например, оператор встроенной сборки используется в функции макроса и/или static inline
, компилятор может, в зависимости от контекста вызова, выбирать разные регистры при разных экземплярах кода. Или, если определенное значение является временем компиляции/константой в одном месте, но не в другом, компилятор может настроить созданную для него сборку.
Подумайте о встроенных ограничениях GCC как о "прототипах расширенных функций" - они сообщают компилятору, какие типы и местоположения для аргументов/возвращаемых значений, плюс немного больше. Если вы не укажете эти ограничения, ваша встроенная сборка создает аналог функций, которые работают только с глобальными переменными/состоянием - что, как мы, вероятно, все согласны, редко делает именно то, что вы намеревались.