В компьютерном программировании встроенный ассемблер — это функция некоторых компиляторов , которая позволяет встраивать низкоуровневый код, написанный на языке ассемблера , в программу, среди кода, который в противном случае был бы скомпилирован из языка более высокого уровня, такого как C или Ada .
Внедрение кода на языке ассемблера обычно выполняется по одной из следующих причин: [1]
С другой стороны, встроенный ассемблер создает прямую проблему для самого компилятора, поскольку он усложняет анализ того, что делается с каждой переменной, ключевой частью распределения регистров. [2] Это означает, что производительность может фактически снизиться. Встроенный ассемблер также усложняет будущее портирование и обслуживание программы. [1]
Альтернативные возможности часто предоставляются как способ упростить работу как для компилятора, так и для программиста. Встроенные функции для специальных инструкций предоставляются большинством компиляторов, а оболочки C-функций для произвольных системных вызовов доступны на каждой платформе Unix .
Стандарт ISO C++ и стандарты ISO C (приложение J) определяют условно поддерживаемый синтаксис для встроенного ассемблера:
Объявление asm имеет форму
asm-declaration :
asm ( string-literal );
Объявление asm поддерживается условно; его значение определяется реализацией. [3]
Однако это определение редко используется в реальном языке C, поскольку оно одновременно слишком либерально (в интерпретации) и слишком ограничено (в использовании только одного строкового литерала).
На практике встроенный ассемблер, работающий со значениями, редко бывает автономным как свободно плавающий код. Поскольку программист не может предсказать, какому регистру назначена переменная, компиляторы обычно предоставляют способ подставить их в качестве расширения.
В целом, компиляторы C/C++ поддерживают два типа встроенного ассемблера:
Два семейства расширений представляют различные понимания разделения труда при обработке встроенного ассемблера. Форма GCC сохраняет общий синтаксис языка и разделяет то, что должен знать компилятор: что необходимо и что изменено. Она явно не требует от компилятора понимания имен инструкций, поскольку компилятору нужно только заменить свои назначения регистров, плюс несколько операций mov , чтобы обработать входные требования. Однако пользователь склонен неправильно указывать затираемые регистры. Форма MSVC встроенного доменно-специфического языка обеспечивает простоту написания, но она требует от самого компилятора знания имен опкодов и их свойств затирания, что требует дополнительного внимания при обслуживании и портировании. [7] Все еще возможно проверить ассемблер в стиле GCC на наличие ошибок затирания, зная набор инструкций. [8]
GNAT (интерфейс языка Ada для пакета GCC) и LLVM используют синтаксис GCC. [9] [10] Язык программирования D использует DSL, аналогичный расширению MSVC, официально предназначенному для x86_64, [11] но LDC на основе LLVM также предоставляет синтаксис в стиле GCC для каждой архитектуры. [12] MSVC поддерживает только встроенный ассемблер на 32-битной платформе x86. [5]
С тех пор язык Rust перешел на синтаксис, абстрагирующий встроенные опции сборки дальше, чем версия LLVM (стиль GCC). Он предоставляет достаточно информации, чтобы преобразовать блок во внешне собранную функцию, если бэкенд не может справиться со встроенной сборкой. [7]
Вызов операционной системы напрямую, как правило, невозможен в системе, использующей защищенную память. ОС работает на более привилегированном уровне (режим ядра), чем пользовательский (пользовательский режим); для выполнения запросов к операционной системе используется (программное) прерывание . Это редко встречается в языке более высокого уровня, поэтому функции-оболочки для системных вызовов пишутся с использованием встроенного ассемблера.
Следующий пример кода C показывает оболочку системного вызова x86 в синтаксисе ассемблера AT&T , использующую GNU Assembler . Такие вызовы обычно пишутся с помощью макросов; полный код включен для ясности. В этом конкретном случае оболочка выполняет системный вызов числа, указанного вызывающим, с тремя операндами, возвращая результат. [13]
Подводя итог, GCC поддерживает как базовый , так и расширенный ассемблер. Первый просто передает текст дословно ассемблеру, а последний выполняет некоторые замены для местоположений регистров. [4]
extern int errno ; int syscall3 ( int num , int arg1 , int arg2 , int arg3 ) { int res ; __asm__ ( "int $0x80" /* выполнить запрос к ОС */ : "=a" ( res ), /* вернуть результат в eax ("a") */ "+b" ( arg1 ), /* передать arg1 в ebx ("b") [как вывод "+", поскольку системный вызов может его изменить] */ "+c" ( arg2 ), /* передать arg2 в ecx ("c") [то же самое] */ "+d" ( arg3 ) /* передать arg3 в edx ("d") [то же самое] */ : "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 x86 ( x87 ) .
// Вычислить тангенс 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 ; // если (!(fp_status & 0x20)) перейти к Lret jnp Lret ; // C2 = 1: x выходит за пределы диапазона, выполнить сокращение аргумента fldpi ; // загрузить pi fxch ; SC17 : fprem1 ; // напоминание (частичное) fstsw AX ; sahf ; jp SC17 ; // C2 = 1: частичное напоминание, нужно зациклить fstp ST ( 1 ) ; // удалить pi из стека jmp SC18 ; } trigerr : return real . nan ; Lret : // Нет необходимости вручную возвращать что-либо, так как значение уже находится в стеке FP ; }
Для читателей, незнакомых с программированием x87, fstsw-sahf , за которым следует идиома условного перехода, используется для доступа к битам C0 и C2 слова состояния FPU x87. fstsw сохраняет статус в регистре общего назначения; sahf устанавливает регистр FLAGS в старшие 8 бит регистра; а переход используется для оценки любого бита флага, который соответствует биту состояния FPU. [14]
Однако можно реализовать поддержку встроенного ассемблера без поддержки со стороны бэкэнда компилятора, используя вместо этого внешний ассемблер.Запрос на отслеживание статуса
Форма FNSTSW AX инструкции используется в основном в условном переходе...