Войти
ПрограммированиеСтатьиОбщее

Ассемблерные вставки в GCC (3 стр)

Автор:

Синтаксис и семантика


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

Подробное изложение можно найти в официальной документации: https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html .

Вместо ключевого слова asm может быть также использовано __asm__ (двойные подчёркивания). Некоторые источники рекомендуют использовать именно __asm__ (asm является (для C) расширением GNU, и в режиме -ansi, и/или строгого соответствия стандартам (опции вида -std= ) может вызывать предупреждения/ошибки компиляции), в частности Linux kernel использует именно эту запись.

Существуют 2 формы ассемблерных вставок: базовая и расширенная.
Все примеры выше по тексту использовали базовую форму.

По умолчанию, компилятор воспринимает ассемблерную вставку, как производящую вычисления, и оптимизатор исходит из этого предположения (в частности, может считать ассемблерную вставку чистой функцией от входных операндов и, например, перенести или вынести наружу цикла). Это может привести к неправильной работе, если ассемблерная вставка содержит побочные эффекты. Для того, чтобы отключить соответствующие оптимизации, можно вслед за asm указать ключевое слово volatile (или __volatile__). Базовая форма всегда является volatile (но явное указание volatile всё-равно допустимо).
Цитата из документации ( https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Volatile ), описывающая (довольно уклончиво) семантику volatile:

GCC's optimizers sometimes discard asm statements if they determine there is no need for the output variables. Also, the optimizers may move code out of loops if they believe that the code will always return the same result (i.e. none of its input values change between calls). Using the volatile qualifier disables these optimizations. asm statements that have no output operands, including asm goto statements, are implicitly volatile.

Раздел содержит несколько примеров и рекомендуется к изучению.

Рекомендуется по возможности избегать volatile ассемблерных вставок (в частности - базовой формы ассемблера), и не препятствовать нормальной работе оптимизатора.

Базовая форма


Базовая форма имеет вид:

asm [ volatile ] ( AssemblerInstructions )

Как отмечалось выше, ключевое слово volatile для базовой формы не обязательно и смысла команды не меняет.

В базовой форме AssemblerInstructions вставляются в выходной файл на ассемблере как есть, без макроподстановок.

В базовой форме отсутствуют операнды (позволяющие задать связи с переменными C++), что заметно затрудняет надёжное взаимодействие с кодом на C++.
Кроме того (опять же по причине отсутствия операндов) компилятор исходит из предположения, что базовая ассемблерная вставка не изменяет значения ни памяти, ни регистров.

Как отмечалось выше, оптимизатор имеет право выбрасывать/перемещать/дублировать ассемблерные вставки.
В общем случае, согласно документации, volatile не даёт гарантий против этого поведения.
В частности нет гарантий относительного порядка следования команд. Если таковой требуется, рекомендуется использовать одну общую ассемблерную вставку с несколькими инструкциями.

Таким образом базовая форма может быть использована для команд, ценных в основном своими побочными эффектами, вроде

#define DebugBreak() asm("int $3")

но довольно неудобна (и небезопасна) для взаимодействия с кодом на C++.

