Inline asm for x86
Colin Plumb (colin@nyx.net)
Mon, 20 Apr 1998 18:56:24 -0600 (MDT)
colin@nyx.net, 20 April 1998
Краткое руководство GCC inline asm
Рассматривается один из инструментов ускорения работы ядра -
расширенный ассемблер.
Рассмотрим пример :
asm("foo %1,%2,%0" : "=r" (output) : "r" (input1), "r" (input2));
Ее можно усложнить следующим образом :
asm("foo %1,%2,%0" : "=r" (ptr->vtable[3](a,b,c)->foo.bar[baz]) :
: "r" (gcc(is) + really(damn->cool)), "r" (42));
GCC интерпретирует это следующим образом :
register int t0, t1, t2;
t1 = gcc(is) + really(damn->cool);
t2 = 42;
asm("foo %1,%2,%0" : "=r" (t0) : "r" (t1), "r" (t2));
ptr->vtable[3](a,b,c)->foo.bar[baz] = t0;
Основная форма asm() :
asm( "code" : outputs : inputs : clobbers);
Внутри "code" , %0 - это обычно 1-й аргумент output.
%1 - второй аргумент , и т.д.
Их может быть не более %9.
Если вы хотите в "code" втиснуть несколько операторов ,
прийдется их разделять "\n\t".
Но разделять так необязательно - можно все записать и в одной строке -
для этого достаточно расставить";" .
Как output , так и input состоят из 2-х частей -
"constraints" и (value).
Для outputs (value) - возвращаемое значение.
outputs должен быть промаркирован знаком равенства "=".
"r" - или "rm" - буквально - register or memory.
"ri" - register или immediate value.
"g" - "general" - может быть и тем и другим.
"o" похоже на "m" - "offsettable", означающее , что вы можете
прибавить к нему смещение.
Для x86 все операнды в памти - offsettable, поддерживающее индексацию.
* Несколько слов о inputs
input может быть временной переменной.
GCC может разместить output в том же регистре, в котором приходит
input ,если последний более не нужен.
* x86 assembly code
GNU использует AT&T-синтаксис,который отличается от
Intel-синтаксиса.
В AT&T например отсутствует DWORD PTR .
Intel использует "op dest,src", AT&T использует
"op src,dest".
AT&T использует знак процента для обозначения регистров - %eax, %ebx.
Не нужно ставить символ подчеркивания перед переменными и именами функций.
Отличие также в том , что в AT&T размер операнда зашит в саму инструкцию -
например , вместо inc нужно - "incb", "incw" ,"incl" соответственно для 8, 16 или 32 бит.
Иногда размер операнда указывать необязательно - например
можно написать "inc %eax" - и так понятно,что речь идет о 2-х байтах.
Но если вы работаете с операндами в памяти , нужно писать"incl foo",
а не "inc foo",иначе будет ошибка.
"incl %al" - это конечно тоже ошибка.
Перед именем внешней си-шной переменной можно ставить символ доллара - $.
При этом , "movl foo,%eax" копирует содержимое памяти, расположенной по адресу foo ,
в регистр %eax.
А "movl $foo,%eax" даст совершенно другой эффект - будет скопирован
адрес переменной foo.
"movl 42,%eax" - копирование абсолютного значения.
Адресные режимы реализованы с помощью формата offset(base,index,scale).
Например , -44(%ebx,%eax) будет эквивалентно -44(%ebx,%eax,1).
В качестве шкалы можно использовать 1, 2 ,4 , 8.
* Equivalence constraints
Иногда может возникнуть ситуация,когда для ввода и вывода
нужен один и тот же регистр.
Для этого можно использовать специальный constraint "0":
asm("foo %1,%0" : "=r" (output) : "r" (input1), "0" (input2));
Поскольку здесь %2 опущен, output и input2 будут использовать
один и тот же регистр . Противоречия здесь нет ,
поскольку GCC сам сгенерит временные регистры.
* Constraints on the x86
i386 имеет достаточно типов регистров для прикладного использования.
Основные типы :
g - general effective address
m - memory effective address
r - register
i - immediate value, 0..0xffffffff
n - immediate value known at compile time.
i386-specific (i386.h):
q - регистры с байтовой адресацией (eax, ebx, ecx, edx)
A - eax или edx
a, b, c, d, S, D - eax, ebx, ecx, edx, esi, edi соответственно
I - immediate 0..31
J - immediate 0..63
K - immediate 255
L - immediate 65535
M - immediate 0..3 (shifts that can be done with lea)
N - immediate 0..255 (one-byte immediate value)
O - immediate 0..32
Пример использования "I" для rotate left:
asm("roll %1,%0" : "=g" (result) : "cI" (rotate), "0" (input));
* Advanced constraints
= используется для маркировки output.
% говорит о том , что данный и следующий операнды могут
обменять свои приоритеты
, разделитель constraints.
x86 разрешает операции типа register-memory и memory-register ,
но запрещает memory-memory:
asm("add %1,%0" : "=r,rm" (output) : "%g,ri" (input1), "0,0" (input2));
Если output - register, input1 может быть чем угодно.
Но если output - memory, input может быть только register либо immediate value.
И input2 должен быть в одном месте с output
& output operand пишется перед inputs , поэтому output и input
должны быть в разных регистрах.
* const and volatile
Есть 2 хинта , которые можно применить к asm-
выражениям.
asm volatile(...) - ключевое слово volatile говорит компилятору ,
что он не может его оптимизировать и обязан выполнять именно так , как оно записано.
asm const() - может быть оптимизировано как обычное subexpression optimization.
Пример:
int foo(int x);
{
int i, y, total;
total = 0;
for (i = 0; i < 100; i++) {
asm volatile("foo %1,%0" : "=r" (y) : "g" (x));
total += y;
}
return total;
}
Можно поставить ключевое слово "const".
В первом случае , без "const" , будет сгенерирован код :
func1:
xorl %ecx,%ecx
pushl %ebx
movl %ecx,%edx
movl 8(%esp),%ebx
.align 4
.L7:
#APP
foo %ebx,%eax
#NO_APP
addl %eax,%ecx
incl %edx
cmpl $99,%edx
jle .L7
movl %ecx,%eax
popl %ebx
ret
>
С использованием const будет сгенеирован другой код :
func2:
xorl %edx,%edx
#APP
foo 4(%esp),%ecx
#NO_APP
movl %edx,%eax
.align 4
.L13:
addl %ecx,%edx
incl %eax
cmpl $99,%eax
jle .L13
movl %edx,%eax
ret
* Alternate keywords
__asm__() - еще одна альтернатива для asm(),
при том что не генерирует варнинги.
* Output substitutions
Иногда возникает необходимость включить какую-то нестандартную конструкцию типа :
asm("lea %1(%2,%3,1<<%4),%0" : "=r" (out)
: "%i" (in1), "r" (in2), "r" (in3), "M" (logscale));
При этом GCC может сгенерировать что-то подобное :
lea $-44(%ebx,%eax,1<<$2),%ecx
на самом деле это синтаксическая ошибка.
Символ $ для констант в данном случае бесполезен.
Существуют символы-модификаторы.
Один из них - "c", который проигнорирует immediate value.
Правильный вариант :
asm("lea %c1(%2,%3,1<<%c4),%0" : "=r" (out)
: "%i" (in1), "r" (in2), "r" (in3), "M" (logscale));
будет сгенерировано
lea -44(%ebx,%eax,1<<2),%ecx
Еще один символ-модификатор - n
%n0 работает аналогично %c0, но делает значение отрицательным
%l0 аналогично %c0, для jump target.
%k0 выводит 32-битную форму операнда. %eax, etc.
%w0 выводит 16-битную форму операнда. %ax, etc.
%b0 выводит 8-битную форму операнда. %al, etc.
%h0 выводит старшие 8 бит регистра. %ah, etc.
%z0 выводит opcode операнда, b, w или l.
По умолчанию %0 выводит регистр в форме соответствующей размеру аргумента.
Т.е.
asm("inc %0" : "=r" (out) : "0" (in))
выведет "inc %al", "inc %ax" или "inc %eax" в зависимости от типа "out".
Можно использовать %w и %b для обьектов не-регистрового типа.
Рассмотрим пример:
#define xchg(m, in, out) \
asm("xchg%z0 %2,%0" : "=g" (*(m)), "=r" (out) : "1" (in))
int
bar(void *m, int x)
{
xchg((char *)m, (char)x, x);
xchg((short *)m, (short)x, x);
xchg((int *)m, (int)x, x);
return x;
}
Будет сгенерирован код :
.globl bar
.type bar,@function
bar:
movl 4(%esp),%eax
movb 8(%esp),%dl
#APP
xchgb %dl,(%eax)
xchgw %dx,(%eax)
xchgl %edx,(%eax)
#NO_APP
movl %edx,%eax
ret
* Extra % patterns
Избыточный % не несет в себе никакой дополнительной информации -
примером является %%.
Еще одним примером является %=, генерирующий уникальное число для asm()-блока.
Это может быть использовано для меток-шаблонов.
* Examples
Пример кода из include/asm-i386/system.h:
#define _set_tssldt_desc(n,addr,limit,type) \
__asm__ __volatile__ ("movw %3,0(%2)\n\t" \
"movw %%ax,2(%2)\n\t" \
"rorl $16,%%eax\n\t" \
"movb %%al,4(%2)\n\t" \
"movb %4,5(%2)\n\t" \
"movb $0,6(%2)\n\t" \
"movb %%ah,7(%2)\n\t" \
"rorl $16,%%eax" \
: "=m"(*(n)) : "a" (addr), "r"(n), "ri"(limit), "i"(type))
Этот пример можно переписать с использованием любого другого регистра ,
отличного от %eax:
#define _set_tssldt_desc(n,addr,limit,type) \
__asm__ __volatile__ ("movw %w3,0(%2)\n\t" \
"movw %w1,2(%2)\n\t" \
"rorl $16,%1\n\t" \
"movb %b1,4(%2)\n\t" \
"movb %4,5(%2)\n\t" \
"movb $0,6(%2)\n\t" \
"movb %h1,7(%2)\n\t" \
"rorl $16,%1" \
: "=m"(*(n)) : "q" (addr), "r"(n), "ri"(limit), "ri"(type))
Проблема в том , что нет возможности закодировать смещение
для данного адреса.
Если адрес равен "40(%eax)" то смещение на 2 может быть сделано как "2+" .
Но если адрес "(%eax)" то "2+(%eax)" будет неверно.
Пример:
#define _set_tssldt_desc(n,addr,limit,type) \
__asm__ __volatile__ ("movw %w2,%0\n\t" \
"movw %w1,2+%0\n\t" \
"rorl $16,%1\n\t" \
"movb %b1,4+%0\n\t" \
"movb %3,5+%0\n\t" \
"movb $0,6+%0\n\t" \
"movb %h1,7+%0\n\t" \
"rorl $16,%1" \
: "=o"(*(n)) : "q" (addr), "ri"(limit), "i"(type))
constraint "o" - то же самое что и "m";
Пример:
__asm__ __volatile__ ("movw %w7,%0\n\t" \
"movw %w6,%1\n\t" \
"rorl $16,%6\n\t" \
"movb %b6,%2\n\t" \
"movb %b8,%3\n\t" \
"movb $0,%4\n\t" \
"movb %h6,%5\n\t" \
"rorl $16,%6" \
: "=m"(*(n)), \
"=m"((n)[2]), \
"=m"((n)[4]), \
"=m"((n)[5]), \
"=m"((n)[6]), \
"=m"((n)[7]) \
: "q" (addr), "g"(limit), "iqm"(type))
Пример
{
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax"); /* clobbered register */
}
В этом примере переменная b приравнивается к a .
* "b" - output operand, ссылка на %0 , и "a" - input operand, ссылка на %1.
* "r" - constraint , указывает на то , что переменные "a" и "b"
хранятся в регистрах.
* Регистр %eax идет с двойным префиксом %
* clobbered register %eax говорит GCC о том , что значение %eax модифицируется
внутри"asm", поэтому GCC не использует этот регистр для хранения
чего-то еще
* movl %1, %%eax - копируем "a" в %eax, и movl %%eax, %0 -копируем
%eax в "b".
* после выполнения "asm" изменеие "b" внутри блока становится
видно снаружи , потому что b определено как output-операнд
Пример
В следующем примере инструкция cpuid получает параметр из регистра %eax
и результат выводит в 4 регистрах: %eax, %ebx, %ecx, %edx. input - переменная "op" -
передается в блок asm через регистр eax . a,b, c, d - constraints.
asm ("cpuid"
: "=a" (_eax),
"=b" (_ebx),
"=c" (_ecx),
"=d" (_edx)
: "a" (op));
Ниже приводится код , который генерирует компилятор :
movl -20(%ebp),%eax /* store 'op' in %eax -- input */
#APP
cpuid
#NO_APP
movl %eax,-4(%ebp) /* store %eax in _eax -- output */
movl %ebx,-8(%ebp) /* store other registers in
movl %ecx,-12(%ebp) respective output variables */
movl %edx,-16(%ebp)
Пример
Функция strcpy может быть реализована с помощью "S" и "D" constraints :
asm ("cld\n
rep\n
movsb"
: /* no input */
:"S"(src), "D"(dst), "c"(count));
Источник src копируется в %esi с помощью "S" constraint, приемник dst копируется в %edi
с помощью "D" constraint. Счетчик копируется в %ecx.
Пример
Увеличим значение переменной i на единицу :
int i = 0;
__asm__("
pushl %%eax\n
movl %0, %%eax\n
addl $1, %%eax\n
movl %%eax, %0\n
popl %%eax"
:
: "g" (i)
);
В следующем примере мы складываем 2 переменных и результат храним
в первой переменной - i :
int i=0, j=1;
__asm__ __volatile__("
pushl %%eax\n
movl %0, %%eax\n
addl %1, %%eax\n
movl %%eax, %0\n
popl %%eax"
:
: "g" (i), "g" (j)
);
/* i = i + j; */
В следующем примере переменной i мы присвоим 1,
используя = :
int i=0;
__asm__ __volatile__("
pushl %%eax\n
movl $1, %%eax\n
movl %%eax, %0\n
popl %%eax"
: "=g" (i)
);
/* i=1; */
В следующем примере мы результат сложения 2-х переменных
положим в 3-ю - переменную k :
int i=0, j=1, k=0;
__asm__ __volatile__("
pushl %%eax\n
movl %1, %%eax\n
addl %2, %%eax\n
movl %%eax, %0\n
popl %%eax"
: "=g" (k)
: "g" (i), "g" (j)
);
/* k = i + j; */
В следующем примере мы говорим компилятору ,
чтобы он сохранил значение регистра ax после входа в asm-блок
и после выхода из него , т.е. ничего специально сами не делаем для
его восстановления :
int i=0, j=1, k=0;
__asm__ __volatile__("
movl %1, %%eax\n
addl %2, %%eax\n
movl %%eax, %0"
: "=g" (k)
: "g" (i), "g" (j)
: "ax", "memory"
);
/* k = i + j; */
Локальные метки внутри инлайна нужно заканчивать
"b" или "f" , в зависимости от того , спереди или сзади они стоят :
__asm__ __volatile__("
0:\n
...
jmp 0b\n
...
jmp 1f\n
...
1:\n
...
);
|