20-10-2023
GCC Inline Assembly — Встроенный ассемблер компилятора GCC, представляющий собой язык макроописания интерфейса компилируемого высокоуровнего кода с ассемблерной вставкой.
Содержание |
Синтаксис и семантика GCC Inline Assembly имеет следующие существенные отличия:
Для того, чтобы хорошо понимать, как работает 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; }
Если мы скажем выполним «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 используют синтаксис AT&T, который существенно отличается от ассемблера Intel. В Вики-учебнике есть статья об этом [1], поэтому законспектируем только основные факты, которые необходимо знать.
Итак, краткий конспект:
Обычно игнорируемый факт того, что внутри директивы 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
Для чего в случае asm служит ключевое слово volatile? Для того чтобы указать компилятору, что вставляемый ассемблерный код может давать побочные эффекты, поэтому попытки оптимизации могут привести к логическим ошибкам.
Случаи, когда ключевое слово volatile ставить обязательно:
Допустим, внутри цикла имеется ассемблерная вставка, производящая проверку на занятость глобальной переменной и ожидания в спинлоке её освобождения. Когда компилятор начинает оптимизировать цикл, он выкидывает из цикла все, что явным образом в цикле не изменяется. Поскольку в случае спинлока оптимизирующий компилятор не видит явной зависимости между параметрами ассемблерной вставки и переменными, изменяющимися в цикле, ассемблерная вставка может быть выкинута из цикла со всеми вытекающими последствиями.
СОВЕТ: Всегда указывайте asm volatile в тех случаях, когда ваша ассемблерная вставка должна «стоять там где стоит». Особенно это касается тех случаев, когда вы работаете с атомарными примитивами.
Следующий «тонкий момент» — явное указание «memory» в clobber list. Помимо простого указания компилятору, что ассемблерная вставка изменяет содержимое памяти, она еще служит директивой Memory Barrier для компилятора. Что это означает? Это означает что те операции обращений в память, которые стоят выше по коду, в результирующем машинном коде будут выполняться до тех, которые стоят ниже ассемблерной вставки. В случае многопоточной среды, когда от этого напрямую зависит риск возникновения race condition это обстоятельство является существенным.
СОВЕТ № 1:
Быстрый способ сделать Memory Barier
#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; }
GCC Inline Assembly.