Ассемблерные вставки в GCC (2 стр)
Автор: FordPerfect
GNU Assembler и синтаксис AT&T
Прежде, чем перейти к рассмотрению собственно синтаксиса и семантики ассемблерных вставок, рассмотрим подробнее синтаксис AT&T, используемый (для x86) GNU Assembler.
Многим программистам более знаком синтаксис Intel, соответственно большая часть раздела посвящена их сравнению, и переводу из одного в другой.
Синтаксис AT&T задуман как более регулярный и более удобный для автоматической генерации и чтения.
Написание и чтение его людьми напрямую является более вторичной целью. Соответственно, многие программисты находят его менее комфортным для написания/чтения (лично у автора данной статьи нет особых предпочтений в ту или другую сторону).
Стоит отметить, что GNU Assembler использует свой диалект AT&T синтаксиса, имеющий лёгкие отличия от других альтернатив.
Документация по GNU Assembler доступна по адресу http://sourceware.org/binutils/docs-2.26/as/index.html .
Основные сведения (см. также http://sourceware.org/binutils/docs-2.26/as/i386_002dVariations.h… 02dVariations ):
1. Мнемоники инструкций в основном те же, что и в синтаксисе Intel. Примеры: nop, mov, ret, rdtsc, pshufb.
2. Имена регистров предваряются знаком %. Пример:
Intel | AT&T |
xor eax,eax | xor %eax,%eax |
3. Непосредственные операнды предваряются знаком $. Значения по умолчанию в десятичной системе, для шестнадцатеричной используется префикс 0x, как в C/C++. Примеры:
Intel | AT&T |
ret 10 | ret $10 |
int 80h | int $0x80 |
4. Размер операнда определятся суффиксом инструкции (в синтаксисе Intel используются спецификаторы вида DWORD PTR и подобные):
Размер (байт) | Суффикс | Этимология |
1 | b | byte |
2 | w | word |
4 | l | long |
8 | q | quad |
10 | t | ten |
Суффикс t в основном используется в командах FPU.
Если размер однозначно определяется из аргументов, то GNU Assembler принимает также инструкцию без суффикса, см. пример с xor %eax,%eax выше. Тот же пример может быть записан как
xorl %eax,%eax
Мнемоники инструкций SIMD (SSE, AVX и т. д.) записываются так же, как в синтаксисе Intel, без суффиксов.
5. Порядок аргументов в синтаксисе AT&T обратен по сравнению с синтаксисом Intel. Для безаргументных и одноаргументных инструкций разница не проявляется. Для двухаргументных инструкций:
Intel | AT&T |
инструкция приёмник,источник | инструкция источник,приёмник |
Исключения: bound, invlpga, инструкции с 2-мя непосредственными операндами (enter и т. д.).
Для трёхаргументных инструкций ситуация несколько сложнее.
Примеры:
Intel | AT&T |
mov eax,ebx | movl %ebx,%eax |
xor eax,1 | xorl $1,%eax |
pshufd xmm1,xmm0,0 | pshufd $0,%xmm0,%xmm1 |
6. Инструкции fsubp/fsubrp (но не fsub/fsubr!!!) имеют обратную семантику:
Intel | AT&T |
fsubp st(1) | fsubrp %st(1) |
fsubrp st(1) | fsubp %st(1) |
См. также: http://sourceware.org/binutils/docs-2.26/as/i386_002dBugs.html#i386_002dBugs .
7. Косвенная адресация. Адрес, в синтаксисе Intel записываемый как
section:[base + index*scale + disp]
в синтаксисе AT&T будет записан:
section:disp(base, index, scale)
Примеры (взяты из http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html ):
Intel | AT&T |
mov eax,[ecx] | movl (%ecx),%eax |
mov eax,[ebx+3] | movl 3(%ebx),%eax |
mov eax,[ebx+20h] | movl 0x20(%ebx),%eax |
add eax,[ebx+ecx*2h] | addl (%ebx,%ecx,0x2),%eax |
lea eax,[ebx+ecx] | leal (%ebx,%ecx),%eax |
sub eax,[ebx+ecx*4h-20h] | subl -0x20(%ebx,%ecx,0x4),%eax |
Подробное рассмотрение псевдокоманд (предваряемых точкой ".") не приводится, т. к. они выходят за рамки статьи (которая не является учебником по ассемблеру).
Можно отметить псевдокоманды .intel_syntax и .att_syntax (см. http://sourceware.org/binutils/docs-2.26/as/i386_002dVariations.html), позволяющие переключать ассемблер между синтаксисами Intel и AT&T. Также они принимают аргумент (prefix или noprefix), задающий, требуется ли префикс % для названий регистров.
Это псевдокоманды можно использовать для написания (с использованием дополнительных макросов) ассемблерных вставок, работающих в разных компиляторах (напр. GCC и MSVC).
Следует отметить, что сам GCC генерирует код именно в синтаксисе AT&T, поэтому после использования .intel_syntax типично в конце ассемблерной вставки нужно будет переключаться обратно.
Также можно упомянуть псевдокоманды для непосредственного включения данных (соответствующие директивам DB и т. д. в синтаксисе Intel):
.byte, .word, .short, .hword, .int, .long, .float, .single, .double, .ascii, .asciz
Т. к. GNU Assembler в первую очередь ориентирован на работу с кодом сгенерированным автоматически (в частности - компилятором), то он, иногда, довольно небрежно относится к обработке ошибок.
В частности код (пример адаптирован из https://mobile.twitter.com/rygorous/status/659148274167734272?p=v )
asm( ".intel_syntax noprefix\n\t" "lea rax, [rax + rbx*4 + rcx*8 + rdx*8 + rsi*8]\n\t" ".att_syntax prefix\n\t");
вполне принимается. Реально сгенерированный код в этом случае эквивалентен
leaq (%rax,%rsi,8),%rax
Написание многострочных команд
Текст ассемблерной вставки является синтаксически C строкой, с обычными возможностями: esc-последовательностями и авто-конкатенацией. В связи с этим сформировалась определённая традиция написания многострочных команд.
Код
#include <cstdio> int main() { printf( "Before asm.\n"); asm( "nop" "nop" "nop" "nop" "nop" ); printf( "After asm.\n"); return 0; }
конечно же не компилируется (точнее, не ассемблируется), т. к. авто-конкатенация не добавляет переводы строки:
Assembler messages: Error: no such instruction: `nopnopnopnopnop'
Версия
#include <cstdio> int main() { printf( "Before asm.\n"); asm( "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" ); printf( "After asm.\n"); return 0; }
работает, но asm код выглядит следующим образом:
.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 # 12 "test_asm.cpp" 1 nop nop nop nop 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
Отсутствие выравнивания выглядит эстетическим недостатком.
Поэтому многострочные ассемблерные вставки обычно записывают в следующем стиле:
#include <cstdio> int main() { printf( "Before asm.\n"); asm( "nop\n\t" "nop\n\t" "nop\n\t" "nop\n\t" "nop\n\t" ); printf( "After asm.\n"); return 0; }
Что приводит к asm коду:
.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 # 12 "test_asm.cpp" 1 nop nop nop nop 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
Можно заметить запись \t после \n, а не в начале следующей строки. Это на практике оказывается удобнее и облегчает чтение.
Иногда вместо
"nop\n\t"
встречается запись вида
"nop" "\n\t"
В C++11 возможна также запись с использованием raw-strings (компилятор понимает их в ассемблерных вставках):
#include <cstdio> int main() { printf( "Before asm.\n"); asm( R"( nop nop nop nop nop )"); printf( "After asm.\n"); return 0; }
Стоит учитывать, что в зависимости от настроек редактора (IDE) отступы в начале в этом случае могут оказаться как пробелами, так и табуляцией (табуляциями).
В C поддержка raw-strings отсутствует.
30 апреля 2016 (Обновление: 4 мая 2016)