Spec-Zone.ru › GCC 14

6.48.2 Расширенные Asm — Инструкции ассемблера с операндами выражений C

С расширенным asm вы можете читать и записывать переменные C из ассемблера и выполнять переходы из ассемблерного кода к меткам C. Расширенный asm синтаксис использует двоеточия (‘:’) для разграничения параметров операндов после шаблона ассемблера:

asm asm-qualifiers ( AssemblerTemplate 
                 : OutputOperands 
                 [ : InputOperands
                 [ : Clobbers ] ])

asm asm-qualifiers ( AssemblerTemplate 
                      : OutputOperands
                      : 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.

  • Volatile
  • Шаблон ассемблера
  • Выходные операнды
  • Выходные операнды флагов
  • Входные операнды
  • Clobbers и рабочие регистры
  • Метки перехода
  • Общие модификаторы операндов
  • Модификаторы операндов AArch64
  • Модификаторы операндов x86
  • Операнды x86 с плавающей запятой asm
  • Модификаторы операндов MSP430
  • Модификаторы операндов LoongArch
  • Модификаторы операндов RISC-V

6.48.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 может дублировать (или удалять дубликаты) вашего ассемблерного кода во время оптимизации. Это может привести к неожиданным ошибкам дублирования символов во время компиляции, если ваш asm код определяет символы или метки. Использование ‘%=’ (см. AssemblerTemplate) может помочь решить эту проблему.

6.48.2.2 Шаблон ассемблера

Шаблон ассемблера — это строковая константа, содержащая инструкции ассемблера. Компилятор заменяет в шаблоне токены, относящиеся к входным данным, выходным данным и меткам переходов, а затем выводит полученную строку в ассемблер. Строка может содержать любые инструкции, распознаваемые ассемблером, включая директивы. GCC не анализирует сами инструкции ассемблера и не знает, что они означают или даже являются ли они корректным вводом для ассемблера. Однако он подсчитывает операторы (см. Размер asm).

Вы можете разместить несколько инструкций ассемблера вместе в одной asm строке, разделяя их символами, обычно используемыми в коде ассемблера для данной системы. Комбинация, которая работает в большинстве случаев, — это перевод строки и табуляция для перехода к полю инструкции (записано как ‘\n\t’). Некоторые ассемблеры допускают использование точки с запятой в качестве разделителя строк. Однако обратите внимание, что некоторые диалекты ассемблера используют точку с запятой для начала комментария.

Не ожидайте, что последовательность asm операторов останется идеально последовательной после компиляции, даже если вы используете квалификатор volatile. Если некоторые инструкции должны оставаться последовательными в выводе, поместите их в один многоинструкционный asm оператор.

Доступ к данным из C-программ без использования операндов ввода/вывода (например, путём прямого использования глобальных символов из шаблона ассемблера) может не работать как ожидается. Аналогично, вызов функций непосредственно из шаблона ассемблера требует подробного понимания целевого ассемблера и ABI.

Поскольку GCC не анализирует шаблон ассемблера, он не видит какие-либо символы, на которые он ссылается. Это может привести к тому, что GCC отбросит эти символы как неиспользуемые, если они не указаны также в качестве операндов ввода, вывода или перехода.

Специальные строковые форматы

В дополнение к токенам, описанным операндами ввода, вывода и перехода, эти токены имеют специальное значение в шаблоне ассемблера:

‘%%’

Выводит одиночный символ ‘%’ в код ассемблера.

‘%=’

Выводит число, уникальное для каждой инстанции 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.48.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

Строковая константа, определяющая ограничения по расположению операнда; см. Ограничения для операндов asm для подробностей.

Ограничения вывода должны начинаться с ‘=’ (перезапись существующего значения) или ‘+’ (при чтении и записи). При использовании ‘=’, не предполагайте, что расположение содержит существующее значение при входе в asm, за исключением случая, когда операнд связан с входом; см. Операнды ввода.

После префикса должно быть одно или несколько дополнительных ограничений (см. Ограничения для операндов asm), описывающих, где находится значение. Общие ограничения включают ‘r’ для регистра и ‘m’ для памяти. Когда вы указываете более одного возможного расположения (например, "=rm"), компилятор выбирает наиболее эффективный в зависимости от текущего контекста. Если вы указываете все возможные варианты, разрешаемые оператором asm, вы даёте оптимизаторам возможность создавать наилучший код. Если вам нужен конкретный регистр, но ваши ограничения машины не дают достаточного управления для выбора нужного регистра, локальные переменные регистра могут дать решение (см. Указание регистров для локальных переменных).

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.48.2.4 Выходные операнды флагов

Некоторые целевые системы имеют специальный регистр, хранящий «флаги» результата операции или сравнения. Обычно содержимое этого регистра либо не изменяется инструкцией asm, либо операция asm считается как изменяющая содержимое.

В некоторых целевых системах существует специальная форма выходного операнда, с помощью которой значения флагов регистра флагов могут быть результатом инструкции asm. Набор поддерживаемых условий зависит от целевой системы, но общее правило заключается в том, что переменная вывода должна быть целочисленной скалярной величиной, а значение — булевым. При поддержке этой функции целевая система определяет препроцессорный символ __GCC_ASM_FLAG_OUTPUTS__.

Из-за специфики выходных операндов флагов, ограничение не должно включать альтернативы.

Чаще всего целевая система имеет только один регистр флагов, который является неявным операндом многих инструкций. В этом случае операнд не должен ссылаться в шаблоне ассемблера через %0 и т.д., так как соответствующего текста в языке ассемблера нет.

ARM
AArch64

Ограничения на выходные операнды флагов для семейства ARM имеют вид ‘=@cccond’, где cond — одно из стандартных условий, определённых в ARM ARM для ConditionHolds.

eq

Флаг Z установлен, или равно

ne

Флаг Z сброшен, или не равно

cs
hs

Флаг C установлен, или беззнаковое больше или равно

cc
lo

Флаг 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

флаг переноса установлен

e
z

«равно» или флаг нуля установлен

g

со знаком больше

ge

со знаком больше или равно

l

со знаком меньше

le

со знаком меньше или равно

o

флаг переполнения установлен

p

флаг чётности установлен

s

флаг знака установлен

na
nae
nb
nbe
nc
ne
ng
nge
nl
nle
no
np
ns
nz

«не» флаг, или инвертированные версии вышеперечисленных

s390

Ограничение выходного операнда флага для s390 — ‘=@cc’. Разрешено только одно такое ограничение. Переменная должна храниться в переменной типа ‘int’.

6.48.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.48.2.6 Clobbers и регистры-временные значения

Хотя компилятор осведомлён о изменениях в записях, указанных в выходных операндах, встроенный asm код может изменять не только выходы. Например, вычисления могут потребовать дополнительных регистров, или процессор может перезаписать регистр в качестве побочного эффекта конкретной инструкции ассемблера. Для того, чтобы проинформировать компилятор об этих изменениях, перечислите их в списке clobber. Элементы списка clobber — это либо имена регистров, либо специальные clobber (перечислены ниже). Каждый элемент списка clobber — это строковая константа, заключённая в двойные кавычки и разделённая запятыми.

Описание clobber не должно никоим образом перекрываться с операндами ввода или вывода. Например, у вас не может быть операнда, описывающего класс регистров с одним членом, при одновременном указании этого регистра в списке clobber. Переменные, объявленные для размещения в определённых регистрах (см. Переменные в указанных регистрах) и используемые в качестве asm операндов ввода или вывода, не должны упоминаться в описании clobber. В частности, нет способа указать, что операнды ввода изменяются, не указав их также в качестве операндов вывода.

Когда компилятор выбирает регистры для представления операндов ввода и вывода, он не использует ни один из регистров clobber. В результате регистры clobber доступны для любого использования в коде ассемблера.

Другое ограничение состоит в том, что список clobber не должен содержать регистр стека. Это связано с тем, что компилятору необходимо, чтобы значение регистра стека было таким же после asm инструкции, как и при входе в неё. Однако предыдущие версии GCC не обеспечивали соблюдение этого правила и позволяли регистру стека появляться в списке с неясными семантиками. Это поведение устарело, и указание регистра стека может стать ошибкой в будущих версиях GCC.

Вот реальный пример для VAX, демонстрирующий использование регистров clobber:

asm volatile ("movc3 %0, %1, %2"
                   : /* No outputs. */
                   : "g" (from), "g" (to), "g" (count)
                   : "r0", "r1", "r2", "r3", "r4", "r5", "memory");

Также есть два специальных аргумента clobber:

"cc"

"cc" clobber указывает, что код ассемблера изменяет регистр флагов. На некоторых машинах GCC представляет коды условий как определённый регистр аппаратного обеспечения; "cc" служит для наименования этого регистра. На других машинах обработка кодов условий отличается, и указание "cc" не оказывает никакого эффекта. Но оно допустимо независимо от целевой платформы.

"memory"

"memory" clobber сообщает компилятору, что код ассемблера выполняет чтение или запись в память элементов, кроме тех, которые указаны в операндах ввода и вывода (например, доступ к памяти, на которую указывает один из входных параметров). Для обеспечения корректности содержимого памяти GCC может потребоваться сбросить определённые значения регистров в память перед выполнением asm. Кроме того, компилятор не предполагает, что любые значения, считанные из памяти до asm, останутся неизменными после этой asm; он перезагружает их по мере необходимости. Использование "memory" clobber эффективно формирует барьер для чтения/записи в памяти для компилятора.

Обратите внимание, что этот clobber не предотвращает процессору от выполнения упреждающего чтения за пределами 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 на языке ассемблера, включая clobber регистров векторов и кодов условий, а также некоторых инициализированных регистров смещения, которые не изменяются 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");
}

