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 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;
}
  • код: додати %1 до %0 і зберегти результат в %0
  • вихідні параметри: універсальний регістр, збережений в локальну змінну, після виконання асемблерного кода.
  • вхідні параметри: універсальні регістри, ініціалізовані від локальних змінних x та y перед виконанням асемблерного коду.
  • змінювані параметри: нічого, крім регістрів вводу-виводу.

Примітки[ред. | ред. код]

  1. Викиучебник: Ассемблер в Linux для программистов C. Архів оригіналу за 26 квітня 2022. Процитовано 6 травня 2022. 

Посилання[ред. | ред. код]