Inline assembly¶
Inline assembly (встроенный ассемблер) — это синтаксическая конструкция в языках программирования высокого уровня (прежде всего C и C++), позволяющая вставлять фрагменты кода на языке ассемблера непосредственно в исходный текст программы. Такая возможность даёт разработчику прямой доступ к машинным инструкциям процессора, регистрам и флагам, минуя промежуточные слои генерации кода компилятором. Inline assembly используется для оптимизации критичных по производительности участков, реализации низкоуровневых операций (например, системных вызовов, управления прерываниями, атомарных инструкций) и взаимодействия с аппаратурой.
¶История
Возможность встраивать ассемблерный код появилась в ранних версиях компиляторов C, разработанных для операционной системы UNIX. В 1970-х годах компилятор C от Денниса Ритчи (Dennis Ritchie) позволял использовать ключевое слово asm для вставки машинного кода. Эта особенность была унаследована и расширена в последующих реализациях, включая компиляторы от AT&T, GNU (GCC) и Microsoft.
Стандартизация inline assembly в языке C++ была предложена в стандарте C++11, но не была принята. Вместо этого компиляторы сохранили собственные, несовместимые друг с другом синтаксисы. Наиболее распространённые реализации — GNU C Compiler (GCC) и Microsoft Visual C++ (MSVC). В стандарте C23 (ISO/IEC 9899:2024) встроенный ассемблер не стандартизирован, но его поддержка остаётся обязательной для многих компиляторов в рамках расширений.
¶Синтаксис и реализации
¶GNU C Compiler (GCC)
В GCC и совместимых компиляторах (Clang, ICC) используется расширенный синтаксис на основе ключевого слова __asm__ (или asm). Базовый формат:
``c __asm__ [volatile] ( "ассемблерные инструкции" : выходные операнды : входные операнды : разрушаемые регистры (clobbers) ); ``
- Ассемблерные инструкции — строка с командами AT&T-синтаксиса (по умолчанию) или Intel-синтаксиса (при указании
.intel_syntax noprefix). - Выходные операнды — переменные C/C++, в которые записываются результаты. Указываются с ограничениями (constraints), например
=r(любой регистр),=m(память). - Входные операнды — переменные, значения которых передаются в ассемблерный блок.
- Разрушаемые регистры — список регистров, которые модифицируются в ходе выполнения ассемблерного кода (например,
"eax","ecx","memory").
Пример для x86-64 (GCC, AT&T-синтаксис):
``c int a = 10, b = 20, result; __asm__ ( "addl %%ebx, %%eax" : "=a" (result) : "a" (a), "b" (b) ); // result = a + b ``
Ключевое слово volatile указывает компилятору не оптимизировать и не удалять ассемблерный блок, даже если его результат не используется.
¶Microsoft Visual C++ (MSVC)
В MSVC используется синтаксис с ключевым словом __asm (или _asm). Блок заключается в фигурные скобки:
``cpp __asm { mov eax, a add eax, b mov result, eax } ``
В MSVC inline assembly поддерживается только для 32-битных платформ (x86). Для 64-битных приложений (x64) встроенный ассемблер отсутствует; вместо него необходимо использовать отдельные ассемблерные файлы (MASM) или intrinsic-функции.
¶Другие компиляторы
- Clang (LLVM) — полностью совместим с синтаксисом GCC.
- Intel C++ Compiler (ICC) — поддерживает как GCC-стиль, так и собственный синтаксис.
- Rust — предоставляет макрос
asm!()(стабилизирован в Rust 1.59), который использует синтаксис, близкий к GCC, но с дополнительными проверками безопасности. - Free Pascal — поддерживает inline assembly через блоки
asm ... end;с синтаксисом AT&T или Intel.
¶Применение
¶Оптимизация производительности
Inline assembly позволяет писать узкоспециализированные фрагменты кода, которые компилятор не может сгенерировать автоматически. Например, ручное управление регистрами, использование инструкций SIMD (SSE, AVX) или обход стандартных соглашений вызова. Типичные сценарии:
- Криптография — реализация алгоритмов, требующих константного времени выполнения (например, AES, SHA).
- Обработка сигналов — работа с DSP-инструкциями (умножение с накоплением, насыщение).
- Ядра операционных систем — переключение контекста, обработка прерываний, управление MMU.
¶Системные вызовы
В операционных системах, таких как Linux, inline assembly используется для выполнения системных вызовов без использования библиотечных обёрток. Например, вызов write в Linux на x86-64:
``c __asm__ volatile ( "mov $1, %%rax\n" // номер системного вызова (sys_write) "mov %0, %%rdi\n" // файловый дескриптор "mov %1, %%rsi\n" // указатель на буфер "mov %2, %%rdx\n" // длина "syscall" : : "g" (fd), "g" (buf), "g" (count) : "rax", "rdi", "rsi", "rdx", "memory" ); ``
¶Атомарные операции
До появления стандартных атомарных типов в C++11 (std::atomic) inline assembly был единственным способом реализовать атомарные инструкции, такие как compare-and-swap (CAS) или fetch-and-add. Пример для x86:
``c int old_value; __asm__ __volatile__ ( "lock cmpxchgl %2, %1" : "=a" (old_value), "+m" (*ptr) : "r" (new_value), "a" (expected) : "memory" ); ``
¶Ограничения и критика
¶Переносимость
Inline assembly привязывает код к конкретной архитектуре процессора (x86, ARM, RISC-V) и синтаксису компилятора. Перенос программы на другую платформу требует полной переработки ассемблерных блоков. Это противоречит принципам переносимого программирования.
¶Безопасность
Ошибки в inline assembly могут привести к повреждению стека, регистров или памяти, что вызывает трудноотлаживаемые сбои. Компилятор не проверяет корректность ассемблерного кода, поэтому разработчик должен вручную соблюдать соглашения о вызовах, управлять стеком и избегать побочных эффектов.
¶Усложнение отладки
Смешанный код (C/C++ + ассемблер) сложнее анализировать в отладчиках, так как отладчик может не показывать исходный текст ассемблерных блоков или неправильно отображать значения регистров.
¶Альтернативы
Современные компиляторы предоставляют intrinsic-функции — встроенные функции, которые транслируются непосредственно в одну или несколько машинных инструкций, но сохраняют типобезопасность и переносимость. Например, в GCC и MSVC доступны __sync_fetch_and_add, _mm_add_epi32 (SSE) и __rdtsc. Использование intrinsic-функций рекомендуется вместо inline assembly во всех случаях, когда это возможно.
¶Примеры использования
¶Вычисление квадратного корня (x86, SSE)
```c
¶include <xmmintrin.h>
float sqrt_fast(float x) { float result; __asm__ ( "sqrtss %1, %0" : "=x" (result) : "x" (x) ); return result; } ```
¶Чтение счётчика тактов (RDTSC)
``c unsigned long long read_tsc() { unsigned int lo, hi; __asm__ __volatile__ ( "rdtsc" : "=a" (lo), "=d" (hi) ); return ((unsigned long long)hi << 32) | lo; } ``
¶Интересные факты
- В ранних версиях компилятора GCC (до 2.95) inline assembly поддерживал только AT&T-синтаксис. Поддержка Intel-синтаксиса была добавлена в GCC 3.x.
- В операционной системе Linux ядро активно использует inline assembly для реализации критических участков, таких как переключение контекста, обработка прерываний и синхронизация.
- В стандарте C++23 предложено расширение
std::asm, но оно не было принято. Вместо этого рекомендовано использоватьstd::atomicиstd::simd.
¶Источники
- GNU C Compiler (GCC) — документация по расширенному ассемблеру (Extended Asm)
- Microsoft Visual C++ — документация по
__asm(Inline Assembler) - Intel 64 and IA-32 Architectures Software Developer Manuals
- Linux kernel source code (arch/x86/include/asm/)
- ISO/IEC 9899:2024 (C23) — стандарт языка C
- ISO/IEC 14882:2020 (C++20) — стандарт языка C++