Вместо выделения фиксированных регистров через clobbers для предоставления регистров-временных значений для asm инструкции, альтернативой является объявление переменной и её использование как early-clobber output, как в a2 и a3 в примере ниже. Это даёт компилятору планировщику регистров большую свободу. Вы также можете объявить переменную и сделать её выходной, привязанной к входу, как в a0 и a1, привязанных соответственно к ap и lda. Конечно, при привязанных выходах ваш asm не может использовать значение входа после изменения регистра вывода, так как они являются одним и тем же регистром. Более того, если вы опустите early-clobber для выхода, возможно, GCC выделит тот же регистр для другого входа, если GCC может доказать, что они имели одинаковое значение при входе в asm. Вот почему a1 имеет early-clobber. Его привязанный вход, lda возможно, известен как имеющий значение 16, и без early-clobber может разделить один и тот же регистр с %11. С другой стороны, ap не может быть таким же, как любой из других входов, поэтому early-clobber для a0 не требуется. В этом случае это также нежелательно. Early-clobber для a0 заставит GCC выделить отдельный регистр для "m" (*(const double (*)[]) ap) ввода. Обратите внимание, что привязка входа к выходу — это способ настроить инициализированный временный регистр, изменяемый asm инструкцией. Вход, не связанный с выходом, считается GCC неизменным, например, "b" (16) ниже устанавливает %11 в 16, и GCC может использовать этот регистр в последующем коде, если значение 16 потребуется. Вы даже можете использовать обычный asm выходной параметр для временного регистра, если все входы, которые могут использовать один и тот же регистр, будут использованы до того, как будет использован временный регистр. Регистры VSX, clobbered инструкцией 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.48.2.7 Метки перехода

