В компьютерное программирование, встроенный ассемблер - это функция некоторых компиляторов, которая позволяет встраивать низкоуровневый код, написанный на языке ассемблера, в программу, среди кода, который в противном случае был скомпилирован из более высокого уровня. -уровневый язык, например, C или Ada.
Встраивание кода на ассемблере обычно выполняется по одной из трех причин:
С другой стороны, встроенный ассемблер создает прямая проблема для самого компилятора, поскольку это усложняет анализ того, что делается с каждой переменной, ключевой частью распределения регистров. Это означает, что производительность может снизиться. Встроенный ассемблер также усложняет будущий перенос и сопровождение программы.
Альтернативные средства часто предоставляются как способ упростить работу как для компилятора, так и для программиста. Внутренние функции для специальных инструкций предоставляются большинством компиляторов, а оболочки C-функций для произвольных системных вызовов доступны на каждой платформе Unix.
Стандарт ISO C ++ и стандарты ISO C (приложение J) определяют условно поддерживаемый синтаксис для встроенного ассемблера:
Объявление asm имеет форма. объявление-asm:. asm (строковый-литерал);. Объявление asm поддерживается условно; его значение определяется реализацией.
Это определение, однако, редко используется в реальном C, поскольку оно одновременно слишком либерально (в интерпретации) и слишком ограничено (в использовании только одного строкового литерала).
На практике встроенная сборка, работающая со значениями, редко бывает автономной в виде свободно плавающего кода. Поскольку программист не может предсказать, какому регистру присвоена переменная, компиляторы обычно предоставляют способ их замены в качестве расширения.
Как правило, компиляторы C / C ++ поддерживают два типа встроенной сборки:
Эти два семейства расширений представляют собой разные понимания разделения труда при обработке встроенной сборки. Форма GCC сохраняет общий синтаксис языка и разделяет то, что компилятору нужно знать: что необходимо и что нужно изменить. Он явно не требует, чтобы компилятор понимал имена инструкций, поскольку компилятору нужно только подставить в свои назначения регистров плюс несколько операций mov для обработки требований ввода. Форма MSVC встроенного предметно-ориентированного языка обеспечивает некоторую простоту написания, но требует, чтобы сам компилятор знал об именах кодов операций и их свойствах затирания, что требует особого внимания при обслуживании и переносе.
GNAT (интерфейс языка Ada из набора GCC), LLVM и язык программирования Rust используют синтаксис, аналогичный синтаксису GCC. Язык программирования D использует DSL, аналогичный расширению MSVC, официально для x86_64, но LDC на основе LLVM также предоставляет синтаксис в стиле GCC для каждой архитектуры.
С тех пор язык Rust перешел на синтаксис, абстрагирующийся от встроенных параметров сборки, кроме версии LLVM (в стиле GCC). Он предоставляет достаточно информации, чтобы можно было преобразовать блок во внешне собранную функцию, если бэкэнд не может обрабатывать встроенную сборку.
Вызов прямая операционная система обычно невозможна в системе, использующей защищенную память. ОС работает на более привилегированном уровне (режим ядра), чем пользовательский (пользовательский режим); (программное) прерывание используется для выполнения запросов к операционной системе. Это редко бывает в языках высокого уровня, поэтому функции-оболочки для системных вызовов написаны с использованием встроенного ассемблера.
В следующем примере кода C показана оболочка системного вызова x86 в синтаксисе ассемблера ATT с использованием GNU Assembler. Такие вызовы обычно пишутся с помощью макросов; полный код включен для ясности. В этом конкретном случае оболочка выполняет системный вызов номера, предоставленного вызывающей стороной, с тремя операндами, возвращая результат.
Напомним, GCC поддерживает как базовую, так и расширенную сборку. Первый просто передает текст ассемблеру дословно, а второй выполняет некоторые замены для местоположений регистров.
extern int errno; int syscall3 (int num, int arg1, int arg2, int arg3) {int res; __asm__ volatile ("int $ 0x80" / * сделать запрос к ОС * /: "= a" (res), / * вернуть результат в eax ("a") * / "+ b" (arg1), / * передать arg1 в ebx ("b") [как вывод "+", потому что системный вызов может его изменить] * / "+ c" (arg2), / * передать arg2 в ecx ("c") [то же самое] * / " + d "(arg3) / * передать arg3 в edx (" d ") [ditto] * /:" a "(num) / * передать номер системного вызова в eax (" a ") * /:" memory "," cc ", / * объявляет компилятору, что память и коды условий были изменены * /" esi "," edi "," ebp "); / * они тоже затираются * / / * Операционная система вернет отрицательное значение при ошибке; * оболочки возвращают -1 при ошибке и устанавливают глобальную переменную errno * / if (-125 <= res res < 0) { errno = -res; res = -1; } return res; }
Этот пример встроенной сборки из языка программирования D показывает код, который вычисляет тангенс x с помощью инструкций FPU (x87 ) x86.
// Вычислить тангенс x real tan ( real x) {asm {fld x [EBP]; // загрузка x fxam; // проверка нечетных значений fstsw AX; sahf; jc trigerr; // C0 = 1: x - NAN, бесконечность или пустая // банка 387 обрабатывать денормальные значения SC18: fptan; fstp ST (0); // дамп X, который всегда равен 1 fstsw AX; sahf; // if (! (fp_status 0x20)) goto Lret jnp Lret; // C2 = 1: x is вне допустимого диапазона, уменьшить аргумент fldpi; // загрузить pi fxch; SC17: fprem1; // напоминание (частичное) fstsw AX; sahf; jp SC17; // C2 = 1: частичное напоминание, необходимо выполнить цикл fstp ST (1) ; // удаляем число пи из стека jmp SC18;} trigerr: return real.nan; Lret:;}
Для читателей, не знакомых с программированием x87, fstsw-sahf, за которым следует conditio Идиома конечного перехода используется для доступа к битам C0 и C2 слова состояния FPU x87. fstsw сохраняет статус в регистре общего назначения; sahf устанавливает регистр FLAGS на старшие 8 бит регистра; и переход используется для определения того, какой бит флага соответствует биту состояния FPU.