Почему эта встроенная сборка не работает с отдельным оператором 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 как о "прототипах расширенных функций" - они сообщают компилятору, какие типы и местоположения для аргументов/возвращаемых значений, плюс немного больше. Если вы не укажете эти ограничения, ваша встроенная сборка создает аналог функций, которые работают только с глобальными переменными/состоянием - что, как мы, вероятно, все согласны, редко делает именно то, что вы намеревались.