asm goto позволяет коду ассемблера переходить к одной или нескольким меткам C. Раздел GotoLabels в инструкции asm goto содержит список всех меток C, к которым может переходить код ассемблера, разделенные запятыми. GCC предполагает, что asm выполнение переходит к следующей инструкции (если это не так, используйте __builtin_unreachable атрибут после asm инструкции). Оптимизация asm goto может быть улучшена с помощью атрибутов меток hot и cold (см. Атрибуты меток).

Если код ассемблера изменяет что-либо, используйте "memory" модификатор для принудительной очистки всех значений регистров в памяти и повторной загрузки их при необходимости после инструкции asm.

Обратите внимание, что инструкция asm goto всегда неявно считается изменяемой.

Будьте осторожны, когда устанавливаете выходные операнды внутри asm goto только на некоторых возможных путях управления потоком. Если вы не устанавливаете выходной операнд на данном пути и никогда не используете его на этом пути, это нормально. В противном случае, используйте модификатор ограничения ‘+’, означающий, что операнд является входным и выходным. С этим модификатором у вас будут правильные значения на всех возможных путях от asm goto.

Для ссылки на метку в шаблоне ассемблера добавьте префикс ‘%l’ (строчная буква ‘L’), после чего следует ее (нумерация с нуля) позиция в GotoLabels плюс количество входных и выходных операндов. Выходной операнд с модификатором ограничения ‘+’ считается как два операнда, так как он рассматривается как один выходной и один входной операнд. Например, если в asm есть три входа, один выходной операнд с модификатором ограничения ‘+’ и один выходной операнд с модификатором ограничения ‘=’ и есть ссылки на две метки, первая метка обозначается как ‘%l6’, а вторая — как ‘%l7’.

