GCC Inline Assembly

GCC Inline Assembly — Встроенный ассемблер компилятора GCC, представляющий собой язык макроописания интерфейса компилируемого высокоуровневого кода с ассемблерной вставкой.

Особенности

править

Синтаксис и семантика GCC Inline Assembly имеет следующие существенные отличия:

  • GCC никак не интерпретирует содержимое ассемблерной вставки.
  • Служит явное описание интерфейса с ассемблерной вставкой.
  • Даёт компилятору возможность свободы выбора регистров.
  • Позволяет явно указать на имеющиеся побочные действия ассемблерного кода.
  • Позволяет использовать все инструкции (и директивы тоже) которые распознает ассемблер, а не только те, что знает и применяет gcc

Предварительные сведения

править

Для понимания работы GCC Inline Assembly, следует хорошо понимать действия, выполняемые в процессе компиляции.

В начале gcc вызывает препроцессор cpp, который включает заголовочные файлы, разворачивает все условные директивы и выполняет макроподстановки. Посмотреть, что получилось после макроподстановки, можно командой gcc -E -o preprocessed.c some_file.c. Ключ -E редко используется, в основном когда вы занимаетесь отладкой макросов.

Затем gcc анализирует полученный код, на этой же фазе производит оптимизацию кода и в итоге производит ассемблерный код. Увидеть сгенерированный ассемблерный код можно командой gcc -S -o some_file.S some_file.c.

Затем gcc вызывает ассемблер gas для того, чтобы он создал из ассемблерного кода объектный код. Обычно ключ -c (compile only) используется в проектах, состоящих из многих файлов.

Затем gcc вызывает компоновщик ld для сборки исполняемого файла из полученных объектных файлов.

Для иллюстрации данного процесса создадим файл test.c следующего содержания:

int main()
 {
 asm ("Bla-Bla-Bla"); // вставим такую инструкцию
 return 0;
 }

Если при компиляции выдается предупреждение -Wimplicit-function-declaration "Неявная декларация функции asm", используйте:

 __asm__ ("Bla-Bla-Bla");

Если мы скажем выполнить gcc -S -o test.S test.c, то мы обнаружим важный факт: компилятор обработал «неправильную» инструкцию и результирующий ассемблерный файл test.S содержит нашу строку «Bla-Bla-Bla». Однако, если мы попробуем создать объектный код или собрать бинарный файл, то gcc выведет следующее:

test.c: Assembler messages: test.c:3: Error: no such instruction: 'Bla-Bla-Bla'

Сообщение исходит именно от Ассемблера.

Отсюда следует важный вывод: GCC никак не интерпретирует содержимое ассемблерной вставки, воспринимая её как макроподстановку времени компиляции.

Синтаксис

править

Общая структура

править

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

asm [volatile] («команды и директивы ассемблера» : выходные параметры : входные параметры : изменяемые параметры);

впрочем, существует и более короткая форма:

asm [volatile] («команды ассемблера»);

Синтаксис команд

править

Особенностью ассемблера gas и компилятора gcc является тот факт, что они используют непривычный для x86 синтаксис AT&T, который существенно отличается от синтаксиса Intel. Основные отличия[1]:

  1. Порядок операндов: Операция Источник,Приёмник.
  2. Названия регистров имеют явный префикс %, указывающий, что это регистр. Это позволяет работать с переменными, которые имеют то же название, что и какой-либо регистр, что невозможно в Intel-синтаксисе, у которого префиксы для регистров не используются, а их названия являются зарезервированными ключевыми словами.
  3. Явное задание размеров операндов в суффиксах команд: b-byte, w-word, l-long, q-quadword. В командах типа movl %edx,%eax это может показаться излишним, однако является весьма наглядным средством, когда речь идёт о incl (%esi) или xorw $0x7,mask
  4. Названия констант начинаются с $ и могут быть выражением. Например movl $1,%eax
  5. Значение без префикса означает адрес. Например:
    movl $123,%eax — записать в %eax число 123,
    movl 123,%eax — записать в %eax содержимое ячейки памяти с адресом 123,
    movl var,%eax — записать в %eax значение переменной var,
    movl $var,%eax — загрузить адрес переменной var
  6. Для косвенной адресации необходимо использовать круглые скобки. Например movl (%ebx),%eax — загрузить в %eax значение переменной по адресу, находящемуся в регистре %ebx
  7. SIB-адресация: смещение (база, индекс, множитель)

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

 asm(
  "our_data_file:\n\t"
  ".incbin \"some_bin_file.txt\"\n\t" // используем директиву .incbin
  "our_data_file_len:\n\t"
  ".long .-our_data_file\n\t"  // вставляем значение .long с вычисленной длиной файла
  );

И затем адресоваться к этому бинарному файлу:

extern char our_data_file[];
extern long our_data_file_len;

Как работает макроподстановка

править

Рассмотрим, как происходит подстановка.

Конструкция:

asm ("movl %0,%%eax"::"i"(1));

Превратится в

movl $1,%eax

Входные и выходные параметры

править

Модификаторы

править

Тонкие моменты

править

Ключевое слово volatile

править

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

Случаи, когда ключевое слово volatile ставить обязательно:

Допустим, внутри цикла имеется ассемблерная вставка, производящая проверку на занятость глобальной переменной и ожидания в спинлоке её освобождения. Когда компилятор начинает оптимизировать цикл, он выкидывает из цикла всё, что явным образом в цикле не изменяется. Поскольку в этом случае оптимизирующий компилятор не видит явной зависимости между параметрами ассемблерной вставки и переменными, изменяющимися в цикле, ассемблерная вставка может быть выкинута из цикла со всеми вытекающими последствиями.

СОВЕТ: Всегда указывайте asm volatile в тех случаях, когда ваша ассемблерная вставка должна «стоять там, где стоит». Особенно это касается тех случаев, когда вы работаете с атомарными примитивами.

«memory» в clobber list

править

Следующий «тонкий момент» — явное указание «memory» в clobber list. Помимо простого указания компилятору, что ассемблерная вставка изменяет содержимое памяти, она ещё служит директивой Memory Barrier для компилятора. Это означает, что те операции обращений в память, которые стоят выше по коду, в результирующем машинном коде будут выполняться до тех, которые стоят ниже ассемблерной вставки. В случае многопоточной среды, когда от этого напрямую зависит риск возникновения race condition, это обстоятельство является существенным.

СОВЕТ № 1:

Быстрый способ сделать Memory Barrier

#define mbarrier() asm volatile ("":::"memory")

СОВЕТ № 2: Указание «memory» в clobber list не только «хороший тон», но и в случае работы с атомарными операциями, призванными разрулить race condition, является обязательным.

Примеры использования

править
int main()
{
  int sum = 0, x = 1, y = 2;
  asm ( "add %1, %0" : "=r" (sum) : "r" (x), "0" (y) ); // sum = x + y;
  printf("sum = %d, x = %d, y = %d", sum, x, y); // sum = 3, x = 1, y = 2
  return 0;
}
  • код: добавить %1 к %0 и сохранить результат в %0
  • выходные параметры: универсальный регистр, сохранённый в локальную переменную, после выполнения ассемблерного кода.
  • входные параметры: универсальные регистры, инициализированные от локальных переменных x и y перед выполнением ассемблерного кода.
  • изменяемые параметры: ничего, кроме регистров ввода-вывода.

Примечания

править
  1. Викиучебник: Ассемблер в Linux для программистов C. Дата обращения: 8 мая 2022. Архивировано 26 апреля 2022 года.

Ссылки

править