Ассемблерные вставки в GCC
Автор: FordPerfect
Статья является попыткой систематизировать brain-dump известной автору информации по ассемблерным вставкам в GCC.
Автор не считает себя авторитетом в данном вопросе, и статья может содержать неточности. Замечания, дополнения и исправления приветствуются.
Вопрос, зачем вообще использовать встроенный ассемблер, выходит за рамки данной статьи.
Предполагается, что читатель знаком с каким-либо ассемблером: учебником по языку per se статья не является.
За исключением случаев, когда явно оговорено обратное, информация в равном мере применима к C и к C++.
Все примеры в статье - для архитектуры x86 (и x86_64), но сама поддержка ассемблерных вставок в GCC не ограничивается этой архитектурой, и большинство информации применимо и для других.
Все отрывки кода, если явно не оговорено обратное и не указано авторство, находятся в общем доступе (public domain).
Общие сведения
Концептуальная модель
GNU Assembler и синтаксис AT&T
Написание многострочных команд
Синтаксис и семантика
Базовая форма
Расширенная форма
Операнды
OutputOperands
InputOperands
Clobbers
GotoLabels
Constraints
Примеры
Переходы и метки
Диспетчеризация CPU
Дополнительно
Модификаторы операндов
Явное указание регистров для переменных
Явное указание имён
Вычисление размера кода
Недокументированные возможности
Литература
Общие сведения
Язык C++ поддерживает ассемблерные вставки, что отражено в Стандарте языка.
Однако данная возможность глубоко специфична индивидуальным компилятору/архитектуре, поэтому Стандарт немногословен и ограничивается следующим текстом:
7.4 The asm declaration [dcl.asm]
An asm declaration has the form
asm-definition:
asm( string-literal ) ;
The asm declaration is conditionally-supported; its meaning is implementation-defined. [ Note: Typically it is used to pass information through the implementation to an assembler. —end note ]
Стандарт языка C ассемблерные вставки вообще не определяет, и упоминает их только в списке распространённых расширений:
J.5.10 The asm keyword
The asm keyword may be used to insert assembly language directly into the translator
output (6.8). The most common implementation is via a statement of the form:
asm ( character-string-literal );
Большинство популярных компиляторов поддерживают эту возможность в том или ином виде.
Компилятор MSVC поддерживает их для 32-битного, но не для 64-битного кода. Также, как можно заметить, синтаксис ассемблерных вставок MSVC отличается от указанного в Стандарте.
Под GCC здесь и далее (если не оговорено обратное) подразумевается компилятор, поддерживающий набор расширений GCC. Это, с некоторых пор, официальное определение макро __GNUC__ (см. https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html , https://sourceforge.net/p/predef/wiki/Compilers/ и далее по ссылкам). В частности компиляторы clang и icc определяют __GNUC__; оба этих компилятора (так же как и сам GCC) поддерживают ассемблерные вставки в синтаксисе GCC.
Концептуальная модель
Типично, компилятор превращает исходный код в объектный код, который потом сборщиком (линкером) собирается в исполняемый файл.
GCC использует несколько необычное решение: компиляторы C и C++ во многом близки к языковым трансляторам ( https://en.wikipedia.org/wiki/Source-to-source_compiler ), и результатом их работы является файл на языке ассемблера (именно на текстовом (мнемоническом) ассемблере, а не в машинном коде), который далее преобразуется в объектный код штатным ассемблером (утилита as; ассемблер, входящий в состав GNU, обычно в документации называют GNU Assembler, сокращённо gas, несмотря на то, что сама утилита называется именно as).
Обычно при вызове компилятор GCC сам производит соответствующие действия (вызов as и т. д.) "под капотом", и для создания объектного файла никаких дополнительных указаний не требуется.
Можно указать компилятору опцию -S (большая буква S; у малой s другое значение), для того чтобы произвести только перевод программы на ассемблер.
Рассмотрим пример. Создадим файл test_asm.cpp со следующим кодом:
#include <cstdio> int main() { printf( "Hello world!\n"); return 0; }
Выполнив команду
gcc test_asm.cpp -S
получим файл test_asm.s (традиционным расширением файлов на языке ассемблера для GCC является .s):
.file "test_asm.cpp" .def ___main; .scl 2; .type 32; .endef .section .rdata,"dr" LC0: .ascii "Hello world!\0" .text .globl _main .def _main; .scl 2; .type 32; .endef _main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $16, %esp call ___main movl $LC0, (%esp) call _puts movl $0, %eax leave ret .ident "GCC: (tdm-1) 4.9.2" .def _puts; .scl 2; .type 32; .endef
Организация кода, который генерирует GCC из исходника на C++ выходит за рамки данной статьи. При беглом просмотре можно заметить:
1. Префикс _ (подчёркивание) для экспортированных функций C (и для main). Для функций на C++ используется несколько более сложный name mangling.
2. Функция __main (два подчёркивания), в которой GCC производит некоторую инициализацию (её реализация находится отдельно, в составе стандартных файлов GCC).
3. printf был соптимизирован до puts (причём никаких опций оптимизации указано не было).
При желании, можно получить несколько более подробный код (с дополнительными комментариями), воспользовавшись опцией -fverbose-asm.
Построчный листинг (в файле test_asm.lst) C++ и ассемблера можно получить, воспользовавшись командой (подробнее см. http://www.delorie.com/djgpp/v2faq/faq8_20.html ):
gcc -c -g -Wa,-a,-ad test_asm.cpp > test_asm.lst
Данная модель достаточно естественным образом приводит к логике ассемблерных вставок, которую и использует GCC.
GCC вставляет, после возможных макроподстановок, текст ассемблерной вставки непосредственно в выходной файл на языке ассемблера.
Пример (о конкретном синтаксисе ассемблерных вставок см. ниже):
#include <cstdio> int main() { printf( "Before asm.\n"); asm( "nop"); printf( "After asm.\n"); return 0; }
преобразуется в
.file "test_asm.cpp" .def ___main; .scl 2; .type 32; .endef .section .rdata,"dr" LC0: .ascii "Before asm.\0" LC1: .ascii "After asm.\0" .text .globl _main .def _main; .scl 2; .type 32; .endef _main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $16, %esp call ___main movl $LC0, (%esp) call _puts /APP # 6 "test_asm.cpp" 1 nop # 0 "" 2 /NO_APP movl $LC1, (%esp) call _puts movl $0, %eax leave ret .ident "GCC: (tdm-1) 4.9.2" .def _puts; .scl 2; .type 32; .endef
Как видим, GCC отметил вставку в коде явным образом.
Модель, используемая GCC имеет несколько важных свойств.
Во-первых, компилятор почти не "подглядывает" в указанный программистом код ассемблерной вставки. Он при необходимости проводит макроподстановки (см. ниже), но в общем передаёт код ассемблеру почти "как есть", и почти не имеет представления о происходящем внутри. В частности, обнаружением ошибок занимается именно ассемблер (GCC передаёт программисту сообщения об ошибках от ассемблера).
Во-вторых, как следствие, ассемблерная вставка является для компилятора (и в частности оптимизатора) единой непрозрачной командой (чёрным ящиком). Всё нужную ему информацию о том, как этот блок взаимодействует с окружающим миром, компилятор получает напрямую от программиста, из явно указанных операндов ассемблерной вставки (задающих связи ассемблерного кода с переменными C++ и список задействованных ресурсов (регистров и т. д.) и изменений (состояния флагов, памяти и т. д.) в ассемблерной вставке), а не из детального рассмотрения текста ассемблерной вставки (которого он не производит). Технически говоря, GCC предоставляет программисту интерфейс к Register Transfer Language. Ответственность о соответствии действительности информации, указанной в операндах, целиком лежит на программисте.
В рамках этих ограничений, компилятор волен обращаться с ассемблерной вставкой (как и с другими командами) так, как ему вздумается: перемещать, дублировать (напр. при подстановке inline-функций), или вообще выбросить, если оптимизатор придёт к такому решению.
30 апреля 2016 (Обновление: 4 мая 2016)