В качестве альтернативы, вы можете ссылаться на метки, используя фактическое имя 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;
}

Следующий пример демонстрирует asm goto, использующий выходной операнд.

int foo(int count)
{
  asm goto ("dec %0; jb %l[stop]"
            : "+r" (count)
            :
            :
            : stop);
  return count;
stop:
  return 0;
}

Следующий искусственный пример демонстрирует asm goto , устанавливающий выходной операнд только на одном пути внутри asm goto. Использование модификатора ограничения = вместо + будет неверным, так как factor используется на всех путях от asm goto.

int foo(int inp)
{
  int factor = 0;
  asm goto ("cmp %1, 10; jb %l[lab]; mov 2, %0"
            : "+r" (factor)
            : "r" (inp)
            :
            : lab);
lab:
  return inp * factor; /* return 2 * inp or 0 if inp < 10 */
}

6.48.2.8 Общие модификаторы операндов

В следующей таблице показаны поддерживаемые всеми целевыми платформами модификаторы и их эффекты:

Модификатор Описание Пример
c Требует константного операнда и выводит выражение константы без знаков препинания. %c0
n Аналогично ‘%c’, за исключением того, что значение константы инвертируется перед выводом. %n0
a Заменяет ссылку на память, где фактический операнд обрабатывается как адрес. Это может быть полезно при выводе инструкции «загрузки адреса», так как синтаксис ассемблера для такой инструкции часто требует записи операнда так, как будто это ссылка на память. %a0
l Выводит имя метки без знаков препинания. %l0

6.48.2.9 Модификаторы операндов AArch64

В следующей таблице показаны поддерживаемые AArch64 модификаторы и их эффекты:

Модификатор Описание
w Выводит имя 32-битного общего регистра или, в случае константного операнда 0, 32-битный нулевой регистр (wzr).
x Выводит имя 64-битного общего регистра или, в случае константного операнда 0, 64-битный нулевой регистр (xzr).
b Выводит имя регистра FP/SIMD с префиксом b (байт, 8 бит).
h Выводит имя регистра FP/SIMD с префиксом h (полуслово, 16 бит).
s Выводит имя регистра FP/SIMD с префиксом s (одно слово, 32 бита).
d Выводит имя регистра FP/SIMD с префиксом d (двойное слово, 64 бита).
q Выводит имя регистра FP/SIMD с префиксом q (четверное слово, 128 бит).
Z Выводит имя регистра FP/SIMD как регистра SVE (т. е. с префиксом z). Это ничто для операндов регистра SVE.

6.48.2.10 Модификаторы операндов 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
%4 $8 8
%5 %xmm0 xmm0
%7 $0 0

В следующей таблице представлен список поддерживаемых модификаторов и их эффекты.

