6.47.2 Расширенный Asm — Инструкции ассемблера с операндами выражений C ¶
С помощью расширенного asm можно читать и записывать переменные C из ассемблера и выполнять переходы из ассемблерного кода к меткам C. Синтаксис расширенного asm использует двоеточия (‘:’) для разграничения параметров операндов после шаблона ассемблера:
asm asm-qualifiers ( AssemblerTemplate
: OutputOperands
[ : InputOperands
[ : Clobbers ] ])
asm asm-qualifiers ( AssemblerTemplate
:
: InputOperands
: Clobbers
: GotoLabels)
где в последней форме asm-qualifiers содержит goto (а в первой форме — нет).
Ключевое слово asm является расширением GNU. При написании кода, который может быть скомпилирован с -ansi и различными параметрами -std, используйте __asm__ вместо asm (см. Альтернативные ключевые слова).
Квалификаторы ¶
volatile-
Типичное использование расширенных
asmоператоров заключается в манипулировании входными значениями для получения выходных значений. Однако вашиasmоператоры также могут иметь побочные эффекты. В этом случае вам может потребоваться использовать квалификаторvolatileдля отключения определенных оптимизаций. См. Volatile. inline-
Если вы используете квалификатор
inline, то для целей встраивания размер оператораasmберется как минимально возможный (см. Размер оператораasm). gotoЭтот квалификатор сообщает компилятору, что оператор
asmможет выполнить переход к одной из меток, перечисленных в GotoLabels. См. GotoLabels.
Параметры ¶
- AssemblerTemplate
-
Это строковая литерал, которая является шаблоном для кода ассемблера. Он представляет собой комбинацию фиксированного текста и токенов, которые ссылаются на входные, выходные и переходы параметры. См. AssemblerTemplate.
- OutputOperands
-
Список переменных C, изменяемых инструкциями в AssemblerTemplate, разделенный запятыми. Разрешен пустой список. См. OutputOperands.
- InputOperands
-
Список выражений C, считываемых инструкциями в AssemblerTemplate, разделенный запятыми. Разрешен пустой список. См. InputOperands.
- Clobbers
-
Список регистров или других значений, изменяемых AssemblerTemplate, помимо тех, которые перечислены как выходные. Разрешен пустой список. См. Clobbers и регистры-временные переменные.
- GotoLabels
-
Когда вы используете форму
gotoоператораasm, этот раздел содержит список всех меток C, в которые код в AssemblerTemplate может выполнить переход. См. GotoLabels.Операторы
asmне могут выполнять переходы в другие операторыasm, только в перечисленные GotoLabels. Оптимизаторы GCC не знают о других переходах; поэтому они не могут учитывать их при принятии решения о том, как оптимизировать.
Общее число входных + выходных + переходов операндов ограничено 30.
Примечания ¶
Оператор asm позволяет вставлять инструкции ассемблера непосредственно в код C. Это может помочь вам максимизировать производительность в коде, чувствительном к времени, или получить доступ к инструкциям ассемблера, которые не легко доступны для программ C.
Обратите внимание, что расширенные операторы asm должны находиться внутри функции. Только базовые операторы asm могут находиться вне функций (см. Basic Asm — Инструкции ассемблера без операндов). Функции, объявленные с атрибутом naked, также требуют базовых операторов asm (см. Объявление атрибутов функций).
Хотя применения оператора asm многообразны, может быть полезно рассматривать оператор asm как последовательность инструкций низкого уровня, которые преобразуют входные параметры в выходные параметры. Так, простой (хотя и не очень полезный) пример для i386, использующий asm, может выглядеть так:
int src = 1;
int dst;
asm ("mov %1, %0\n\t"
"add $1, %0"
: "=r" (dst)
: "r" (src));
printf("%d\n", dst);
Этот код копирует src в dst и добавляет 1 к dst.
- Volatile
- Шаблон ассемблера
- Выходные операнды
- Флаговые выходные операнды
- Входные операнды
- Clobbers и регистры-временные переменные
- Метки переходов
- Модификаторы операндов x86
- Операнды x86 с плавающей точкой
asm
6.47.2.1 Volatile ¶
Оптимизаторы GCC иногда отбрасывают операторы asm, если они определяют, что выходные переменные не нужны. Кроме того, оптимизаторы могут перемещать код из циклов, если считают, что код всегда возвращает одно и то же значение (т.е. ни одно из входных значений не изменяется между вызовами). Квалификатор volatile отключает эти оптимизации. Операторы asm, у которых нет выходных операндов, в том числе операторы asm goto, неявно являются volatile.
Этот код i386 демонстрирует случай, который не использует (или не требует) квалификатор volatile. Если он выполняет проверку утверждений, этот код использует asm для выполнения проверки. В противном случае dwRes не используется каким-либо кодом. В результате оптимизаторы могут отбросить оператор asm, что, в свою очередь, устраняет необходимость в всей процедуре DoCheck. Опуская квалификатор volatile, когда он не нужен, вы позволяете оптимизаторам генерировать наиболее эффективный код.
void DoCheck(uint32_t dwSomeValue)
{
uint32_t dwRes;
// Assumes dwSomeValue is not zero.
asm ("bsfl %1,%0"
: "=r" (dwRes)
: "r" (dwSomeValue)
: "cc");
assert(dwRes > 3);
}
Следующий пример показывает случай, когда оптимизаторы могут распознать, что входной параметр (dwSomeValue) никогда не меняется во время выполнения функции, и, следовательно, могут переместить оператор asm за пределы цикла для генерации более эффективного кода. Опять же, использование квалификатора volatile отключает этот тип оптимизации.
void do_print(uint32_t dwSomeValue)
{
uint32_t dwRes;
for (uint32_t x=0; x < 5; x++)
{
// Assumes dwSomeValue is not zero.
asm ("bsfl %1,%0"
: "=r" (dwRes)
: "r" (dwSomeValue)
: "cc");
printf("%u: %u %u\n", x, dwSomeValue, dwRes);
}
}
Следующий пример демонстрирует случай, когда вам нужно использовать квалификатор volatile. Он использует инструкцию x86 rdtsc, которая считывает счётчик времени процессора. Без квалификатора volatile, оптимизаторы могут предположить, что блок asm всегда возвращает одно и то же значение, и поэтому оптимизировать второй вызов.
uint64_t msr;
asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX.
"shl $32, %%rdx\n\t" // Shift the upper bits left.
"or %%rdx, %0" // 'Or' in the lower bits.
: "=a" (msr)
:
: "rdx");
printf("msr: %llx\n", msr);
// Do other work...
// Reprint the timestamp
asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX.
"shl $32, %%rdx\n\t" // Shift the upper bits left.
"or %%rdx, %0" // 'Or' in the lower bits.
: "=a" (msr)
:
: "rdx");
printf("msr: %llx\n", msr);
Оптимизаторы GCC не обрабатывают этот код так, как не-volatile код в предыдущих примерах. Они не перемещают его из циклов или не пропускают его, исходя из предположения, что результат предыдущего вызова всё ещё действителен.
Обратите внимание, что компилятор может перемещать даже инструкции volatile asm относительно другого кода, включая переходы. Например, на многих целевых платформах существует системный регистр, который управляет режимом округления операций с плавающей точкой. Установка его с помощью оператора volatile asm, как в следующем примере PowerPC, не работает надёжно.
asm volatile("mtfsf 255, %0" : : "f" (fpenv));
sum = x + y;
Компилятор может вернуть добавление назад перед оператором volatile asm. Чтобы заставить это работать как ожидается, добавьте искусственную зависимость к оператору asm путем обращения к переменной в последующем коде, например:
asm volatile ("mtfsf 255,%1" : "=X" (sum) : "f" (fpenv));
sum = x + y;
В определенных обстоятельствах GCC может дублировать (или удалять дубликаты) вашего кода ассемблера при оптимизации. Это может привести к непредвиденным ошибкам дублирования символов во время компиляции, если ваш код ассемблера определяет символы или метки. Использование ‘%=’ (см. AssemblerTemplate) может помочь в решении этой проблемы.
6.47.2.2 Шаблон ассемблера ¶
Шаблон ассемблера — это строка, содержащая инструкции ассемблера. Компилятор заменяет в шаблоне токены, которые ссылаются на входные, выходные данные и метки goto, а затем выводит полученную строку в ассемблер. Строка может содержать любые инструкции, распознаваемые ассемблером, включая директивы. GCC не анализирует сами инструкции ассемблера и не знает, что они означают или даже являются ли они допустимым входом для ассемблера. Однако он считает количество инструкций (см. Размер asm).
Вы можете поместить несколько инструкций ассемблера вместе в одну строку asm, разделённые символами, обычно используемыми в коде ассемблера для системы. Комбинация, которая работает в большинстве случаев, — это перевод строки, плюс табуляция для перемещения в поле инструкции (записанная как ‘\n\t’). Некоторые ассемблеры допускают использование точки с запятой в качестве разделителя строк. Однако обратите внимание, что некоторые диалекты ассемблера используют точку с запятой для начала комментария.
Не ожидайте, что последовательность asm инструкций останется идеально последовательной после компиляции, даже когда вы используете квалификатор volatile. Если некоторые инструкции должны оставаться последовательными в выводе, поместите их в одну многоинструкционную asm инструкцию.
Доступ к данным из C-программ без использования операндов ввода/вывода (например, путём прямого использования глобальных символов из шаблона ассемблера) может не работать как ожидается. Аналогично, вызов функций непосредственно из шаблона ассемблера требует подробного понимания целевого ассемблера и ABI.
Поскольку GCC не анализирует шаблон ассемблера, он не видит какие-либо символы, на которые он ссылается. Это может привести к тому, что GCC отбросит эти символы как неиспользуемые, если они также не указаны в качестве операндов ввода, вывода или goto.
Специальные строки формата ¶
Помимо токенов, описанных операндами ввода, вывода и goto, эти токены имеют особое значение в шаблоне ассемблера:
- ‘%%’
-
Выводит одиночный символ ‘%’ в код ассемблера.
- ‘%=’
-
Выводит число, уникальное для каждого экземпляра инструкции
asmво всей компиляции. Этот вариант полезен при создании локальных меток и их многократном использовании в одном шаблоне, который генерирует несколько инструкций ассемблера. - ‘%{’
- ‘%|’
- ‘%}’
Выводит символы ‘{’, ‘|’, и ‘}’ (соответственно) в код ассемблера. Без экранирования эти символы имеют специальное значение для указания нескольких диалектов ассемблера, как описано ниже.
Несколько диалектов ассемблера в шаблонах ассемблера ¶
На таких целевых платформах, как x86, GCC поддерживает несколько диалектов ассемблера. Опция -masm управляет тем, какой диалект GCC использует по умолчанию для встроенного ассемблера. В документации к опции -masm для конкретной платформы содержится список поддерживаемых диалектов, а также диалект по умолчанию, если опция не указана. Эта информация может быть важна для понимания, так как ассемблерный код, который корректно компилируется с одним диалектом, вероятно, потерпит неудачу при компиляции с другим. См. Опции x86.
Если ваш код должен поддерживать несколько диалектов ассемблера (например, если вы пишете общедоступные заголовки, которые должны поддерживать различные варианты компиляции), используйте конструкции следующего вида:
{ dialect0 | dialect1 | dialect2... }
Эта конструкция выводит dialect0 при компиляции кода с диалектом #0, dialect1 для диалекта #1 и т. д. Если альтернатив в фигурных скобках меньше, чем количество диалектов, поддерживаемых компилятором, конструкция ничего не выводит.
Например, если компилятор x86 поддерживает два диалекта (‘att’, ‘intel’), шаблон ассемблера такого вида:
"bt{l %[Offset],%[Base] | %[Base],%[Offset]}; jc %l2"
эквивалентен одному из
"btl %[Offset],%[Base] ; jc %l2" /* att dialect */ "bt %[Base],%[Offset]; jc %l2" /* intel dialect */
Используя тот же компилятор, этот код:
"xchg{l}\t{%%}ebx, %1"
соответствует либо
"xchgl\t%%ebx, %1" /* att dialect */ "xchg\tebx, %1" /* intel dialect */
Вложенные альтернативы диалектов не поддерживаются.
6.47.2.3 Операнды вывода ¶
Утверждение asm имеет ноль или более операндов вывода, указывающих имена C-переменных, изменяемых кодом ассемблера.
В этом примере i386, old (упоминаемый в строке шаблона как %0) и *Base (как %1) являются выходами, а Offset (%2) — входом:
bool old;
__asm__ ("btsl %2,%1\n\t" // Turn on zero-based bit #Offset in Base.
"sbb %0,%0" // Use the CF to calculate old.
: "=r" (old), "+rm" (*Base)
: "Ir" (Offset)
: "cc");
return old;
Операнды разделяются запятыми. Каждый операнд имеет следующий формат:
[ [asmSymbolicName] ] constraint (cvariablename)
- asmSymbolicName
-
Указывает символическое имя для операнда. Ссылайтесь на имя в шаблоне ассемблера, заключив его в квадратные скобки (например, ‘%[Значение]’). Область действия имени — утверждение
asm, содержащее определение. Приемлемо любое допустимое имя C-переменной, включая имена, уже определённые в окружающем коде. Два операнда в одном и том же утвержденииasmне могут использовать одно и то же символическое имя.Если вы не используете asmSymbolicName, используйте (нулевую) позицию операнда в списке операндов в шаблоне ассемблера. Например, если есть три операнда вывода, используйте ‘%0’ для первого, ‘%1’ для второго и ‘%2’ для третьего.
- constraint
-
Строковая константа, определяющая ограничения на размещение операнда; см. Ограничения для операндов
asmдля подробностей.Ограничения вывода должны начинаться с ‘=’ (перезапись существующего значения) или ‘+’ (чтение и запись). При использовании ‘=’, не предполагайте, что расположение содержит текущее значение при входе в
asm, за исключением случаев, когда операнд связан с входом; см. Операнды входа.После префикса должно быть одно или несколько дополнительных ограничений (см. Ограничения для операндов
asm), описывающие расположение значения. Общие ограничения включают ‘r’ для регистра и ‘m’ для памяти. Если вы перечислите несколько возможных расположений (например,"=rm"), компилятор выберет наиболее эффективный вариант на основе текущего контекста. Если вы перечислите все возможные варианты, позволите оптимизаторам создавать наилучший возможный код. Если вы должны использовать определённый регистр, но ваше ограничение машины не обеспечивает достаточный контроль для выбора нужного регистра, локальные переменные регистра могут предоставить решение (см. Указание регистров для локальных переменных). - cvariablename
-
Указывает выражение C lvalue для хранения вывода, обычно имя переменной. Вложенные скобки являются обязательной частью синтаксиса.
При выборе компилятором регистров для представления операндов вывода он не использует ни один из регистров с пометами (см. Регистры с пометами и регистры-временные).
Выражения операндов вывода должны быть lvalue. Компилятор не может проверить, имеют ли операнды типы данных, подходящие для выполняемой инструкции. Для выражений вывода, которые не являются непосредственно адресуемыми (например, битовое поле), ограничение должно допускать регистр. В этом случае GCC использует регистр в качестве результата asm, а затем сохраняет этот регистр в выходные данные.
Операнды с модификатором ограничения ‘+’ учитываются как два операнда (то есть и как вход, и как выход) в отношении максимального общего количества в 30 операндов на утверждение asm.
Используйте модификатор ограничения ‘&’ (см. Символы модификатора ограничения) для всех операндов вывода, которые не должны перекрывать вход. В противном случае GCC может разместить операнд вывода в том же регистре, что и не связанный операнд входа, исходя из предположения, что код ассемблера потребляет свои входы перед получением выходов. Это предположение может быть ложным, если код ассемблера на самом деле состоит из более чем одной инструкции.
Та же проблема может возникнуть, если один параметр вывода (a) допускает ограничение регистра, а другой параметр вывода (b) — ограничение памяти. Код, сгенерированный GCC для доступа к адресу памяти в b, может содержать регистры, которые могут быть совместно использованы с a, и GCC рассматривает эти регистры как входы в asm. Как и выше, GCC предполагает, что такие регистры ввода потребляются перед записью каких-либо выходов. Это предположение может привести к неправильному поведению, если утверждение asm записывает в a перед использованием b. Сочетание модификатора ‘&’ с ограничением регистра на a гарантирует, что изменение a не повлияет на адрес, на который ссылается b. В противном случае расположение b неопределено, если a изменяется до использования b.
asm поддерживает модификаторы операндов (например, ‘%k2’ вместо просто ‘%2’). Как правило, эти квалификаторы зависят от аппаратного обеспечения. Список поддерживаемых модификаторов для x86 находится в Модификаторы операндов x86.
Если C-код, следующий за asm, не использует ни один из операндов вывода, используйте volatile для утверждения asm, чтобы предотвратить отбрасывание оптимизаторами утверждения asm как не нужного (см. Volatile).
В этом коде не используется необязательное asmSymbolicName. Поэтому он ссылается на первый операнд вывода как на %0 (если был бы второй, он был бы %1, и так далее). Номер первого операнда входа на единицу больше, чем номер последнего операнда вывода. В этом примере i386 это делает Mask, упоминаемым как %1.
uint32_t Mask = 1234;
uint32_t Index;
asm ("bsfl %1, %0"
: "=r" (Index)
: "r" (Mask)
: "cc");
Этот код перезаписывает переменную Index (‘=’), помещая значение в регистр (‘r’). Использование универсального ограничения ‘r’ вместо ограничения для определённого регистра позволяет компилятору выбрать регистр, что может привести к более эффективному коду. Это может быть невозможно, если инструкция ассемблера требует определённого регистра.
Следующий пример i386 использует синтаксис asmSymbolicName. Он производит тот же результат, что и код выше, но некоторые могут посчитать его более читабельным или более поддерживаемым, так как не требуется переупорядочивать индексы при добавлении или удалении операндов. Имена aIndex и aMask используются только в этом примере, чтобы подчеркнуть, какие имена используются где. Приемлемо повторное использование имён Index и Mask.
uint32_t Mask = 1234;
uint32_t Index;
asm ("bsfl %[aMask], %[aIndex]"
: [aIndex] "=r" (Index)
: [aMask] "r" (Mask)
: "cc");
Вот ещё несколько примеров операндов вывода.
uint32_t c = 1;
uint32_t d;
uint32_t *e = &c;
asm ("mov %[e], %[d]"
: [d] "=rm" (d)
: [e] "rm" (*e));
Здесь d может быть как в регистре, так и в памяти. Поскольку компилятор может уже иметь текущее значение расположения uint32_t, на которое указывает e, в регистре, вы можете позволить ему выбрать лучшее расположение для d, указав оба ограничения.
6.47.2.4 Операнды вывода флагов ¶
Некоторые целевые архитектуры имеют специальный регистр, хранящий «флаги» результата операции или сравнения. Обычно содержимое этого регистра либо не изменяется инструкцией asm, либо инструкция asm считается, что она уничтожает это содержимое.
В некоторых целевых архитектурах существует специальная форма операнда вывода, с помощью которой условия в регистре флагов могут быть выводами инструкции asm. Набор поддерживаемых условий зависит от целевой архитектуры, но общее правило состоит в том, что переменная вывода должна быть целочисленной скалярной переменной, а значение — булевым. Если такая возможность поддерживается, целевая архитектура определяет предопределённый символ __GCC_ASM_FLAG_OUTPUTS__.
Из-за специальной природы операндов вывода флагов ограничение не может включать альтернативы.
Чаще всего целевая архитектура имеет только один регистр флагов, который является неявным операндом многих инструкций. В этом случае операнд не должен ссылаться в шаблоне ассемблера через %0 и т. д., так как в языке ассемблера нет соответствующего текста.
- ARM
- AArch64
-
Ограничения вывода флагов для семейства ARM имеют вид ‘=@cccond’, где cond — одно из стандартных условий, определённых в ARM ARM для
ConditionHolds.eqФлаг Z установлен, или равно
neФлаг Z сброшен, или не равно
cshsФлаг C установлен или беззнаковое больше или равно
ccloФлаг C сброшен или беззнаковое меньше
miФлаг N установлен или «минус»
plФлаг N сброшен или «плюс»
vsФлаг V установлен или переполнение со знаком
vcФлаг V сброшен
hiбеззнаковое больше
lsбеззнаковое меньше или равно
geсо знаком больше или равно
ltсо знаком меньше
gtсо знаком больше
leсо знаком меньше или равно
Ограничения вывода флагов не поддерживаются в режиме thumb1.
- Семейство x86
-
Ограничения вывода флагов для семейства x86 имеют вид ‘=@cccond’, где cond — одно из стандартных условий, определённых в руководстве ISA для
jccилиsetcc.a«выше» или беззнаковое больше
ae«выше или равно» или беззнаковое больше или равно
b«ниже» или беззнаковое меньше
be«ниже или равно» или беззнаковое меньше или равно
cфлаг переноса установлен
ez«равно» или флаг нуля установлен
gсо знаком больше
geсо знаком больше или равно
lсо знаком меньше
leсо знаком меньше или равно
oфлаг переполнения установлен
pфлаг чётности установлен
sфлаг знака установлен
nanaenbnbencnengngenlnlenonpnsnz«не» флаг, или инвертированные версии вышеперечисленных
6.47.2.5 Операнды ввода ¶
Операнды ввода предоставляют значения из переменных и выражений C, доступные коду ассемблера.
Операнды разделяются запятыми. Каждый операнд имеет следующий формат:
[ [asmSymbolicName] ] constraint (cexpression)
- asmSymbolicName
-
Указывает символическое имя для операнда. Ссылка на имя в шаблоне ассемблера осуществляется в квадратных скобках (например, ‘%[Value]’). Область действия имени — это инструкция
asm, содержащая определение. Принимаются все допустимые имена переменных C, включая имена, уже определённые в окружающем коде. Две переменные внутри одной инструкцииasmне могут иметь одинаковое символическое имя.Если asmSymbolicName не используется, используется (нумерация с нуля) позиция операнда в списке операндов в шаблоне ассемблера. Например, если есть два операнда вывода и три операнда ввода, используйте ‘%2’ для ссылки на первый операнд ввода, ‘%3’ для второго и ‘%4’ для третьего.
- constraint
-
Строковая константа, задающая ограничения размещения операнда; см. Ограничения для
asmоперандов для получения подробностей.Строки ограничений ввода не могут начинаться с ‘=’ или ‘+’. При указании нескольких возможных мест (например, ‘"irm"’), компилятор выбирает наиболее эффективный вариант на основе текущего контекста. Если необходимо использовать определённый регистр, но ограничения вашей машины не обеспечивают достаточного контроля для выбора нужного регистра, локальные регистровые переменные могут предложить решение (см. Указание регистров для локальных переменных).
Ограничения ввода также могут быть числами (например,
"0"). Это указывает, что указанный операнд ввода должен находиться в том же месте, что и ограничение вывода на (нумерация с нуля) индексе в списке ограничений вывода. При использовании синтаксиса asmSymbolicName для операндов вывода, вы можете использовать эти имена (в скобках ‘[]’) вместо чисел. - cexpression
-
Это переменная или выражение C, передаваемые инструкции
asmв качестве ввода. Скобки обязательны.
При выборе регистров для представления операндов ввода компилятор не использует ни один из регистров, помеченных как уничтожаемые (см. Регистры-уничтожители и регистры-временные).
Если нет операндов вывода, но есть операнды ввода, поставьте две двоеточия там, где должны быть операнды вывода:
__asm__ ("some instructions"
: /* No outputs. */
: "r" (Offset / 8));
Предупреждение: не изменяйте содержимое операндов только для ввода (кроме ввода, связанного с выводом). Компилятор предполагает, что по выходу из инструкции asm эти операнды содержат те же значения, что и до выполнения инструкции. Невозможно использовать уничтожаемые регистры, чтобы проинформировать компилятор о том, что значения в этих входах изменяются. Одно распространённое решение — связать изменяемую переменную ввода с переменной вывода, которая никогда не используется. Однако обратите внимание, что если код, следующий за инструкцией asm, не использует ни один из операндов вывода, оптимизаторы GCC могут отбросить инструкцию asm как ненужную (см. Переменные Volatile).
asm поддерживает модификаторы операндов в операндах (например, ‘%k2’ вместо просто ‘%2’). Обычно эти квалификаторы зависят от аппаратного обеспечения. Список поддерживаемых модификаторов для x86 находится по адресу Модификаторы операндов x86.
В этом примере, использующем фиктивную инструкцию combine, ограничение "0" для операнда ввода 1 указывает, что он должен занимать то же место, что и операнд вывода 0. Только операнды ввода могут использовать числа в ограничениях, и каждое из них должно ссылаться на операнд вывода. Только число (или символическое имя ассемблера) в ограничении может гарантировать, что один операнд находится в том же месте, что и другой. Сам факт, что foo — значение обоих операндов, недостаточно, чтобы гарантировать, что они находятся в одном и том же месте в генерируемом коде ассемблера.
asm ("combine %2, %0"
: "=r" (foo)
: "0" (foo), "g" (bar));
Вот пример с символическими именами.
asm ("cmoveq %1, %2, %[result]"
: [result] "=r"(result)
: "r" (test), "r" (new), "[result]" (old));
6.47.2.6 Регистры захвата и временные регистры ¶
Хотя компилятор осведомлён о изменениях в записях, перечисленных в выходных операндах, встроенный код asm может изменять не только выходные значения. Например, вычисления могут потребовать дополнительных регистров, или процессор может перезаписать регистр как побочный эффект конкретной ассемблерной инструкции. Для того, чтобы проинформировать компилятор об этих изменениях, перечислите их в списке захвата. Элементы списка захвата — это либо имена регистров, либо специальные захваты (перечисленные ниже). Каждый элемент списка захвата — это строковая константа, заключённая в двойные кавычки и разделённая запятыми.
Описание захватов не должно ни в коем случае перекрываться с входным или выходным операндом. Например, у вас не может быть операнда, описывающего класс регистров с одним членом, когда вы перечисляете этот регистр в списке захвата. Переменные, объявленные для размещения в определённых регистрах (см. Переменные в указанных регистрах) и используемые в качестве asm входных или выходных операндов, не должны упоминаться в описании захвата. В частности, нет способа указать, что входные операнды изменяются, не указав их также как выходные операнды.
При выборе компилятором регистров для представления входных и выходных операндов, он не использует ни один из захваченных регистров. В результате захваченные регистры доступны для любого использования в ассемблерном коде.
Другое ограничение заключается в том, что список захвата не должен содержать регистр стека. Это связано с тем, что компилятор требует, чтобы значение регистра стека было таким же после инструкции asm, как и при входе в неё. Однако предыдущие версии GCC не соблюдали это правило и допускали появление регистра стека в списке с неясными семантиками. Такое поведение устарело, и перечисление регистра стека может стать ошибкой в будущих версиях GCC.
Вот реалистичный пример для VAX, демонстрирующий использование захваченных регистров:
asm volatile ("movc3 %0, %1, %2"
: /* No outputs. */
: "g" (from), "g" (to), "g" (count)
: "r0", "r1", "r2", "r3", "r4", "r5", "memory");
Также существуют два специальных аргумента захвата:
"cc"-
Захват
"cc"указывает, что ассемблерный код изменяет регистр флагов. На некоторых машинах GCC представляет коды условий как отдельный аппаратный регистр;"cc"служит для наименования этого регистра. На других машинах обработка кодов условий отличается, и указание"cc"не оказывает никакого влияния. Но оно является допустимым независимо от целевой платформы. "memory"-
Захват
"memory"сообщает компилятору, что ассемблерный код выполняет чтение или запись памяти в элементы, отличные от перечисленных в входных и выходных операндах (например, доступ к памяти, на которую указывает один из входных параметров). Для обеспечения корректности содержимого памяти GCC может потребоваться очистить определённые значения регистров в памяти перед выполнениемasm. Кроме того, компилятор не предполагает, что любые значения, прочитанные из памяти доasm, останутся неизменными послеasm; он перегружает их по мере необходимости. Использование захвата"memory"эффективно формирует барьер памяти для чтения/записи компилятора.Обратите внимание, что этот захват не предотвращает процессору от выполнения упреждающих чтений за пределами
asmинструкции. Чтобы предотвратить это, вам потребуются инструкции ограждения, специфичные для процессора.
Очистка регистров в памяти имеет последствия для производительности и может представлять проблему для кода с жёсткими временными ограничениями. Вы можете предоставить GCC более подробную информацию, чтобы избежать этого, как показано в следующих примерах. Как минимум, правила алиасинга позволяют GCC знать, какая память не нуждается в очистке.
Вот фиктивная инструкция суммирования квадратов, которая принимает два указателя на значения с плавающей точкой в памяти и производит выходной регистр с плавающей точкой. Обратите внимание, что x, и y оба появляются дважды в параметрах asm, один раз для указания доступа к памяти, и один раз для указания базового регистра, используемого asm. Обычно вы не будете тратить регистр на это, так как GCC может использовать один и тот же регистр для обеих целей. Тем не менее, было бы глупо использовать и %1 и %3 для x в этом asm и ожидать, что они будут одинаковыми. На самом деле %3 может и не быть регистром. Это может быть символическая ссылка на память на объект, на который указывает x.
asm ("sumsq %0, %1, %2"
: "+f" (result)
: "r" (x), "r" (y), "m" (*x), "m" (*y));
Вот фиктивная *z++ = *x++ * *y++ инструкция. Обратите внимание, что регистры-указатели x, y и z должны быть указаны как вход/выход, потому что asm изменяет их.
asm ("vecmul %0, %1, %2"
: "+r" (z), "+r" (x), "+r" (y), "=m" (*z)
: "m" (*x), "m" (*y));
Пример x86, где строковый аргумент памяти имеет неизвестную длину.
asm("repne scasb"
: "=c" (count), "+D" (p)
: "m" (*(const char (*)[]) p), "0" (-1), "a" (0));
Если вам известно, что будет читаться только массив из десяти байт, то вы можете использовать вход из памяти, например: "m" (*(const char (*)[10]) p).
Вот пример реализации масштабирования вектора PowerPC в ассемблере, с захватами векторов и кодов условий, а также некоторыми инициализированными регистрами смещения, которые не изменяются asm.
void
dscal (size_t n, double *x, double alpha)
{
asm ("/* lots of asm here */"
: "+m" (*(double (*)[n]) x), "+&r" (n), "+b" (x)
: "d" (alpha), "b" (32), "b" (48), "b" (64),
"b" (80), "b" (96), "b" (112)
: "cr0",
"vs32","vs33","vs34","vs35","vs36","vs37","vs38","vs39",
"vs40","vs41","vs42","vs43","vs44","vs45","vs46","vs47");
}
Вместо выделения фиксированных регистров через захваты для предоставления временных регистров для asm инструкции, альтернативой является определение переменной и установка её как выходного значения с ранним захватом, как в a2 и a3 в примере ниже. Это даёт компилятору-распределителю регистров большую свободу. Вы также можете определить переменную и сделать её выходным значением, связанным с входным значением, как в a0 и a1, связанными соответственно с ap и lda. Конечно, при использовании связанных выходов ваше asm не может использовать входное значение после изменения выходного регистра, так как они являются одним и тем же регистром. Более того, если вы опустите ранний захват на выходе, возможно, GCC может выделить тот же регистр для другого входа, если GCC может доказать, что они имели одинаковое значение при входе в asm. Именно поэтому a1 имеет ранний захват. Его связанный вход, lda возможно, известен как имеющий значение 16, и без раннего захвата, они могли бы поделиться тем же регистром, что и %11. С другой стороны, ap не может быть таким же, как любой из других входов, поэтому ранний захват на a0 не нужен. В этом случае он также нежелателен. Ранний захват на a0 заставит GCC выделить отдельный регистр для входного значения "m" (*(const double (*)[]) ap). Обратите внимание, что связывание входа с выходом — это способ настроить инициализированный временный регистр, модифицированный asm инструкцией. GCC предполагает, что входной параметр, не связанный с выходом, не изменяется, например "b" (16) ниже устанавливает %11 в 16, и GCC может использовать этот регистр в последующем коде, если значение 16 потребуется. Вы даже можете использовать обычный asm выход для временного регистра, если все входные данные, которые могут разделяться одним и тем же регистром, будут потреблены до использования временного регистра. Регистры VSX, захваченные asm инструкцией, могли бы использовать эту технику, но из-за ограничения GCC на количество asm параметров.
static void
dgemv_kernel_4x4 (long n, const double *ap, long lda,
const double *x, double *y, double alpha)
{
double *a0;
double *a1;
double *a2;
double *a3;
__asm__
(
/* lots of asm here */
"#n=%1 ap=%8=%12 lda=%13 x=%7=%10 y=%0=%2 alpha=%9 o16=%11\n"
"#a0=%3 a1=%4 a2=%5 a3=%6"
:
"+m" (*(double (*)[n]) y),
"+&r" (n), // 1
"+b" (y), // 2
"=b" (a0), // 3
"=&b" (a1), // 4
"=&b" (a2), // 5
"=&b" (a3) // 6
:
"m" (*(const double (*)[n]) x),
"m" (*(const double (*)[]) ap),
"d" (alpha), // 9
"r" (x), // 10
"b" (16), // 11
"3" (ap), // 12
"4" (lda) // 13
:
"cr0",
"vs32","vs33","vs34","vs35","vs36","vs37",
"vs40","vs41","vs42","vs43","vs44","vs45","vs46","vs47"
);
}
6.47.2.7 Метки перехода ¶
asm goto позволяет ассемблерному коду переходить к одной или нескольким меткам C. Раздел GotoLabels в asm goto инструкции содержит список всех меток C, на которые может перейти ассемблерный код. GCC предполагает, что выполнение asm происходит по порядку к следующей инструкции (если это не так, рассмотрите использование __builtin_unreachable встроенной функции после asm инструкции). Оптимизация asm goto может быть улучшена с помощью атрибутов меток hot и cold (см. Атрибуты меток).
asm goto инструкция не может иметь выходных значений. Это связано с внутренним ограничением компилятора: инструкции передачи управления не могут иметь выходных значений. Если ассемблерный код всё-таки изменяет что-либо, используйте захват "memory" для принудительной очистки всех значений регистров в памяти и перезагрузки их при необходимости после asm инструкции.
Также обратите внимание, что asm goto инструкция всегда неявно считается переменной.
Для ссылки на метку в шаблоне ассемблера добавьте префикс ‘%l’ (строчная буква ‘L’) за ней следует (нумерация с нуля) её позиция в GotoLabels плюс количество входных операндов. Например, если в asm есть три входных значения и две метки, обратитесь к первой метке как к ‘%l3’, а ко второй как к ‘%l4’.
В качестве альтернативы, вы можете ссылаться на метки с использованием фактического имени метки C, заключённого в скобки. Например, чтобы сослаться на метку с именем carry, вы можете использовать ‘%l[carry]’. Метка должна всё равно быть перечислена в разделе GotoLabels при использовании этого подхода.
Вот пример asm goto для i386:
asm goto (
"btl %1, %0\n\t"
"jc %l2"
: /* No outputs. */
: "r" (p1), "r" (p2)
: "cc"
: carry);
return 0;
carry:
return 1;
Следующий пример показывает asm goto, использующую захват памяти.
int frob(int x)
{
int y;
asm goto ("frob %%r5, %1; jc %l[error]; mov (%2), %%r5"
: /* No outputs. */
: "r"(x), "r"(&y)
: "r5", "memory"
: error);
return y;
error:
return -1;
}
6.47.2.8 Модификаторы операндов x86 ¶
Ссылки на операнды входные, выходные и перехода в шаблоне ассемблера расширенных asm инструкций могут использовать модификаторы для изменения формата операндов в коде вывода для ассемблера. Например, следующий код использует модификаторы ‘h’ и ‘b’ для x86:
uint16_t num;
asm volatile ("xchg %h0, %b0" : "+a" (num) );
Эти модификаторы генерируют такой код ассемблера:
xchg %ah, %al
В остальной части этого обсуждения используется следующий код для иллюстрации.
int main()
{
int iInt = 1;
top:
asm volatile goto ("some assembler instructions here"
: /* No outputs. */
: "q" (iInt), "X" (sizeof(unsigned char) + 1), "i" (42)
: /* No clobbers. */
: top);
}
Без модификаторов, такой результат вывода операндов для диалектов ассемблера ‘att’ и ‘intel’:
| Операнд | ‘att’ | ‘intel’ |
|---|---|---|
%0 |
%eax |
eax |
%1 |
$2 |
2 |
%3 |
$.L3 |
OFFSET FLAT:.L3 |
Ниже представлен список поддерживаемых модификаторов и их эффекты.
| Модификатор | Описание | Операнд | ‘att’ | ‘intel’ |
|---|---|---|---|---|
A |
Печать абсолютной ссылки на память. | %A0 |
*%rax |
rax |
b |
Печать имени регистра QImode. | %b0 |
%al |
al |
c |
Требуется константный операнд и печать выражения константы без знаков препинания. | %c1 |
2 |
2 |
E |
Печать адреса в режиме Double Integer (DImode) (8 байт), если целевой процессор 64-битный. В противном случае режим не определён (VOIDmode). | %E1 |
%(rax) |
[rax] |
h |
Печать имени QImode для «высокого» регистра. | %h0 |
%ah |
ah |
H |
Добавление 8 байт к смещаемой ссылке на память. Полезно при обращении к высоким 8 байтам SSE значений. Для memref в (%rax) генерирует | %H0 |
8(%rax) |
8[rax] |
k |
Печать имени регистра SImode. | %k0 |
%eax |
eax |
l |
Печать имени метки без знаков препинания. | %l3 |
.L3 |
.L3 |
p |
Печать исходного имени символа (без синтаксических префиксов). | %p2 |
42 |
42 |
P |
Если используется для функции, печатает суффикс PLT и генерирует код PIC. Например, выводит foo@PLT вместо ’foo’ для функции foo(). Если используется для константы, удаляет все синтаксические префиксы и выводит саму константу. См. p выше. |
|||
q |
Печать имени регистра DImode. | %q0 |
%rax |
rax |
w |
Печать имени регистра HImode. | %w0 |
%ax |
ax |
z |
Печать суффикса кода операции для размера текущего целочисленного операнда (один из b/w/l/q). |
%z0 |
l |
V — специальный модификатор, который печатает имя полного целочисленного регистра без %.
6.47.2.9 Операнды x86 с плавающей точкой ¶
На целевых платформах x86 существуют несколько правил использования стековых регистров в операндах asm. Эти правила применяются только к операндам, которые являются стековыми регистрами:
- При заданном наборе входных регистров, которые исчезают в
asm, необходимо знать, какие из них неявно извлекаются из стекаasm, а какие необходимо явно извлечь GCC.Входной регистр, который неявно извлекается из стека
asm, должен быть явно помечен как модифицированный, если он не ограничен для соответствия выходному операнду. - Для любого входного регистра, который неявно извлекается
asm, необходимо знать, как скорректировать стек для компенсации извлечения. Если любой не извлекаемый входной регистр расположен ближе к вершине стека регистров, чем регистр, неявно извлекаемый из стека, невозможно определить, как выглядел стек — непонятно, как остальная часть стека «сдвинулась вверх».Все неявно извлекаемые входные регистры должны располагаться ближе к вершине стека регистров, чем любые входные регистры, которые не извлекаются неявно.
Возможна ситуация, когда, если входной операнд умирает в
asm, компилятор может использовать его для перезагрузки выходного операнда. Рассмотрим этот пример:asm ("foo" : "=t" (a) : "f" (b));Этот код указывает, что входной
bне извлекается из стекаasm, аasmпомещает результат в стек регистров, то есть, послеasmстек будет на один элемент глубже, чем до него. Однако, возможно, перезагрузка может решить, что она может использовать тот же регистр как для входного, так и для выходного операндов.Чтобы предотвратить это, если любой входной операнд использует ограничение ‘f’, все ограничения выходных регистров должны использовать модификатор «раннего изменения» ‘&’.
Приведенный выше пример должен быть записан правильно как:
asm ("foo" : "=&t" (a) : "f" (b)); - Некоторые операнды должны быть в определённых местах стека. Все выходные операнды входят в эту категорию — у GCC нет другого способа узнать, в каких регистрах находятся выходные операнды, кроме как указать это в ограничениях.
- Выходные операнды не могут быть «вставлены» между существующими регистрами стека. Поскольку ни одна 387 операция не использует операнд для чтения/записи, все выходные операнды умирают до
asm, и подталкиваютсяasm. Не имеет смысла подталкивать куда-либо, кроме вершины стека регистров. - Некоторые
asmинструкции могут потребовать дополнительного места в стеке для внутренних вычислений. Это можно гарантировать, пометив в качестве модифицированных регистры стека, не связанные с входными и выходными операндами.
Эта asm принимает один вход, который неявно извлекается, и генерирует два выхода.
asm ("fsincos" : "=t" (cos), "=u" (sin) : "0" (inp));
Эта asm принимает два входа, которые извлекаются операцией fyl2xp1, и заменяет их одним выходом. st(1) модификатор необходим компилятору, чтобы знать, что fyl2xp1 извлекает оба входа.
asm ("fyl2xp1" : "=t" (result) : "0" (x), "u" (y) : "st(1)");
© Free Software Foundation
Licensed under the GNU Free Documentation License, Version 1.3.
https://gcc.gnu.org/onlinedocs/gcc-10.5.0/gcc/Extended-Asm.html