6.41 Инструкции ассемблера с операндами выражений C
В инструкции ассемблера, использующей asm, вы можете указать операнды инструкции, используя выражения C. Это означает, что вам не нужно угадывать, в каких регистрах или ячейках памяти находятся данные, которые вы хотите использовать.
Вы должны указать шаблон инструкции ассемблера, аналогичный тому, что представлен в описании машины, а также строку ограничений операндов для каждого операнда.
Например, вот как использовать инструкцию fsinx 68881:
asm ("fsinx %1,%0" : "=f" (result) : "f" (angle)); Здесь angle — выражение C для входного операнда, а result — выражение C для выходного операнда. У каждого из них есть ограничение операнда ‘"f"’, указывающее, что требуется регистр с плавающей точкой. Символ ‘=’ в ‘=f’ указывает, что операнд является выходным; все ограничения выходных операндов должны использовать ‘=’. Ограничения используют тот же язык, что и в описании машины (см. Ограничения).
Каждый операнд описывается строкой ограничений операндов, за которой следует выражение C в скобках. Двоеточие отделяет шаблон ассемблера от первого выходного операнда, а другое — последний выходной операнд от первого входного, если таковые имеются. Операнды в каждой группе разделяются запятыми. Текущее максимальное количество операндов ограничено 30; это ограничение может быть снято в будущих версиях GCC.
Если нет выходных операндов, но есть входные операнды, вы должны поместить две последовательные двоеточия вокруг места, где должны находиться выходные операнды.
Начиная с версии GCC 3.1, также можно указывать входные и выходные операнды, используя символические имена, которые можно использовать в коде ассемблера. Эти имена указываются в квадратных скобках перед строкой ограничений и могут использоваться в коде ассемблера с помощью %[имя] вместо знака процента, за которым следует номер операнда. Используя именованные операнды, пример выше может выглядеть так:
asm ("fsinx %[angle],%[output]"
: [output] "=f" (result)
: [angle] "f" (angle)); Обратите внимание, что символические имена операндов никак не связаны с другими идентификаторами C. Вы можете использовать любое имя, даже те, которые уже существуют в C, но должны убедиться, что никакие два операнда в одной конструкции ассемблера не используют одно и то же символическое имя.
Выражения выходных операндов должны быть lvalue; компилятор может проверить это. Входные операнды не должны быть lvalue. Компилятор не может проверить, имеют ли операнды типы данных, разумные для выполняемой инструкции. Он не анализирует шаблон ассемблерной инструкции и не знает, что это значит, или даже является ли это допустимым вводом ассемблера. Расширенная asm функция чаще всего используется для машинных инструкций, существование которых компилятор сам не знает. Если выражение вывода не может быть непосредственно адресовано (например, это битовое поле), ваше ограничение должно допускать регистр. В этом случае GCC использует регистр как вывод asm, а затем сохраняет этот регистр в выходные данные.
Обычные выходные операнды должны быть только для записи; GCC предполагает, что значения в этих операндах до выполнения инструкции мертвы и не должны генерироваться. Расширенный asm поддерживает входные/выходные или чтение/запись операнды. Используйте символ ограничения ‘+’, чтобы указать такой операнд и включите его в выходные операнды.
В качестве альтернативы вы можете логически разделить его функцию на два отдельных операнда: один входной операнд и один выходной операнд только для записи. Связь между ними выражается ограничениями, которые говорят, что они должны находиться в одном месте при выполнении инструкции. Вы можете использовать одно и то же выражение C для обоих операндов или разные выражения. Например, здесь мы пишем (гипотетическую) инструкцию ‘combine’ с bar в качестве только для чтения источника и foo в качестве чтения/записи назначения:
asm ("combine %2,%0" : "=r" (foo) : "0" (foo), "g" (bar)); Ограничение ‘"0"’ для операнда 1 говорит, что он должен занимать то же место, что и операнд 0. Число в ограничении разрешено только в входном операнде, и оно должно относиться к выходному операнду.
Только число в ограничении может гарантировать, что один операнд находится в том же месте, что и другой. Просто тот факт, что foo является значением обоих операндов, недостаточно для гарантии, что они находятся в одном месте в сгенерированном коде ассемблера. Следующее не работает надежно:
asm ("combine %2,%0" : "=r" (foo) : "r" (foo), "g" (bar)); Различные оптимизации или перезагрузка могут привести к тому, что операнды 0 и 1 будут в разных регистрах; GCC не знает, почему бы этого не сделать. Например, компилятор может найти копию значения foo в одном регистре и использовать его для операнда 1, но сгенерировать выходной операнд 0 в другом регистре (скопировав его впоследствии в адрес foo).
Конечно, поскольку регистр для операнда 1 даже не упоминается в коде ассемблера, результат не будет работать, но GCC не может этого понять.
Начиная с версии GCC 3.1, можно записать [имя] вместо номера операнда для соответствующего ограничения. Например:
asm ("cmoveq %1,%2,%[result]"
: [result] "=r"(result)
: "r" (test), "r"(new), "[result]"(old)); Иногда вам нужно сделать операнд asm конкретным регистром, но для этого регистра нет соответствующей буквы ограничения самостоятельно. Чтобы принудительно поместить операнд в этот регистр, используйте локальную переменную для операнда и укажите регистр в объявлении переменной. См. Явные регистровые переменные. Затем для операнда asm используйте любую букву ограничения регистра, которая соответствует регистру:
register int *p1 asm ("r0") = ...;
register int *p2 asm ("r1") = ...;
register int *result asm ("r0");
asm ("sysint" : "=r" (result) : "0" (p1), "r" (p2)); В приведенном выше примере обратите внимание, что регистр, который изменяется вызовом, согласно ABI целевой системы, будет перезаписан любым вызовом функции в присваивании, включая вызовы библиотек для арифметических операторов. Кроме того, регистр может быть изменен при генерации некоторых операций, таких как сдвиг переменных, копирование памяти или перемещение памяти в x86. Предполагая, что это регистр, изменяемый вызовом, это может произойти с r0 выше при присваивании p2.
Если вам необходимо использовать такой регистр, используйте временные переменные для выражений между присваиванием регистра и использованием:
int t1 = ...;
register int *p1 asm ("r0") = ...;
register int *p2 asm ("r1") = t1;
register int *result asm ("r0");
asm ("sysint" : "=r" (result) : "0" (p1), "r" (p2)); Некоторые инструкции изменяют определенные жесткие регистры. Для описания этого напишите третью двоеточие после входных операндов, после которой следуют имена изменяемых жестких регистров (указанные как строки). Вот реальный пример для VAX:
asm volatile ("movc3 %0,%1,%2"
: /* no outputs */
: "g" (from), "g" (to), "g" (count)
: "r0", "r1", "r2", "r3", "r4", "r5"); Вы не можете описать изменение таким образом, чтобы оно перекрывалось с входным или выходным операндом. Например, вы не можете иметь операнд, описывающий класс регистров с одним членом, если вы упомянули этот регистр в списке изменений. Переменные, объявленные для работы в определенных регистрах (см. Явные регистровые переменные), и используемые в качестве входных или выходных операндов asm, не должны упоминаться в описании изменения. Нет способа указать, что входной операнд изменяется, не указав его также в качестве выходного операнда. Обратите внимание, что если все указанные вами выходные операнды предназначены для этой цели (и поэтому не используются), вам также необходимо указать volatile для конструкции asm, как описано ниже, чтобы предотвратить удаление GCC инструкции asm как неиспользуемой.
Если вы ссылаетесь на определенный аппаратный регистр из кода ассемблера, вам, вероятно, придется перечислить регистр после третьей двоеточия, чтобы сообщить компилятору, что значение регистра изменено. В некоторых ассемблерах имена регистров начинаются с ‘%’; чтобы получить ‘%’ в коде ассемблера, вы должны написать ‘%%’ в вводе.
Если ваша инструкция ассемблера может изменить регистр состояния, добавьте ‘cc’ в список изменяемых регистров. GCC на некоторых машинах представляет регистры состояния как определенный аппаратный регистр; ‘cc’ служит для именования этого регистра. На других машинах регистр состояния обрабатывается иначе, и указание ‘cc’ не имеет эффекта. Но это допустимо независимо от машины.
Если ваши инструкции ассемблера непредсказуемо обращаются к памяти, добавьте ‘memory’ в список изменяемых регистров. Это заставит GCC не хранить значения памяти в регистрах через инструкцию ассемблера и не оптимизировать сохранение или загрузку в эту память. Вы также должны добавить ключевое слово volatile если затрагиваемая память не указана во вводах или выходах asm, так как изменение ‘memory’ не учитывается как побочный эффект инструкции asm. Если вы знаете, какой объем памяти используется, вы можете добавить его как ввод или вывод, но если это неизвестно, вы должны добавить ‘memory’. Например, если вы обращаетесь к десяти байтам строки, вы можете использовать такой вход памяти:
{"m"( ({ struct { char x[10]; } *p = (void *)ptr ; *p; }) )}. Обратите внимание, что в приведенном ниже примере вход памяти необходим, в противном случае GCC может оптимизировать сохранение в x:
int foo ()
{
int x = 42;
int *y = &x;
int result;
asm ("magic stuff accessing an 'int' pointed to by '%1'"
: "=&d" (result) : "a" (y), "m" (*y));
return result;
} Вы можете объединить несколько инструкций ассемблера в одном шаблоне asm, разделенных символами, обычно используемыми в коде ассемблера системы. В большинстве случаев подойдет сочетание новой строки для разрыва строки и символа табуляции для перехода в поле инструкции (записано как ‘\n\t’). Иногда можно использовать точка с запятой, если ассемблер допускает точку с запятой в качестве символа разрыва строки. Обратите внимание, что некоторые диалекты ассемблеров используют точку с запятой для начала комментария. Входные операнды гарантированно не используют ни один из изменяемых регистров, и не используют адреса выходных операндов, поэтому вы можете читать и записывать изменяемые регистры столько раз, сколько захотите. Вот пример нескольких инструкций в шаблоне; он предполагает, что подпрограмма _foo принимает аргументы в регистрах 9 и 10:
asm ("movl %0,r9\n\tmovl %1,r10\n\tcall _foo"
: /* no outputs */
: "g" (from), "g" (to)
: "r9", "r10"); Если для выходного операнда не указано ограничение ‘&’, GCC может разместить его в том же регистре, что и не связанный входной операнд, исходя из предположения, что входные данные потребляются до того, как будут произведены выходные. Это предположение может быть ложным, если код ассемблера фактически состоит из более чем одной инструкции. В этом случае используйте ‘&’ для каждого выходного операнда, который не может перекрывать входной. См. Модификаторы.
Если вы хотите проверить регистр состояния, произведенный инструкцией ассемблера, вы должны включить разветвление и метку в конструкции asm, как показано ниже:
asm ("clr %0\n\tfrob %1\n\tbeq 0f\n\tmov #1,%0\n0:"
: "g" (result)
: "g" (input)); Предполагается, что ваш ассемблер поддерживает локальные метки, как это делает ассемблер GNU и большинство ассемблеров Unix.
Говоря о метках, переходы от одной asm к другой не поддерживаются. Оптимизаторы компилятора не знают об этих переходах и, следовательно, не могут учитывать их при принятии решения об оптимизации. См. Расширенный asm с goto.
Обычно наиболее удобный способ использования этих инструкций asm — их инкапсуляция в макросы, похожие на функции. Например,
#define sin(x) \
({ double __value, __arg = (x); \
asm ("fsinx %1,%0": "=f" (__value): "f" (__arg)); \
__value; }) Здесь используется переменная __arg для проверки, что инструкция работает с правильным значением double, и что принимаются только те аргументы x, которые могут быть автоматически преобразованы в double.
Другой способ убедиться, что инструкция работает с правильным типом данных, — использовать приведение типов в asm. Это отличается от использования переменной __arg, так как оно преобразует больше различных типов. Например, если желаемый тип — int, приведение аргумента к int принимает указатель без замечаний, в то время как присваивание аргумента переменной типа int с именем __arg предупреждает о использовании указателя, если вызывающий код явно не привёл его к нужному типу.
Если у asm есть выходные операнды, GCC предполагает для оптимизации, что инструкция не имеет побочных эффектов, кроме изменения выходных операндов. Это не означает, что инструкции с побочными эффектами нельзя использовать, но нужно быть осторожным, потому что компилятор может их удалить, если выходные операнды не используются, или перенести их из циклов, или заменить две на одну, если они составляют общую подвыражение. Кроме того, если ваша инструкция имеет побочный эффект на переменную, которая, по-видимому, не изменяется, старое значение переменной может быть повторно использовано позже, если оно окажется в регистре.
Вы можете предотвратить удаление инструкции asm посредством записи ключевого слова volatile после asm. Например:
#define get_and_set_priority(new) \
({ int __old; \
asm volatile ("get_and_set_priority %0, %1" \
: "=g" (__old) : "g" (new)); \
__old; }) Ключевое слово volatile указывает, что инструкция имеет важные побочные эффекты. GCC не удаляет инструкцию с модификатором volatile asm, если она доступна. (Инструкция всё ещё может быть удалена, если GCC может доказать, что поток управления никогда не достигает местоположения инструкции.) Обратите внимание, что даже инструкция с модификатором 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; Аналогично, вы не можете ожидать, что последовательность инструкций volatile asm останется идеально последовательной. Если вам нужны последовательные результаты, используйте одну инструкцию asm. Кроме того, GCC выполняет некоторые оптимизации через инструкцию volatile asm; GCC не «забывает всё», когда встречает инструкцию volatile asm так, как это делают некоторые другие компиляторы.
Инструкция asm без выходных операндов обрабатывается идентично инструкции volatile asm.
Естественно попытаться получить доступ к коду условия, оставленному инструкцией ассемблера. Однако, когда мы пытались реализовать это, мы не нашли способа сделать это надёжно. Проблема в том, что выходные операнды могут потребовать перезагрузки, что приводит к дополнительным инструкциям «записи». На большинстве машин эти инструкции изменяют код условия до того, как появляется возможность его проверить. Эта проблема не возникает для обычных инструкций «проверки» и «сравнения», так как у них нет выходных операндов.
По причинам, аналогичным описанным выше, невозможно дать инструкции ассемблера доступ к коду условия, оставленному предыдущими инструкциями.
Начиная с GCC версии 4.5, можно использовать asm goto для перехода в одну или несколько меток C. В этом формате пятая секция после списка модификаций содержит список всех меток C, в которые может перейти ассемблер. Каждый операнд метки неявно имеет своё имя. Также предполагается, что asm также переходит к следующему оператору.
Этот формат asm ограничен отсутствием выходных данных. Это связано с внутренней ограничением компилятора, согласно которому инструкции передачи управления не могут иметь выходные данные. Это ограничение для asm goto может быть снято в будущих версиях компилятора. Тем временем, asm goto может включать модификацию памяти, тем самым оставляя выходные данные в памяти.
int frob(int x)
{
int y;
asm goto ("frob %%r5, %1; jc %l[error]; mov (%2), %%r5"
: : "r"(x), "r"(&y) : "r5", "memory" : error);
return y;
error:
return -1;
} В этом (неэффективном) примере инструкция frob устанавливает бит переноса, чтобы указать на ошибку. Инструкция jc обнаруживает это и переходит к метке error. Наконец, результат инструкции frob (%r5) записывается в память для переменной y, которая позже считывается оператором return.
void doit(void)
{
int i = 0;
asm goto ("mfsr %%r1, 123; jmp %%r1;"
".pushsection doit_table;"
".long %l0, %l1, %l2, %l3;"
".popsection"
: : : "r1" : label1, label2, label3, label4);
__builtin_unreachable ();
label1:
f1();
return;
label2:
f2();
return;
label3:
i = 1;
label4:
f3(i);
} В этом (тоже неэффективном) примере инструкция mfsr считывает адрес из внедиапазонного системного регистра, а следующая инструкция jmp переходит по этому адресу. Предполагается, что адрес, считанный инструкцией mfsr, был ранее установлен с помощью некоторого механизма, специфичного для приложения, в качестве одного из четырёх значений, хранящихся в разделе doit_table. Наконец, инструкция asm следует за вызовом __builtin_unreachable, чтобы указать, что asm фактически не переходит к следующей инструкции.
#define TRACE1(NUM) \
do { \
asm goto ("0: nop;" \
".pushsection trace_table;" \
".long 0b, %l0;" \
".popsection" \
: : : : trace#NUM); \
if (0) { trace#NUM: trace(); } \
} while (0)
#define TRACE TRACE1(__COUNTER__) В этом примере (который фактически вдохновил функцию asm goto) мы в редких случаях хотим вызвать функцию trace; в других случаях мы хотим минимизировать накладные расходы. Обычный путь состоит из одной инструкции nop. Однако мы записываем адрес этой инструкции nop вместе с адресом метки, которая вызывает функцию trace. Это позволяет инструкции nop в процессе выполнения быть заменена безусловным переходом к сохраненной метке. Предполагается, что оптимизирующий компилятор переместит помеченную область, чтобы оптимизировать путь перехода от asm.
Если вы пишете заголовочный файл, который должен быть включён в программы на языке ISO C, используйте __asm__ вместо asm. См. Альтернативные ключевые слова.
6.41.1 Размер инструкции asm
Некоторые целевые платформы требуют, чтобы GCC отслеживал размер каждой используемой инструкции для генерации правильного кода. Поскольку окончательная длина инструкции asm известна только ассемблеру, GCC должен оценить её размер. Оценка формируется путём подсчёта количества операторов в шаблоне asm и умножения этого значения на длину самой длинной инструкции на данном процессоре. Операторы в шаблоне asm идентифицируются символами новой строки и символами разделителей операторов, поддерживаемыми ассемблером; на большинстве процессоров это символ ‘;’.
Обычно оценка GCC является вполне достаточной для обеспечения генерации корректного кода, но компилятор может быть сбит с толку, если вы используете псевдоинструкции или макросы ассемблера, которые расширяются до нескольких реальных инструкций, или если вы используете директивы ассемблера, которые занимают больше места в объектном файле, чем требуется для одной инструкции. Если это произойдёт, ассемблер выдаст диагностическое сообщение, что метка недостижима.
6.41.2 Операнды с плавающей точкой i386 asm
На целевых платформах i386 существуют некоторые правила использования стековых регистров в операндах инструкции asm. Эти правила применяются только к операндам, являющимся стековыми регистрами:
- При заданном наборе входных регистров, которые уничтожаются в инструкции
asm, необходимо знать, какие из них неявно извлекаются инструкциейasm, а какие должны быть явно извлечены GCC.Входной регистр, который неявно извлекается инструкцией
asm, должен быть явно помечен как clobbered, за исключением случаев, когда он ограничен соответствием выходному операнду. - Для любого входного регистра, который неявно извлекается инструкцией
asm, необходимо знать, как скорректировать стек для компенсации извлечения. Если какой-либо не извлекаемый вход находится ближе к вершине стека регистров, чем неявно извлекаемый регистр, то невозможно понять, как выглядел остальной стек — неясно, как остальная часть стека «сдвигается вверх».Все неявно извлекаемые входные регистры должны находиться ближе к вершине стека регистров, чем любой входной, который не извлекается неявно.
Возможна ситуация, когда, если входной операнд уничтожается в инструкции
asm, компилятор может использовать входной регистр для перезагрузки выходного. Рассмотрим этот пример:asm ("foo" : "=t" (a) : "f" (b));Этот код указывает, что входной операнд
bне извлекается инструкциейasm, и что инструкцияasmпомещает результат в стек регистров, то есть, стек становится на один уровень глубже после инструкцииasm, чем был до неё. Однако, возможна ситуация, когда модуль перезагрузки может предположить, что он может использовать один и тот же регистр как для входного, так и для выходного.Чтобы предотвратить это, если какой-либо входной операнд использует ограничение
f, все выходные ограничения регистров должны использовать модификатор early-clobber&.Приведённый выше пример был бы правильно написан следующим образом:
asm ("foo" : "=&t" (a) : "f" (b)); - Некоторые операнды должны находиться в определённых местах стека. Все выходные операнды относятся к этой категории — GCC не знает, в каких регистрах появляются выходные данные, если вы не укажете это в ограничениях.
Выходные операнды должны чётко указывать, в каком регистре появляется выходной операнд после инструкции
asm.=fнедопустимо: ограничения операндов должны выбирать класс с одним регистром. - Выходные операнды не могут быть «вставлены» между существующими стековыми регистрами. Так как ни одна операция 387 не использует операнд чтения/записи, все выходные операнды уничтожаются до выполнения инструкции
asm, и добавляются инструкциейasm. Нет смысла добавлять что-либо кроме вершины стека регистров.Выходные операнды должны начинаться с вершины стека: выходные операнды не могут «пропустить» регистр.
- Некоторые инструкции
asmмогут потребовать дополнительного места в стеке для внутренних вычислений. Это можно гарантировать путём отметки как clobbered стековых регистров, не связанных с входными и выходными операндами.
Вот несколько разумных примеров инструкций 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-4.9.4/gcc/Extended-Asm.html