Немного не по теме, но я хотел бы немного рассказать о встроенной сборке 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);
Это говорит компилятору, что сборка:
- есть один аргумент в регистре,
"+r"(...)
, который должен быть инициализирован перед оператором сборки, модифицирован оператором сборки и связан с ним переменной bar
.
- есть второй аргумент в регистре,
"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:
- оператор имеет выходной операнд, переменную
bar
, которая после того, как оператор будет найден в регистре, "=r"(...)
- в операторе есть входной операнд, переменная
foo
, которая должна быть помещена в регистр, "r"(...)
- операнд-ноль также является входным операндом и должен быть инициализирован с помощью
bar
Или, опять же, альтернатива:
asm("add %1, %0" : "+r"(bar) : "g"(foo));
что говорит gcc:
- бла (зевок - такой же, как и раньше,
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 <printf@plt>
[ ... ]
, потому что теперь GCC
фактически выяснил foo
- это константа времени компиляции и просто вставил значение в add
инструкция ! Разве это не аккуратно?
Правда, это сложно и требует привыкания. Преимущество состоит в том, что позволяет компилятору выбирать , который регистрирует для использования, для каких операндов позволяет оптимизировать код в целом; если, например, встроенный оператор сборки используется в макросе и / или функции static inline
, компилятор может, в зависимости от контекста вызова, выбирать разные регистры в разных экземплярах кода. Или, если определенное значение является вычисляемым / постоянным во время компиляции в одном месте, но не в другом, компилятор может адаптировать для него созданную сборку.
Думайте об ограничениях встроенной сборки GCC как о «прототипах расширенных функций» - они сообщают компилятору, какие типы и местоположения для аргументов / возвращаемых значений, плюс еще немного. Если вы не укажете эти ограничения, ваша встроенная сборка создаст аналог функций, которые работают только с глобальными переменными / состоянием - которые, как мы, вероятно, все согласны, редко когда-либо делают именно то, что вы хотели.