Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
iakovlev.org

GCC-Inline-Assembly-HOWTO

Sandeep.S

v0.1, 01 March 2003.


Этот HOWTO обьясняет как использовать inline assembly. Для чтения этой статьи нужны базовые знани по x86 assembly и C.

1. Introduction.

2. Overview of the whole thing.

3. GCC Assembler Syntax.

4. Basic Inline.

5. Extended Asm.

6. More about constraints.

7. Some Useful Recipes.

8. Concluding Remarks.

9. References.


1. Introduction.

1.1 Copyright and License.

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.


2. Общий обзор.

Под понятием inline-кода имеются ввиду функции , код которых реально вставляется в том месте, откуда он вызывается . Это немного похоже на макросы .

В чем преимущество inline functions?

Этот метод уменьшает функциональный оверхед . Для обьявления такой функции нужно использовать ключевое слово inline .

inline assembly - это несколько строчек кода на асме , записанных в теле inline functions. Этот прием используется в системном программировании . Для декларирования inline assembly функции используется ключевое слово asm.

Внутри таких функций можно оперировать внешними си-шными переменными .


3. GCC Assembler Syntax.

GCC, GNU C компилятор для Linux, использует AT&T& UNIX синтаксис. AT&T syntax немного отличается от Intel syntax , который используется например в винде . Отличия :

  1. Иной порядок Source-Destination .

    В Intel 1-й операнд - destination, 2-й - source . В AT&T все наоборот : 1-й - source и 2-й - destination. Т.е.

    "Op-code dst src" - Intel

    "Op-code src dst" - AT&T .

  2. Наименовани регистров

    В AT&T перед именем регистра идет префикс процент % , т.е например для eax будет %eax.

  3. Immediate Operand.

    AT&T immediate - операнд идет с префиксом доллара ’$’. Для статических "C" также идет префикс доллара ’$’. Для Intel , 16-ричные константы идут с префиксом ’h’ , а для AT&T идет другой префикс ’0x’ . В результате для AT&T для 16-ричных мы имеем сначала ’$’, потом ’0x’ и потом уже сам операнд.

  4. 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 .

  5. 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 |
 +------------------------------+------------------------------------+
 


4. Basic Inline.

Базовый формат 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)");
 


5. Extended Asm.

В базовом 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.2 Operands.

В шаблоне каждый операнд есть ссылка на число . Первый операнд имеет индекс ноль , и далее по возрастанию .

Давайте рассмотрим пример с умножением числа на 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) 
              );
 

5.3 Clobber List.

Регистры , помещаемые в этот список , говорят компилятору , что их будут модифицировать внутри asm. Нам этот список не нужен , потому что мы раньше указали , с каким регистром мы работаем явно .

Наполнив этот список какими-то регистрами , мы можем много раз их использовать Рассмотрим пример с вызовом процедуры _foo , аргументы которой прописаны в регистрах eax и ecx.


        asm ("movl %0,%%eax;
               movl %1,%%ecx;
               call _foo"
              : /* no outputs */
              : "g" (from), "g" (to)
              : "eax", "ecx"
              );
 

5.4 Volatile ...?

Если вам приходилось копаться в исходниках ядра , вы наверно обратили внимание на ключевое слово volatile или __volatile__.

Если ассемблерная инструкция должна быть выполнена там , где она положена , т.е. например не может быть перемещена куда-то в другое место при отимизации , мы используем ключевое слово volatile внутри asm .

asm volatile ( ... : ... : ... : ...);

Обычно не нужно злоупотреблять этим ключевым словом и нужно давать компилятору возможность оптимизировать код - он это делает лучше нас :-)


6. Constraints.

Констрэйнты выполняют большой обьем работы внутря inline assembly. Они указывают на регистр , в котором находится операнд , или на память , и т.д.

6.1 Часто используемые constraints.

  1. 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        |
     +---+--------------------+
     

  2. Matching(Digit) constraints

    Иногда одна переменная может обслуживать одновременно и input и output-операнды.

    asm ("incl %0" :"=a"(var):"0"(var));

    В данном случае регистр eax используется для input и output . В этом примере переменная var попадает в eax , там изменяется , и это измененное значение регистра eax попадает назад в память по адресу , в котором находится переменная var :-)

    Другие случаи применения constraints:

    1. "m" : memory operand
    2. "o" : memory operand с адресом , к которому прибавляется смещение из таблицы
    3. "V" : противовес предыдущему варианту
    4. "i" : immediate integer operand с заранее неизвестным значением
    5. "n" : immediate integer operand с заданным значением
    6. "g" : любой регистр , память или переменная

    Следующие констрэйнты специфичны только для x86 .

    1. "r" : Register operand constraint, look table given above.
    2. "q" : Registers a, b, c or d.
    3. "I" : Constant in range 0 to 31 (for 32-bit shifts).
    4. "J" : Constant in range 0 to 63 (for 64-bit shifts).
    5. "K" : 0xff.
    6. "L" : 0xffff.
    7. "M" : 0, 1, 2, or 3 (shifts for lea instruction).
    8. "N" : Constant in range 0 to 255 (for out instruction).
    9. "f" : Floating point register
    10. "t" : First (top of stack) floating point register
    11. "u" : Second floating point register
    12. "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.

    6.2 Constraint Modifiers.

  3. "=" Операнд типа output write-only
  4. "&" : Означает , что операнд будет модифицирован прежде чем input-инструкция будет закончена .


7. Несколько полезных примеров.

  1. Программа для сложения 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 - это целое и должно быть помещено в регистр .

  2. Теперь несколько операций с регистрами/переменными и сравнениями :


     __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 , то код изменяет контент памяти .

  3. Как установить/очистить бит в регистре ?


    __asm__ __volatile__(   "btsl %1,%0"
                           : "=m" (ADDR)
                           : "Ir" (pos)
                           : "cc"
                           );
     

    Здесь бит в позиции ’pos’ для переменной ADDR (в памяти) устанавливается в 1 Можно использовать ’btrl’ для ’btsl’ для установки бита в ноль. Констрэйнт "Ir" для pos говорит , что pos храним в регистре, и его значение в диапазоне 0-31 .

  4. Копирование строки :


    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.

  5. В 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) :


9. References.

  1. Brennan’s Guide to Inline Assembly
  2. Using Assembly Language in Linux
  3. Using as, The GNU Assembler
  4. Using and Porting the GNU Compiler Collection (GCC)
  5. 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