Почему эта встроенная сборка не работает с отдельным оператором asm volatile для каждой инструкции? - PullRequest
10 голосов
/ 17 января 2012

Для следующего кода:

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!

Любые идеипочему он не работает должным образом и как его решить?

Ответы [ 3 ]

22 голосов
/ 17 января 2012

Вы обрезаете память, но не говорите об этом 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 просто %rax), пропускаете информацию через GCC, или вы получите неожиданное результаты.

__volatile__ неверно.

Причина, по которой __volatile__ существует, заключается в том, что вы можете гарантировать, что компилятор поместит ваш код именно там, где он есть ... что является абсолютно ненужной гарантией для этого кода. Это необходимо для реализации расширенных функций, таких как барьеры памяти, но почти полностью бесполезен, если вы только изменяете память и регистры.

GCC уже знает, что не может переместить эту сборку после printf, поскольку вызов printf обращается к buf, и сборка buf может быть засорена сборкой. GCC уже знает, что не может переместить сборку до rrax=0x39;, поскольку rax является вводом кода сборки. Так что же __volatile__ дает вам? Ничего.

Если ваш код не работает без __volatile__, тогда в коде есть ошибка, которая должна быть фиксированной вместо простого добавления __volatile__ и надежды, что все станет лучше. Ключевое слово __volatile__ не волшебно и не должно рассматриваться как таковое.

Альтернативное исправление:

Нужно ли __volatile__ для вашего исходного кода? Нет. Просто отметьте входы и значения затвора правильно.

/* 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, часто думаем, что уже почти программируем на ассемблере. Не были. Компиляторы Си много анализируют потоки данных, поэтому вам нужно объяснить поток данных компилятору для вашего кода сборки. Таким образом, компилятор может безопасно манипулировать вашим фрагментом сборки точно так же, как он обрабатывает сборку, которую он генерирует.

Если вы часто используете __volatile__, в качестве альтернативы вы можете написать целую функцию или модуль в файле сборки.

4 голосов
/ 17 января 2012

Компилятор использует регистры и может записывать значения, которые вы в них ввели.

В этом случае компилятор, вероятно, использует регистр rbx после присваивания rrbx и перед секцией встроенной сборки.

Как правило, не следует ожидать, что регистры сохранят свои значения после и между последовательностями кода встроенной сборки.

2 голосов
/ 18 января 2012

Немного не по теме, но я хотел бы немного рассказать о встроенной сборке gcc.

(Не) потребность в __volatile__ проистекает из того факта, что GCC оптимизирует встроенную сборку. GCC проверяет инструкцию сборки на наличие побочных эффектов / предпосылок, и если он обнаруживает, что их не существует, он может переместить инструкцию сборки или даже решить удалить ее. Все, что __volatile__ делает, это говорит компилятору "прекратить заботиться и поместить это прямо здесь".

Что обычно не то, что вы действительно хотите.

Именно здесь возникает необходимость в ограничениях . Имя перегружено и фактически используется для различных вещей во встроенной сборке GCC:

  • ограничения определяют операнды ввода / вывода, используемые в блоке asm()
  • определяют «список дубликатов», в котором подробно указано, на какое «состояние» (регистры, коды условий, память) влияет asm().
  • ограничения указывают классы операндов (регистры, адреса, смещения, константы, ...)
  • ограничения объявляют ассоциации / привязки между сущностями ассемблера и переменными / выражениями C / 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 C ++, но это не так для 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 <printf@plt>
[...]
Вы можете найти инструкцию 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 <printf@plt>
[ ... ]
Ваши инициализации обоих «используемых» регистров больше не существуют. Компилятор отбросил их, потому что ничто из того, что он не видел, использовал их, и, в то время как он сохранил инструкцию по сборке, он поставил перед любым использованием двух переменных. Он есть, но ничего не делает (к счастью, на самом деле ... если бы 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);

Это говорит компилятору, что сборка:

  1. есть один аргумент в регистре, "+r"(...), который должен быть инициализирован перед оператором сборки, модифицирован оператором сборки и связан с ним переменной bar.
  2. есть второй аргумент в регистре, "r"(...), который должен быть инициализирован перед оператором сборки и обрабатываться как оператор только для чтения / без изменения оператором. Здесь ассоциируйте 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 <printf@plt>
[ ... ]
Ограничения встроенной сборки GCC почти всегда необходимы в той или иной форме, но может быть несколько возможных способов описания одних и тех же требований к компилятору; вместо вышесказанного вы также можете написать:
asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));

Это говорит gcc:

  1. оператор имеет выходной операнд, переменную bar, которая после того, как оператор будет найден в регистре, "=r"(...)
  2. в операторе есть входной операнд, переменная foo, которая должна быть помещена в регистр, "r"(...)
  3. операнд-ноль также является входным операндом и должен быть инициализирован с помощью bar

Или, опять же, альтернатива:

asm("add %1, %0" : "+r"(bar) : "g"(foo));

что говорит gcc:

  1. бла (зевок - такой же, как и раньше, bar оба входа / выхода)
  2. в операторе есть входной операнд, переменная 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 <printf@plt>
[ ... ]
, потому что теперь GCC фактически выяснил foo - это константа времени компиляции и просто вставил значение в add инструкция ! Разве это не аккуратно?

Правда, это сложно и требует привыкания. Преимущество состоит в том, что позволяет компилятору выбирать , который регистрирует для использования, для каких операндов позволяет оптимизировать код в целом; если, например, встроенный оператор сборки используется в макросе и / или функции static inline, компилятор может, в зависимости от контекста вызова, выбирать разные регистры в разных экземплярах кода. Или, если определенное значение является вычисляемым / постоянным во время компиляции в одном месте, но не в другом, компилятор может адаптировать для него созданную сборку.

Думайте об ограничениях встроенной сборки GCC как о «прототипах расширенных функций» - они сообщают компилятору, какие типы и местоположения для аргументов / возвращаемых значений, плюс еще немного. Если вы не укажете эти ограничения, ваша встроенная сборка создаст аналог функций, которые работают только с глобальными переменными / состоянием - которые, как мы, вероятно, все согласны, редко когда-либо делают именно то, что вы хотели.

...