Модификатор Описание Операнд ‘att’ ‘intel’
A Вывести абсолютную ссылку на память. %A0 *%rax rax
b Вывести имя регистра в QImode. %b0 %al al
B Вывести суффикс кода операции b. %B0 b
c Требовать константный операнд и вывести выражение константы без знаков препинания. %c1 2 2
d Вывести дублированный операнд регистра для инструкции AVX. %d5 %xmm0, %xmm0 xmm0, xmm0
E Вывести адрес в режиме Double Integer (DImode) (8 байт) при 64-битном целевом процессоре. В противном случае режим не определён (VOIDmode). %E1 %(rax) [rax]
g Вывести имя регистра в V16SFmode. %g0 %zmm0 zmm0
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
L Вывести суффикс кода операции l. %L0 l
N Вывести maskz. %N7 {z} {z}
p Вывести исходное имя символа (без синтаксически-специфичных префиксов). %p2 42 42
P Если используется для функции, вывести суффикс PLT и сгенерировать PIC-код. Например, вывести foo@PLT вместо ‘foo’ для функции foo(). Если используется для константы, удалить все синтаксически-специфичные префиксы и вывести константу без префиксов. См. p выше.
q Вывести имя регистра в DImode. %q0 %rax rax
Q Вывести суффикс кода операции q. %Q0 q
R Вывести встраиваемые округления и sae. %R4 {rn-sae}, , {rn-sae}
r Вывести только sae. %r4 {sae}, , {sae}
s Вывести сдвиг двойного счётчика, за которым следует разделитель аргументов ассемблера. Вывести суффикс кода операции s. %s1 $2, 2,
S Вывести суффикс кода операции s. %S0 s
t Вывести имя регистра в V8SFmode. %t5 %ymm0 ymm0
T Вывести суффикс кода операции t. %T0 t
V Вывести полное имя целочисленного регистра без символа «%». %V0 eax eax
w Вывести имя регистра в HImode. %w0 %ax ax
W Вывести суффикс кода операции w. %W0 w
x Вывести имя регистра в V4SFmode. %x5 %xmm0 xmm0
y Вывести «st(0)» вместо «st» как регистр. %y6 %st(0) st(0)
z Вывести суффикс кода операции для размера текущего целочисленного операнда (один из b/w/l/q). %z0 l
Z Как z, со специальными суффиксами для инструкций x87.

6.48.2.11 Операторы x86 с плавающей точкой

На целевых платформах x86 существует несколько правил использования регистров стека в операндах asm. Эти правила применяются только к операндам, которые являются регистрами стека:

  1. Учитывая набор входных регистров, которые умирают в asm, необходимо знать, какие из них неявно извлекаются из стека asm, а какие должны быть явно извлечены компилятором GCC.

    Входной регистр, который неявно извлекается из стека asm, должен быть явно объявлен как измененный, если только он не ограничен соответствием выходному операнду.

  2. Для любого входного регистра, который неявно извлекается из стека asm, необходимо знать, как скорректировать стек, чтобы компенсировать извлечение. Если любой неивлучаемый регистр ближе к вершине стека, чем регистр, который неявно извлекается из стека, невозможно определить, как выглядел стек — неясно, как остальная часть стека «сдвигается вверх».

    Все неявно извлекаемые входные регистры должны быть ближе к вершине стека, чем любые входные регистры, которые не извлекаются неявно.

    Возможно, если входной регистр умирает в asm, компилятор может использовать входной регистр для перезагрузки выходного значения. Рассмотрим этот пример:

    asm ("foo" : "=t" (a) : "f" (b));

    Этот код говорит, что входной b не извлекается asm, и что asm помещает результат в стек, т.е. стек на один уровень глубже после asm, чем до него. Однако, возможно, модуль перезагрузки может решить, что он может использовать тот же регистр как для входа, так и для выхода.

    Чтобы предотвратить это, если любой входной операнд использует ограничение «f», все ограничения выходных регистров должны использовать модификатор «&» для раннего изменения.

    Пример выше должен быть написан следующим образом:

    asm ("foo" : "=&t" (a) : "f" (b));
  3. Некоторые операнды должны находиться в определенных местах стека. Все выходные операнды попадают в эту категорию — GCC не может узнать, в каких регистрах находятся выходные значения, если вы не укажете это в ограничениях.

    Выходные операнды должны четко указывать, в какой регистр попадает выходное значение после asm. ‘=f’ недопустимо: ограничения операндов должны выбирать класс с одним регистром.

  4. Выходные операнды не могут быть «вставлены» между существующими регистрами стека. Поскольку ни одна 387-команда не использует операнд чтения/записи, все выходные операнды уничтожаются до asm, и помещаются в стек asm. Нет смысла помещать их куда-то, кроме вершины стека.

    Выходные операнды должны начинаться с вершины стека: выходные операнды не могут «пропускать» регистры.

  5. Некоторые 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)");

