В компьютерном программировании , встроенный ассемблер является особенностью некоторых компиляторов , что позволяет низкоуровневого код , написанный на языке ассемблера , чтобы быть внедрены в рамках программы, в том числе кода , который в противном случае было составлено из языка высокого уровня , таких как C или Ada .
Мотивация и альтернативы
Встраивание кода на ассемблере обычно выполняется по одной из трех причин: [1]
- Оптимизация : программисты могут использовать код на языке ассемблера для реализации наиболее чувствительных к производительности частей алгоритмов своей программы , код, который может быть более эффективным, чем тот, который в противном случае мог бы быть сгенерирован компилятором.
- Доступ к специфическим для процессора инструкциям : большинство процессоров предлагают специальные инструкции, такие как инструкции сравнения и замены и проверки и установки, которые могут использоваться для создания семафоров или других примитивов синхронизации и блокировки. Практически каждый современный процессор имеет эти или похожие инструкции, поскольку они необходимы для реализации многозадачности . Примеры специализированных инструкций можно найти в наборах инструкций SPARC VIS , Intel MMX и SSE , а также Motorola Altivec .
- Доступ к специальным соглашениям о вызовах, еще не поддерживаемым компилятором.
- Системные вызовы и прерывания: языки высокого уровня редко имеют прямую возможность выполнять произвольные системные вызовы, поэтому используется ассемблерный код. Прямые прерывания используются еще реже.
- Для создания специальных директив для компоновщика или ассемблера, например, для изменения секционирования, макросов или создания псевдонимов символов.
С другой стороны, встроенный ассемблер представляет собой прямую проблему для самого компилятора, поскольку усложняет анализ того, что делается с каждой переменной, ключевой частью распределения регистров. [2] Это означает, что производительность может снизиться. Встроенный ассемблер также усложняет перенос и сопровождение программы в будущем. [1]
Альтернативные средства часто предоставляются как способ упростить работу как компилятору, так и программисту. Внутренние функции для специальных инструкций предоставляются большинством компиляторов, а оболочки C-функций для произвольных системных вызовов доступны на каждой платформе Unix .
Синтаксис
В языковых стандартах
Стандарт ISO C ++ и стандарты ISO C (приложение J) определяют условно поддерживаемый синтаксис для встроенного ассемблера:
Объявление asm имеет вид
объявление
asm : asm ( строковый литерал );
Объявление asm поддерживается условно; его значение определяется реализацией. [3]
Это определение, однако, редко используется в реальном C, поскольку оно одновременно слишком либерально (в интерпретации) и слишком ограничено (в использовании только одного строкового литерала).
В реальных компиляторах
На практике встроенная сборка, работающая со значениями, редко бывает автономной в виде свободно плавающего кода. Поскольку программист не может предсказать, какому регистру назначена переменная, компиляторы обычно предоставляют способ заменить их в качестве расширения.
Как правило, компиляторы C / C ++ поддерживают два типа встроенной сборки:
- asm (или __asm__ ) в GCC . GCC использует прямое расширение правил ISO: шаблон кода сборки записывается в виде строк, причем входы, выходы и закрытые регистры указываются после строк в двоеточиях. Переменные C используются напрямую, а имена регистров заключаются в строковые литералы. [4]
- __asm в Microsoft Visual C ++ (MSVC), компиляторе Borland / Embarcadero C и потомках. Этот синтаксис вообще не основан на правилах ISO; программисты просто пишут ASM внутри блока без необходимости соответствовать синтаксису C. Переменные доступны, как если бы они были регистрами, и разрешены некоторые выражения C. [5] Компилятор ARM имел подобное средство. [6]
Два семейства расширений представляют собой разные представления о разделении труда при выполнении встроенной сборки. Форма GCC сохраняет общий синтаксис языка и разделяет то, что компилятору нужно знать: что необходимо и что нужно изменить. Он явно не требует от компилятора понимания имен инструкций, поскольку компилятор нужен только для замены своих назначений регистров, плюс несколько mov для обработки требований ввода. Однако пользователь склонен к неправильному указанию засоренных регистров. Форма MSVC встроенного предметно-ориентированного языка обеспечивает простоту написания, но требует, чтобы сам компилятор знал об именах кодов операций и их свойствах затирания, что требует дополнительного внимания при обслуживании и переносе. [7] Зная набор инструкций, все еще можно проверить сборку в стиле GCC на наличие ошибок тупика. [8]
GNAT (интерфейс GCC на языке Ada), а LLVM использует синтаксис GCC. [9] [10] В языке программирования D используется DSL, аналогичный официальному расширению MSVC для x86_64, [11] но LDC на основе LLVM также обеспечивает синтаксис в стиле GCC для каждой архитектуры. [12] MSVC поддерживает встроенный ассемблер только на 32-битной платформе x86. [5]
С тех пор язык Rust перешел на синтаксис, абстрагирующийся от встроенных параметров сборки, а не на версию LLVM (в стиле GCC). Он предоставляет достаточно информации, чтобы можно было преобразовать блок во внешнюю сборку, если серверная часть не может обрабатывать встроенную сборку. [7]
Примеры
Системный вызов в GCC
Как правило, прямой вызов операционной системы невозможен в системе, использующей защищенную память. ОС работает на более привилегированном уровне (режим ядра), чем пользовательский (пользовательский режим); (программное) прерывание используется для выполнения запросов к операционной системе. Это редко встречается в языках высокого уровня, поэтому функции-оболочки для системных вызовов пишутся с использованием встроенного ассемблера.
В следующем примере кода 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 ") [ditto] * / : " a " ( num ) / * передать номер системного вызова в eax (" a ") * / : " memory " , " cc " " , / * сообщают компилятору, что память и коды условий были изменены * / " esi " , " edi " , " ebp " ); / * они тоже затерты * / / * Операционная система вернет отрицательное значение при ошибке; * обертки возвращают -1 в случае ошибки и устанавливают глобальную переменную errno * / if ( -125 <= res && res < 0 ) { errno = - res ; res = -1 ; } return res ; }
Специфическая для процессора инструкция на языке D
Этот пример инлайн сборки из D языка программирования шоу кода , который вычисляет тангенс х с использованием x86 «с FPU ( x87 ) инструкции.
// Вычислить тангенс x вещественный tan ( вещественный x ) { asm { fld x [ EBP ] ; // загружаем x fxam ; // проверка нечетных значений fstsw AX ; сахф ; jc trigerr ; // C0 = 1: x - NAN, бесконечность или пусто // 387-е могут обрабатывать денормальные значения SC18 : fptan ; fstp ST ( 0 ) ; // дамп X, который всегда равен 1 fstsw AX ; сахф ; // если (! (fp_status & 0x20)) goto Lret jnp Lret ; // C2 = 1: x вне допустимого диапазона, уменьшить аргумент fldpi ; // загружаем pi fxch ; SC17 : fprem1 ; // напоминание (частичное) fstsw AX ; сахф ; jp SC17 ; // C2 = 1: частичное напоминание, необходимо выполнить цикл fstp ST ( 1 ) ; // удалить пи из стека jmp SC18 ; } trigerr : вернуть реальный . нан ; Lret : ; }
Для читателей, незнакомых с программированием x87, fstsw-sahf, за которым следует идиома условного перехода, используется для доступа к битам C0 и C2 слова состояния FPU x87. fstsw сохраняет статус в универсальном регистре; sahf устанавливает регистр FLAGS на старшие 8 бит регистра; и переход используется для определения того, какой бит флага соответствует биту состояния FPU. [14]
Рекомендации
- ^ a b "DontUseInlineAsm" . GCC Wiki . Проверено 21 января 2020 года .
- ^ Стригель, Бен. " " Для компилятора капля встроенной сборки - это как пощечина. " " . Reddit . Проверено 15 января 2020 года .
- ^ C ++, [dcl.asm]
- ^ а б «Расширенный ассемблер - инструкции ассемблера с операндами выражения C» . Использование GNU C Compiler . Проверено 15 января 2020 года .
- ^ а б «Встроенный ассемблер» . docs.microsoft.com .
- ^ https://www.keil.com/support/man/docs/armclang_mig/armclang_mig_ioz1485879652178.htm
- ^ а б Дантрас, Аманьё (13 декабря 2019 г.). «Rust RFC-2873: стабильный встроенный asm» . Проверено 15 января 2020 года .
Однако можно реализовать поддержку встроенной сборки без поддержки со стороны компилятора, используя вместо этого внешний ассемблер.
Pull Request для отслеживания статуса - ^ «⚙ D54891 [RFC] Проверка корректности встроенной сборки» . reviews.llvm.org .
- ^ «Справочник по языку LLVM: встроенные выражения ассемблера» . Документация LLVM . Проверено 15 января 2020 года .
- ^ «Встроенная сборка» . Документация по Rust (1.0.0) . Проверено 15 января 2020 года .
- ^ «Встроенный ассемблер» . D язык программирования . Проверено 15 января 2020 года .
- ^ "Выражения встроенного ассемблера LDC" . D Wiki . Проверено 15 января 2020 года .
- ^ - Руководство программиста Linux - Системные вызовы
- ^ «FSTSW / FNSTSW - сохранить слово состояния x87 FPU» .
Форма инструкции FNSTSW AX используется в основном при условном переходе ...
Внешние ссылки
- GCC-Inline-Assembly-HOWTO
- Встроенная сборка Clang
- Встроенный ассемблер GNAT
- Справочник по встроенному ассемблеру GCC
- Обозреватель компилятора