6.45.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 могут находиться вне функций (см. Базовый 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.
6.45.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.45.2.2 Шаблон ассемблера
Шаблон ассемблера — это строковая константа, содержащая ассемблерные инструкции. Компилятор заменяет в шаблоне маркеры, которые ссылаются на входные, выходные и метки переходов, а затем выводит полученную строку в ассемблер. Строка может содержать любые инструкции, распознаваемые ассемблером, включая директивы. GCC не анализирует ассемблерные инструкции сами и не знает, что они означают или даже являются ли они корректным входным ассемблерным кодом. Однако он считает инструкции (см. Размер asm).
Вы можете поместить несколько ассемблерных инструкций вместе в одну asm строку, разделенные символами, обычно используемыми в ассемблерном коде системы. Сочетание, работающее в большинстве мест, — это перевод строки для разрыва строки и табуляция для перемещения к полю инструкции (записанное как ‘\n\t’). Некоторые ассемблеры допускают использование точки с запятой в качестве разделителя строк. Однако обратите внимание, что в некоторых диалектах ассемблера точка с запятой используется для начала комментария.
Не ожидайте, что последовательность asm инструкций останется идеально последовательной после компиляции, даже когда вы используете квалификатор volatile. Если некоторые инструкции должны оставаться последовательными в выходе, поместите их в одну инструкцию asm с несколькими инструкциями.
Доступ к данным из C-программ без использования входных/выходных операндов (например, путём использования глобальных символов непосредственно из шаблона ассемблера) может не работать как ожидается. Аналогично, вызов функций непосредственно из шаблона ассемблера требует детального понимания целевого ассемблера и ABI.
Поскольку GCC не анализирует шаблон ассемблера, у него нет возможности видеть какие-либо символы, к которым он ссылается. Это может привести к тому, что GCC отбросит эти символы как неиспользуемые, если они не указаны также как входные, выходные или операнды перехода.
Специальные строковые форматы
Помимо маркеров, описанных входными, выходными и операндами переходов, эти маркеры имеют специальное значение в шаблоне ассемблера:
- ‘%%’
-
Выводит одиночный ‘%’ в ассемблерный код.
- ‘%=’
-
Выводит число, уникальное для каждого экземпляра
asmинструкции во всей компиляции. Эта опция полезна при создании локальных меток и ссылках на них несколько раз в одном шаблоне, который генерирует несколько ассемблерных инструкций. - ‘%{’
- ‘%|’
- ‘%}’
Выводит символы ‘{’, ‘|’ и ‘}’ (соответственно) в ассемблерный код. В неускользнутом виде эти символы имеют специальное значение, чтобы указать на несколько диалектов ассемблера, как описано ниже.
Несколько диалектов ассемблера в шаблонах asm
На таких целевых платформах, как x86, GCC поддерживает несколько диалектов ассемблера. Опция -masm управляет диалектом, который GCC использует по умолчанию для встроенного ассемблера. В документации, специфичной для целевой платформы, для опции -masm представлен список поддерживаемых диалектов, а также диалект по умолчанию, если опция не указана. Эта информация может быть важна для понимания, поскольку ассемблерный код, работающий корректно при компиляции с использованием одного диалекта, вероятно, даст сбой при компиляции с использованием другого. См. x86 Options.
Если ваш код должен поддерживать несколько диалектов ассемблера (например, если вы пишете общедоступные заголовки, которые должны поддерживать различные параметры компиляции), используйте конструкции следующего вида:
{ 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.45.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
-
Указывает символическое имя для оператора. Ссылайтесь на имя в шаблоне ассемблера, заключив его в квадратные скобки (например, «%[Value]»). Область действия имени — выражение
asm, содержащее определение. Приемлемо любое допустимое имя переменной C, включая имена, уже определенные в окружающем коде. Два оператора внутри одного выраженияasmне могут использовать одно и то же символическое имя.Если вы не используете asmSymbolicName, используйте позицию (с нуля) оператора в списке операторов в шаблоне ассемблера. Например, если есть три оператора вывода, в шаблоне используйте «%0» для первого, «%1» для второго и «%2» для третьего.
- constraint
-
Строковая константа, указывающая ограничения на размещение оператора; см. Constraints для подробностей.
Операторы вывода должны начинаться либо с «=» (перезапись существующего значения переменной), либо с «+» (при чтении и записи). При использовании «=», не предполагайте, что местоположение содержит существующее значение при входе в
asm, за исключением случаев, когда оператор связан с вводом; см. Операторы ввода.После префикса должно быть одно или несколько дополнительных ограничений (см. Constraints), которые описывают расположение значения. Общие ограничения включают «r» для регистра и «m» для памяти. Когда вы перечисляете более одного возможного расположения (например,
"=rm"), компилятор выбирает наиболее эффективный вариант на основе текущего контекста. Если вы перечисляете все возможные альтернативы, доступные для выраженияasm, вы предоставляете оптимизаторам возможность создавать наилучший возможный код. Если вам необходимо использовать определенный регистр, но ваши ограничения для машины не предоставляют достаточного контроля для выбора нужного регистра, локальные переменные регистров могут предложить решение (см. Локальные переменные регистров). - cvariablename
-
Указывает выражение C lvalue для хранения результата, обычно имя переменной. Вложенные скобки являются обязательной частью синтаксиса.
При выборе компилятором регистров для представления операторов вывода он не использует ни один из регистров, помеченных как модифицированные (см. Регистры сбоя и регистры-временные переменные).
Выражения операторов вывода должны быть lvalue. Компилятор не может проверить, соответствуют ли типы операндов разумным для выполняемой инструкции. Для выражений операторов вывода, которые не являются напрямую адресуемыми (например, битовое поле), ограничение должно разрешать регистр. В этом случае GCC использует регистр в качестве результата asm, а затем сохраняет этот регистр в результат.
Операторы с модификатором ограничения «+» считаются двумя операторами (то есть как вводом, так и выводом) с точки зрения максимального количества 30 операторов в выражении asm .
Используйте модификатор ограничения «&» (см. Modifiers) для всех операторов вывода, которые не должны перекрывать ввод. В противном случае GCC может выделить оператор вывода в том же регистре, что и несвязанный оператор ввода, исходя из предположения, что код ассемблера потребляет свои вводы перед созданием выводов. Это предположение может быть ложным, если код ассемблера на самом деле состоит из более чем одной инструкции.
Такая же проблема может возникнуть, если один параметр вывода (a) допускает ограничение для регистра, а другой параметр вывода (b) допускает ограничение для памяти. Код, сгенерированный GCC для доступа к адресу памяти в b, может содержать регистры, которые могут быть общими с a, и GCC рассматривает эти регистры как вводы в asm. Как и выше, GCC предполагает, что такие регистры ввода используются перед записью каких-либо выводов. Это предположение может привести к неправильному поведению, если asm записывает в a до использования b. Объединение модификатора «&» с ограничением регистра на a гарантирует, что модификация a не повлияет на адрес, к которому ссылается b. В противном случае расположение b неопределено, если a модифицируется до использования b.
asm поддерживает модификаторы операторов (например, «%k2» вместо просто «%2»). Обычно эти квалификаторы зависят от аппаратного обеспечения. Список поддерживаемых модификаторов для x86 находится по адресу x86 Operand modifiers.
Если код 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.45.2.4 Операторы вывода флагов
Некоторые целевые платформы имеют специальный регистр, который хранит «флаги» для результата операции или сравнения. Обычно содержимое этого регистра либо не изменяется asm, либо asm считается разрушающим содержимое.
На некоторых целевых платформах существует специальная форма оператора вывода, позволяющая использовать условия в регистре флагов в качестве результатов asm. Набор поддерживаемых условий специфичен для целевой платформы, но общим правилом является то, что переменная вывода должна быть скалярным целым числом, а значение — булевым. При поддержке целевая платформа определяет препроцессорную константу __GCC_ASM_FLAG_OUTPUTS__.
Из-за специфики операторов вывода флагов ограничение, возможно, не должно включать альтернативы.
Чаще всего целевая платформа имеет только один регистр флагов, который, следовательно, является неявным операндом многих инструкций. В этом случае оператор не должен ссылаться в шаблоне ассемблера через %0 и т. д., так как нет соответствующего текста на языке ассемблера.
- Семейство x86
-
Ограничения на вывод флагов для семейства x86 имеют вид ‘=@cccond’, где cond — одно из стандартных условий, определенных в руководстве ISA для
jccилиsetcc.a«выше» или беззначительно больше
ae«выше или равно» или беззначительно больше или равно
b«ниже» или беззначительно меньше
be«ниже или равно» или беззначительно меньше или равно
cфлаг переноса установлен
ez«равно» или флаг нуля установлен
gзначительно больше
geзначительно больше или равно
lзначительно меньше
leзначительно меньше или равно
oфлаг переполнения установлен
pфлаг чётности установлен
sфлаг знака установлен
nanaenbnbencnengngenlnlenonpnsnz«не» флаг, или инвертированные версии вышеперечисленных
6.45.2.5 Операнды ввода
Операнды ввода предоставляют значения из переменных и выражений C для кода ассемблера.
Операнды разделяются запятыми. Каждый операнд имеет такой формат:
[ [asmSymbolicName] ] constraint (cexpression)
- asmSymbolicName
-
Указывает символическое имя для операнда. Ссылайтесь на имя в шаблоне ассемблера, заключив его в квадратные скобки (например, ‘%[Value]’). Сфера действия имени — оператор
asm, содержащий определение. Любое допустимое имя переменной C приемлемо, включая имена, уже определенные в окружающем коде. Два операнда в одном оператореasmне могут использовать одно и то же символическое имя.Если не используется asmSymbolicName, используйте (нумерацию с нуля) позицию операнда в списке операндов в шаблоне ассемблера. Например, если есть два выходных операнда и три входных, используйте ‘%2’ для первого входного операнда, ‘%3’ для второго и ‘%4’ для третьего.
- constraint
-
Строковая константа, задающая ограничения на размещение операнда; см. Ограничения для подробностей.
Строки ограничений ввода не могут начинаться с ‘=’ или ‘+’. Если вы указываете несколько возможных мест (например, ‘"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.45.2.6 Затираемые и рабочие регистры
Хотя компилятор знает об изменениях, внесенных в записи, указанные в выходных операндах, код ассемблера asm может изменять больше, чем просто выходы. Например, для вычислений могут потребоваться дополнительные регистры, или процессор может перезаписать регистр как побочный эффект определенной инструкции ассемблера. Чтобы проинформировать компилятор об этих изменениях, укажите их в списке «затираемых» значений. Элементы списка «затираемых» значений — это имена регистров или специальные затираемые значения (перечисленные ниже). Каждый элемент списка «затираемых» значений — это строковая константа в двойных кавычках, разделенная запятыми.
Описание затираемых значений не должно никоим образом пересекаться с операндами ввода или вывода. Например, вы не можете иметь операнд, описывающий класс регистров с одним членом, при указании этого регистра в списке «затираемых» значений. Переменные, объявленные как находящиеся в определенных регистрах (см. Явные переменные регистра) и используемые в качестве операндов ввода или вывода asm , не должны быть упомянуты в описании затираемых значений. В частности, нет способа указать, что входные операнды изменяются, не указав их также как выходные операнды.
При выборе компилятором регистров для представления входных и выходных операндов он не использует ни один из регистров, помеченных как «затираемые». В результате затираемые регистры доступны для любого использования в коде ассемблера.
Вот реальный пример для 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.45.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.45.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)
: /* No clobbers. */
: top);
}
Без модификаторов, вот как будет выглядеть вывод операндов для диалектов ассемблера ‘att’ и ‘intel’:
| Операнд | ‘att’ | ‘intel’ |
|---|---|---|
%0 |
%eax |
eax |
%1 |
$2 |
2 |
%2 |
$.L2 |
OFFSET FLAT:.L2 |
В таблице ниже приведён список поддерживаемых модификаторов и их эффекты.
| Модификатор | Описание | Операнд | ‘att’ | ‘intel’ |
|---|---|---|---|---|
z |
Печать постфикса кода операции для размера текущего целочисленного операнда (один из b/w/l/q). |
%z0 |
l |
|
b |
Печать имени регистра для типа QI. | %b0 |
%al |
al |
h |
Печать имени регистра для “высокого” регистра типа QI. | %h0 |
%ah |
ah |
w |
Печать имени регистра типа HI. | %w0 |
%ax |
ax |
k |
Печать имени регистра типа SI. | %k0 |
%eax |
eax |
q |
Печать имени метки без знаков препинания. | %q0 |
%rax |
rax |
l |
Требование константного операнда и печать выражения константы без знаков препинания. | %l2 |
.L2 |
.L2 |
V — это специальный модификатор, который печатает полное имя целочисленного регистра без %.
6.45.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 не может узнать, в каких регистрах находятся выходы, если вы не укажете это в ограничениях.
Выходные операнды должны чётко указывать, в каком регистре находится выход после оператора
asm. ‘=f’ не допускается: операнды должны выбирать класс с единственным регистром. - Выходные операнды не могут быть “вставлены” между существующими регистрами стека. Поскольку ни одна команда 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)");
Далее: Ограничения, Предыдущее: Основные ассемблерные команды, Верхний уровень: Использование ассемблерного языка с C [Оглавление][Индекс]
© Free Software Foundation
Licensed under the GNU Free Documentation License, Version 1.3.
https://gcc.gnu.org/onlinedocs/gcc-8.5.0/gcc/Extended-Asm.html