Однако иногда базовой форме нет альтернативы:
1. Использование ассемблерных вставок на верхнем уровне единицы трансляции (расширенная форма обязана находиться внутри тела функции). Это может быть использовано для установки директив ассемблера.
2. Использование в функции с атрибутом naked (см. https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html#Function-Attributes ).

По возможности рекомендуется использовать расширенную форму.

Расширенная форма


Расширенная форма существует в 2-х видах:
Обычный:

asm [volatile] ( AssemblerTemplate
                      : OutputOperands
                      [ : InputOperands
                      [ : Clobbers ] ])

goto:

asm [volatile] goto ( AssemblerTemplate
                          :
                          : InputOperands
                          : Clobbers
                          : GotoLabels)

goto-версия может выполнять переход на одну из меток (определённых в коде на C/C++), указанных в GotoLabels. Она не содержит OutputOperands, и автоматически является volatile даже без явного указания.
Если есть необходимость изменить переменную внутри goto-вставки, то можно (вследствие отсутствия OutputOperands) воспользоваться записью в память, уведомив об этом компилятор, указав "memory" в clobbers.
Переходы между разными ассемблерными вставками запрещены. Переходы внутри одной ассемблерной вставки не требуют goto-версии, и рассмотрены ниже.

AssemblerTemplate - это строка, которая вставляется в выходной ассемблерный файл. Но, в отличии от базовой формы, компилятор производит над ней макроподстановки.
Макроподстановки используют символ "%". Кроме имён, сопоставленных операндам, доступны:
%% - генерирует одинарный символ % (экранирование символа %)
%= - генерирует уникальное целое число (полезно для создания уникальных идентификаторов)
%{ - генерирует символ {
%| - генерирует символ |
%} - генерирует символ }

Соответственно, инструкция

movl %eax,%ebx

будет записана

"movl %%eax,%%ebx"

(в отличие от базовой формы).

OutputOperands, InputOperands, Clobbers задают операнды ассемблерной вставки.
Суммарное количество операндов ограничено 30.

Операнды

OutputOperands


OutputOperands связывают имя, видимое из ассемблерной вставки, с l-value кода на С++.
Их общий вид:

[ [asmSymbolicName] ] constraint (cvariablename)

asmSymbolicName - имя видимое из ассемблерной вставки (если OutputOperand указан как [foo]"m"(bar), то в ассемблерной вставке вставке имя будет доступно как %[foo]). Эта часть не обязательна, если имя опущено, то операнд доступен по номеру (%0, %1 и т. д.). Компилятор проводит макроподстановку и вставляет в результирующий ассемблерный файл конкретное выражение (известное ему), обозначающее данное l-value. См. пример ниже.
constraint - ограничения на тип операнда (область памяти, регистр, возможность записи/чтения). Подробнее о constraint см. ниже.
cvariablename - выражение на C++, обозначающее l-value, связанное с данным именем (не обязательно имя переменной).

InputOperands


InputOperands связывают имя, видимое из ассемблерной вставки, со значением выражения на С++.
Их общий вид:

[ [asmSymbolicName] ] constraint (cexpression)

asmSymbolicName - имя видимое из ассемблерной вставки (подробности см. выше).
constraint - ограничения на тип операнда.
cexpression - выражение на C++, связанное с данным именем.

Clobbers


Clobbers задают список ресурсов (в основном регистров), которые изменяет ассемблерная вставка.
Регистры задействованные в InputOperands или OutputOperands указывать в Clobbers не требуется.
Для регистров указываются их обычные имена (не их сокращённые обозначения из constraints), причём без (!) префикса %, в виде C-строк.
Дополнительно существуют 2 аргумента:
"cc" - означает, что ассемблерная вставка изменяет значения флагов
"memory" - означает, что ассемблерная вставка производит чтение/запись памяти, кроме указанной в InputOperands/OutputOperands. Ниже приводится дословно соответствующий отрывок из документации ( https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Extended-Asm ):

The "memory" clobber tells the compiler that the assembly code performs memory reads or writes to items other than those listed in the input and output operands (for example, accessing the memory pointed to by one of the input parameters). To ensure memory contains correct values, GCC may need to flush specific register values to memory before executing the asm. Further, the compiler does not assume that any values read from memory before an asm remain unchanged after that asm; it reloads them as needed. Using the "memory" clobber effectively forms a read/write memory barrier for the compiler.

Note that this clobber does not prevent the processor from doing speculative reads past the asm statement. To prevent that, you need processor-specific fence instructions.

Flushing registers to memory has performance implications and may be an issue for time-sensitive code. You can use a trick to avoid this if the size of the memory being accessed is known at compile time. For example, if accessing ten bytes of a string, use a memory input like:

{"m"( ({ struct { char x[10]; } *p = (void *)ptr ; *p; }) )}.

GotoLabels


GotoLabels содержат список имён меток C++ (обычные имена, не заключённые в кавычки).
Доступ к ним из ассемблерной вставки происходит по индексу (отсчитываемому от 0 с начала списка операндов, т. е. часто начальная метка будет иметь индекс, отличный от 0). Имена меток, видимые из ассемблерной вставки имеют вид %lиндекс (малая английская буква L), например %l1, %l5.
Альтернативно вместо индекса возможно указание имени C-метки в квадратных скобках, например %l[label].

Constraints


Constraint представляет собой C-строку, задающую ограничения на связанный операнд.
Неполный список constraints (более полный см. в https://gcc.gnu.org/onlinedocs/gcc/Constraints.html):
ConstraintЗначение
Общие
mАдресуемая область памяти
rРегистр общего назначения
iНепосредственный константный операнд
gРегистр общего назначения, область памяти или непосредственный операнд
XЛюбой операнд
0, 1, 2, ... 9Операнд, совпадающий с операндом с указанным индексом (matching constraint)
x86/x86_64
a, b, c, dРегистры ax, bx, cx, dx и их версии (ah,ecx,rdx)
S, DРегистры si, di и их версии
AПара ax:dx и её версии
fРегистр стека FPU
tВерхний регистр стека FPU (ST(0))
uST(1)
yЛюбой регистр MMX
xЛюбой регистр SSE
YzРегистр xmm0
GВещественная константа x387
CНулевая константа SSE

Примечания:
1. Кроме m существуют также o и V обозначающие offsettable и non-offsettable адреса соответственно.
2. Matching constraint означает, что используется один операнд, но ассемблерная вставка различает несколько связанных с ним имён, например для использования одного и того же операнда в InputOperands и OutputOperands. Цитата из документации:

An operand that matches the specified operand number is allowed. If a digit is used together with letters within the same alternative, the digit should come last.

This number is allowed to be more than a single digit. If multiple digits are encountered consecutively, they are interpreted as a single decimal integer. There is scant chance for ambiguity, since to-date it has never been desirable that ‘10’ be interpreted as matching either operand 1 or operand 0. Should this be desired, one can use multiple alternatives instead.

This is called a matching constraint and what it really means is that the assembler has only a single operand that fills two roles which asm distinguishes. For example, an add instruction uses two input operands and an output operand, but on most CISC machines an add instruction really has only two operands, one of them an input-output operand:
          addl #35,r12

Matching constraints are used in these circumstances. More precisely, the two operands that match must include one input-only operand and one output-only operand. Moreover, the digit must be a smaller number than the number of the operand that uses it in the constraint.

Компилятор пытается, по возможности, удовлетворить constraints (например, поместить значение переменной в регистр перед ассемблерной вставкой, или прочитать её значение из регистра после). Если ему это не удаётся, то он выдаёт сообщение об ошибке.
В constraint можно указать несколько альтернатив, предоставив компилятору выбрать одну по своему усмотрению. Синтаксически это достигается конкатенацией соответствующих constraints (например "rm"); при этом цифры (matching constraints) должны идти в конце (после букв).
Также можно указать несколько альтернативных наборов: для каждого constraint через запятую приводятся значения для каждой из альтернатив (подробнее см. в https://gcc.gnu.org/onlinedocs/gcc/Multi-Alternative.html ).

Constraints могут быть снабжены модификатором:
= - означает, что ассемблерная вставка производит запись в операнд (затирая предыдущее значение). Возможен только для OutputOperands.
+ - означает, что ассемблерная вставка производит и чтение (из операнда) и запись (в него)
& - (так называемый earlyclobber). Обычно компилятор исходит из предположения, что все чтения из InputOperands завершаются до начала записи в OutputOperands, и может, например отвести один и тот же регистр для независимых InputOperand и OutputOperand. Снабжение OutputOperand модификатором & сообщает компилятору, что запись в регистр может произойти раньше, препятствуя возникновению этой ситуации (также это препятствует, например, использованию этого регистра компилятором/ассемблером для хранения/вычисления адреса другого операнда). Цитаты из документации:

Use the ‘&’ constraint modifier (see Modifiers) on all output operands that must not overlap an input. Otherwise, GCC may allocate the output operand in the same register as an unrelated input operand, on the assumption that the assembler code consumes its inputs before producing outputs. This assumption may be false if the assembler code actually consists of more than one instruction.

The same problem can occur if one output parameter (a) allows a register constraint and another output parameter (b) allows a memory constraint. The code generated by GCC to access the memory address in b can contain registers which might be shared by a, and GCC considers those registers to be inputs to the asm. As above, GCC assumes that such input registers are consumed before any outputs are written. This assumption may result in incorrect behavior if the asm writes to a before using b. Combining the ‘&’ modifier with the register constraint on a ensures that modifying a does not affect the address referenced by b. Otherwise, the location of b is undefined if a is modified before using b.

Means (in a particular alternative) that this operand is an earlyclobber operand, which is written before the instruction is finished using the input operands. Therefore, this operand may not lie in a register that is read by the instruction or as part of any memory address.

‘&’ applies only to the alternative in which it is written. In constraints with multiple alternatives, sometimes one alternative requires ‘&’ while others do not. See, for example, the ‘movdf’ insn of the 68000.

A operand which is read by the instruction can be tied to an earlyclobber operand if its only use as an input occurs before the early result is written. Adding alternatives of this form often allows GCC to produce better code when only some of the read operands can be affected by the earlyclobber. See, for example, the ‘mulsi3’ insn of the ARM.

Furthermore, if the earlyclobber operand is also a read/write operand, then that operand is written only after it's used.

‘&’ does not obviate the need to write ‘=’ or ‘+’. As earlyclobber operands are always written, a read-only earlyclobber operand is ill-formed and will be rejected by the compiler.

% - означает, что ассемблерная вставка коммутативна по этому и следующему операндам (подробности см. в https://gcc.gnu.org/onlinedocs/gcc/Modifiers.html )

Модификатор = или + обязателен для OutputOperands.

Страницы: 1 2 3 4 5 Следующая »

#GCC, #inline, #ассемблер

30 апреля 2016 (Обновление: 4 мая 2016)

Комментарии [32]