GCC-Inline-Assembly-HOWTO
v0.1, 01 March 2003.
Этот HOWTO обьясняет как использовать inline assembly.
Для чтения этой статьи нужны базовые знани по x86 assembly и C.
Copyright (C)2003 Sandeep S.
This document is free; you can redistribute and/or modify this under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version.
This document is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
Под понятием inline-кода имеются ввиду функции , код которых реально вставляется в том месте,
откуда он вызывается . Это немного похоже на макросы .
В чем преимущество inline functions?
Этот метод уменьшает функциональный оверхед .
Для обьявления такой функции нужно использовать ключевое слово inline .
inline assembly - это несколько строчек кода на асме , записанных в теле inline functions.
Этот прием используется в системном программировании .
Для декларирования inline assembly функции используется ключевое слово asm .
Внутри таких функций можно оперировать внешними си-шными переменными .
GCC, GNU C компилятор для Linux, использует AT&T& UNIX синтаксис.
AT&T syntax немного отличается от Intel syntax , который используется например в винде . Отличия :
- Иной порядок Source-Destination .
В Intel 1-й операнд - destination, 2-й - source .
В AT&T все наоборот : 1-й - source и 2-й - destination. Т.е.
"Op-code dst src" - Intel
"Op-code src dst" - AT&T .
- Наименовани регистров
В AT&T перед именем регистра идет префикс процент % , т.е например для eax будет %eax.
- Immediate Operand.
AT&T immediate - операнд идет с префиксом доллара ’$’. Для статических "C"
также идет префикс доллара ’$’.
Для Intel , 16-ричные константы идут с префиксом ’h’ ,
а для AT&T идет другой префикс ’0x’ .
В результате для AT&T для 16-ричных мы имеем сначала ’$’, потом ’0x’
и потом уже сам операнд.
- Operand Size.
Для AT&T это префиксы ’b’, ’w’,
и ’l’ определяют byte(8-bit), word(16-bit), и
long(32-bit) . В Intel это соответственно ’byte ptr’, ’word ptr’, и ’dword ptr’.
Например Intel-ская инструкция "mov al, byte ptr foo" трансформируется в "movb foo, %al"
для AT&T .
- Memory Operands.
В Intel базовый регистр закрывается квадратными скобками ’[’ и ’]’
в то время как в AT&T они круглые ’(’ и ’)’.
В Intel это похоже на
section:[base + index*scale + disp],
section:disp(base, index, scale) для AT&T.
Не забывайте что для констант
disp/scale, нужен префикс доллара’$’
А теперь несколько примеров
+------------------------------+------------------------------------+
| Intel Code | AT&T Code |
+------------------------------+------------------------------------+
| mov eax,1 | movl $1,%eax |
| mov ebx,0ffh | movl $0xff,%ebx |
| int 80h | int $0x80 |
| mov ebx, eax | movl %eax, %ebx |
| 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 |
+------------------------------+------------------------------------+
Базовый формат inline assembly :
asm("assembly code");
Пример
asm("movl %ecx %eax"); /* содержимое ecx копируется в eax */
/* копирование байта из регистра bh в память по адресу в eax*/
__asm__("movb %bh (%eax)");
Здесь можно заметить разницу в использовании asm и __asm__ .
Оба варианта верные. Можно использовать __asm__ если ключевое слово asm
конфликтует с чем-нибудь в программе.
Если инструкций больше одной , последующая выносится на новую строку и ставяься двойные кавычки,
и строка в конце квотируется ’\n’ and ’\t’ .
Пример
__asm__ ("movl %eax, %ebx\n\t"
"movl $56, %esi\n\t"
"movl %ecx, $label(%edx,%ebx,$4)\n\t"
"movb %ah, (%ebx)");
В базовом inline assembly, мы можем оперировать только с инструкциями.
В расширенном варианте можно оперировать с операндами .
Можно определить входящие и выходящие регистры .
Базовый формат расширенного варианта :
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
Шаблон включает инструкции . Двоеточие разделяет инпут:аутпут .
Запятая разделяет операнды внутри группы .
Общее число операндов ограничено десятью .
Если output-операнды отсутствуют , это место нужно ограничить двумя двоеточиями.
Пример:
asm ("cld\n\t"
"rep\n\t"
"stosl"
: /* output отсутствует */
: "c" (count), "a" (fill_value), "D" (dest)
: "%ecx", "%edi"
);
Что же выполняет эта невнятная конструкция ?
Содержимое fill_value будет скопировано
count раз по адресу , который находится в в регистре edi .
Еще один пример:
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
Тут смысл такой : сначала переменная a копируется в регистр eax , а потом содержимое регистра
eax копируется в переменную b , т.е. переменные a и b становятся равны .
Нужно учесть следующее :
- "b" - это output-операнд , который ссылается на %0 и "a" input-операнд и ссылается на %1.
- "r" - это конструктор . Знак = перед ним указывает на то , что операнд имеет атрибут write
- Два знака процент % - это не опечатка. В данном контексте 2 процента отличают регистр от операндов ,
которые имеют один процент .
- Регистр %eax после третьего двоеточия говорит о том , что его значение модифицировано внутри
блока asm .
После того как мы выйдем из "asm"-блока , значение переменной "b" будет равно 10.
В шаблоне каждый операнд есть ссылка на число .
Первый операнд имеет индекс ноль , и далее по возрастанию .
Давайте рассмотрим пример с умножением числа на 5.
Для этого используем инструкцию lea .
asm ("leal (%1,%1,4), %0"
: "=r" (five_times_x)
: "r" (x)
);
Здесь инпутом служит внешняя си-ная переменная ’x’.
Мы специально не оговариваем регистр , который будет использоваться .
GCC сам выберет регистры для input и output и сделает то , что мы захотели.
С помощью конструкторов (constraint) этот пример можно переписать так :
asm ("leal (%0,%0,4), %0"
: "=r" (five_times_x)
: "0" (x)
);
В этом случае input и output будут попадать уже в один регистр.
Но мы пока не знаем в какой . Если мы специально хотим его определить , тогда :
asm ("leal (%%ecx,%%ecx,4), %%ecx"
: "=c" (x)
: "c" (x)
);
Регистры , помещаемые в этот список , говорят компилятору , что их будут
модифицировать внутри asm. Нам этот список не нужен , потому что мы раньше указали ,
с каким регистром мы работаем явно .
Наполнив этот список какими-то регистрами , мы можем много раз их использовать
Рассмотрим пример с вызовом процедуры _foo , аргументы которой прописаны в регистрах
eax и ecx .
asm ("movl %0,%%eax;
movl %1,%%ecx;
call _foo"
: /* no outputs */
: "g" (from), "g" (to)
: "eax", "ecx"
);
Если вам приходилось копаться в исходниках ядра , вы наверно обратили внимание на ключевое
слово volatile или __volatile__ .
Если ассемблерная инструкция должна быть выполнена там , где она положена ,
т.е. например не может быть перемещена куда-то в другое место при отимизации ,
мы используем ключевое слово volatile внутри asm .
asm volatile ( ... : ... : ... : ...);
Обычно не нужно злоупотреблять этим ключевым словом и нужно давать компилятору
возможность оптимизировать код - он это делает лучше нас :-)
Констрэйнты выполняют большой обьем работы внутря inline assembly.
Они указывают на регистр , в котором находится операнд , или на память , и т.д.
- Register operand constraint(r)
Если операнд определен с помощью констрэйнта , он хранится в
General Purpose Registers(GPR). Пример :
asm ("movl %%eax, %0\n" :"=r"(myval));
В данном случае переменная myval хранится в регистре , затем значение регистра
eax копируется в этот регистр, после чего значение этого регистра копируется в память по адресу ,
в котором находится значение переменной myval.
При использовании "r" gcc может хранить переменную myval в любом из доступных GPR.
Для определения этого регистра можно сделать так :
+---+--------------------+
| r | Register(s) |
+---+--------------------+
| a | %eax, %ax, %al |
| b | %ebx, %bx, %bl |
| c | %ecx, %cx, %cl |
| d | %edx, %dx, %dl |
| S | %esi, %si |
| D | %edi, %di |
+---+--------------------+
- Matching(Digit) constraints
Иногда одна переменная может обслуживать одновременно и input и output-операнды.
asm ("incl %0" :"=a"(var):"0"(var));
В данном случае регистр eax используется для input и output .
В этом примере переменная var попадает в eax , там изменяется ,
и это измененное значение регистра eax попадает назад в память по адресу ,
в котором находится переменная var :-)
Другие случаи применения constraints:
- "m" : memory operand
- "o" : memory operand с адресом , к которому прибавляется смещение из таблицы
- "V" : противовес предыдущему варианту
- "i" : immediate integer operand с заранее неизвестным значением
- "n" : immediate integer operand с заданным значением
- "g" : любой регистр , память или переменная
Следующие констрэйнты специфичны только для x86 .
- "r" : Register operand constraint, look table given above.
- "q" : Registers a, b, c or d.
- "I" : Constant in range 0 to 31 (for 32-bit shifts).
- "J" : Constant in range 0 to 63 (for 64-bit shifts).
- "K" : 0xff.
- "L" : 0xffff.
- "M" : 0, 1, 2, or 3 (shifts for lea instruction).
- "N" : Constant in range 0 to 255 (for out instruction).
- "f" : Floating point register
- "t" : First (top of stack) floating point register
- "u" : Second floating point register
- "A" : Specifies the `a’ or `d’ registers. This is primarily useful
for 64-bit integer values intended to be returned with the `d’ register
holding the most significant bits and the `a’ register holding the least
significant bits.
- "="
Операнд типа output write-only
- "&" : Означает , что операнд будет модифицирован прежде
чем input-инструкция будет закончена .
Программа для сложения 2-х чисел :
int main(void)
{
int foo = 10, bar = 15;
__asm__ __volatile__("addl %%ebx,%%eax"
:"=a"(foo)
:"a"(foo), "b"(bar)
);
printf("foo+bar=%d\n", foo);
return 0;
}
Мы сохраняем переменную foo в регистре %eax, переменную bar в регистре %ebx и
результат помещаем в %eax.
Символ ’=’ указывает на то , что это output-register.
Теперь прибавим число к переменной :
__asm__ __volatile__(
" lock ;\n"
" addl %1,%0 ;\n"
: "=m" (my_var)
: "ir" (my_int), "m" (my_var)
: /* no clobber-list */
);
Это пример с атомарным сложением - на это указывает инструкция ’lock’ .
В output "=m" мы говорим , что переменная my_var находится в памяти.
Префикс "ir" говорит , что my_int - это целое и должно быть помещено в регистр .
Теперь несколько операций с регистрами/переменными и сравнениями :
__asm__ __volatile__( "decl %0; sete %1"
: "=m" (my_var), "=q" (cond)
: "m" (my_var)
: "memory"
);
Переменная my_var уменьшается на единицу и если результат равен нулю ,
то устанавливается переменная cond .
Можно использовать "incl %0" вместо "decl %0" для увеличения переменной my_var.
my_var - переменная в памяти , переменная cond - ее значение может быть произвольным образом
положено компилятором в любой из регистров в любой из регистров eax, ebx, ecx and edx.
constraint "=q" это гарантирует .
Поскольку в clobber list находится ссылка на ключевое слово memory , то код изменяет
контент памяти .
Как установить/очистить бит в регистре ?
__asm__ __volatile__( "btsl %1,%0"
: "=m" (ADDR)
: "Ir" (pos)
: "cc"
);
Здесь бит в позиции ’pos’ для переменной ADDR (в памяти) устанавливается в 1
Можно использовать ’btrl’ для ’btsl’ для установки бита в ноль.
Констрэйнт "Ir" для pos говорит , что pos храним в регистре,
и его значение в диапазоне 0-31 .
Копирование строки :
static inline char * strcpy(char * dest,const char *src)
{
int d0, d1, d2;
__asm__ __volatile__( "1:\tlodsb\n\t"
"stosb\n\t"
"testb %%al,%%al\n\t"
"jne 1b"
: "=&S" (d0), "=&D" (d1), "=&a" (d2)
: "0" (src),"1" (dest)
: "memory");
return dest;
}
Адрес источника хранится в esi, приемника - в edi, и после того как мы начинаем копирование
и достигаем 0, копирование завершается.
Констрэйнты "&S", "&D", "&a"
говорят , что регистры esi, edi и eax находятся в списке clobber registers.
Память тоже в clobberlist.
Макрос для копирования массива double words :
#define mov_blk(src, dest, numwords) \
__asm__ __volatile__ ( \
"cld\n\t" \
"rep\n\t" \
"movsl" \
: \
: "S" (src), "D" (dest), "c" (numwords) \
: "%ecx", "%esi", "%edi" \
)
Здесь нет outputs, меняется контент регистров ecx, esi и edi , поэтому они добавлены в clobber list.
-
В Linux, system calls реализованы через GCC inline assembly. Посмотрим на пример такой реализации.
Все system calls находятся в (linux/unistd.h).
В следующем примере определен system call с 3-мя аргументами :
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile ( "int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3))); \
__syscall_return(type,__res); \
}
Номер системного вызова находится в eax, параметры - в ebx, ecx, edx.
И вызов "int 0x80" запускает сам system call . Возвращаемое значение попадает в eax.
Вызов system call Exit :
{
asm("movl $1,%%eax; /* SYS_exit is 1 */
xorl %%ebx,%%ebx; /* Argument is in ebx, it is 0 */
int $0x80" /* Enter kernel mode */
);
}
Номер exit = "1" и параметр = 0. eax =1 и ebx = 0 , int $0x80 , exit(0) :
-
Brennan’s Guide to Inline Assembly
-
Using Assembly Language in Linux
-
Using as, The GNU Assembler
-
Using and Porting the GNU Compiler Collection (GCC)
-
Linux Kernel Source
|
AlexAnder | > asm volatile ( ... : ... : ... : ...);
> Обычно не нужно злоупотреблять этим ключевым словом и нужно давать компилятору возможность оптимизировать код - он это делает лучше нас :-)
Neverojatno gluboko oshibochnoe mnenie! Kak govorjat, doverjay, no proverjaj. Ja - vsjo proverjaju. I ludi, kto pishet jadro uz s nimi hotjabi to ne sporte!!! 2012-10-10 19:30:30 | Сергей Яковлев | Да никто и не спорит
Естественно, что системные программисты не доверяют и проверяют
А какого рода работу делаете Вы, если проверяете код, сгенеренный компилятором ? 2012-10-10 22:56:14 | |
|