6.48.2.12 Модификаторы операндов MSP430

Ниже описаны поддерживаемые модификаторы и их влияние для MSP430.

Модификатор Описание
A Выбрать младшие 16 бит операнда (константа/регистр/память).
B Выбрать старшие 16 бит операнда (константа/регистр/память).
C Выбрать биты 32-47 операнда (константа/регистр/память).
D Выбрать биты 48-63 операнда (константа/регистр/память).
H Эквивалентно B (для обратной совместимости).
I Распечатать обратное (логическое NOT) значение константы.
J Распечатать целое число без префикса #.
L Эквивалентно A (для обратной совместимости).
O Смещение текущей области памяти от вершины стека.
Q Использовать постфикс команды A.
R Обратное условие кода, для беззнаковых сравнений.
W Вычесть 16 из значения константы.
X Использовать постфикс команды X.
Y Вычесть 4 из значения константы.
Z Вычесть 1 из значения константы.
b Добавить .B, .W или .A к инструкции в зависимости от режима.
d Смещение на 1 байт памяти или значения константы.
e Смещение на 3 байта памяти или значения константы.
f Смещение на 5 байт памяти или значения константы.
g Смещение на 7 байт памяти или значения константы.
p Вывести значение 2, возведенное в степень заданной константы. Используется для выбора позиции бита.
r Обратное условие кода, для знаковых сравнений.
x Эквивалентно X, но только для указателей.

6.48.2.13 Модификаторы операндов LoongArch

Ниже описаны поддерживаемые модификаторы и их влияние для LoongArch.

Модификатор Описание
d То же, что и c.
i Вывести символ ”i”, если операнд не является регистром.
m То же, что и c, но выводимое значение operand - 1.
u Вывести регистр LASX.
w Вывести регистр LSX.
X Вывести целочисленный операнд в шестнадцатеричном формате.
z Вывести операнд в неизменённом виде, после чего добавить запятую.

Ссылки на входные и выходные операнды в шаблоне ассемблера расширенных ассемблерных инструкций могут использовать модификаторы для влияния на способ форматирования операндов в генерируемом ассемблерном коде. Например, следующий код использует модификатор ‘w’ для LoongArch:

test-asm.c:

#include <lsxintrin.h>

__m128i foo (void)
{
__m128i  a,b,c;
__asm__ ("vadd.d %w0,%w1,%w2\n\t"
   :"=f" (c)
   :"f" (a),"f" (b));

return c;
}

Команда компиляции для тестового случая выглядит следующим образом:

gcc test-asm.c -mlsx -S -o test-asm.s

Ассемблерная инструкция генерирует следующий ассемблерный код:

vadd.d $vr0,$vr0,$vr1

Это инструкция 128-битного векторного сложения, c (указано в строке шаблона как %0) — выходное значение, а a (%1) и b (%2) — входные значения. __m128i — тип данных для векторов, определённый в файле lsxintrin.h (см. Векторные инструкции SX для LoongArch). Символ ‘=f’ обозначает ограничение, использующее регистр с плавающей точкой для выходного типа, а ‘f’ в входном операнде обозначает ограничение, использующее регистр с плавающей точкой для операнда, что может ссылаться на определение ограничения (см. Ограничения для asm операндов) в gcc.

6.48.2.14 Модификаторы операндов RISC-V

Ниже описаны поддерживаемые модификаторы и их влияние для RISC-V.

Модификатор Описание
z Вывести ”zero” вместо 0, если операнд — непосредственное значение с нулевым значением.
i Вывести символ ”i”, если операнд — непосредственное значение.

© Free Software Foundation
Licensed under the GNU Free Documentation License, Version 1.3.
https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Extended-Asm.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API