Содержание


Разработка модулей ядра Linux

Часть 15. Ассемблерные возможности компилятора GCC

Comments

Серия контента:

Этот контент является частью # из серии # статей: Разработка модулей ядра Linux

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Разработка модулей ядра Linux

Следите за выходом новых статей этой серии.

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

Нотация AT&T

Синтаксис ассемблера, используемый в GCC, называется нотацией AT&T и отличается от привычной многим нотации Intel (используемой во всех инструментах Microsoft, компиляторе С/С++ Intel и мульти-платформенном ассемблер NASM).

Примечание: У этого факта есть простое объяснение, так как все названные инструменты, использующие нотацию Intel, применяют её исключительно для процессоров архитектуры Intel x86. Но GCC является мульти-платформенным инструментом, поддерживающим не один десяток аппаратных платформ, и ассемблерный код каждой из этих платформ может быть записан в AT&T нотации.

Строка, записанная в нотации AT&T как:

movl %ebx, %eax

В нотации Intel выглядит следующим образом:

mov eax, ebx

Ниже перечислены некоторые из правил нотации AT&T (не все, но этого достаточно для представления общей картины), принципиально отличающиеся от более привычной нотации Intel:

  1. Порядок операндов (направление выполнения операции) слева - направо: <Операция> <Источник> <Приемник>, в нотации Intel используется обратный порядок (справа - налево).
  2. В названиях регистров имеется обязательный префикс %, указывающий, что это регистр: %eax, %dl и т.д. То, что названия регистров не являются зарезервированными словами, это несомненное преимущество такого решения, так как для различных процессорных архитектур могут использоваться разные формы записи, например: %1, %2 и т.д. (VAX, Motorola 68000).
  3. Обязательное указание размеров операндов в суффиксах команд: b - байт, w — 16 бит, l — 32 бит, q — 64 бит. В командах типа movl %edx, %eax это может показаться и излишним, однако будет весьма полезным для таких выражений как: incl (%esi) или xorw $0x7, mask.
  4. Названия констант начинаются с литеры $ и могут быть выражением. Например: movl $1, %eax.
  5. Значение без префикса всегда означает адрес, поэтому просто следует запомнить, что:
    • movl $123, %eax — записать в регистр %eax число 123;
    • movl 123, %eax — записать в регистр %eax содержимое ячейки памяти с адресом 123;
  6. Для косвенной адресации необходимо использовать круглые скобки (в нотации Intel обычно используются квадратные). Например: movl (%ebx), %eax — загрузить в регистр %eax значение переменной по адресу, находящемуся в регистре %ebx.
Листинг 1. Однострочные примеры в нотации AT&T
popw  %ax             /* извлечь 2 байта из стека и записать в %ax */
movl  $0x12345, %eax  /* записать в регистр константу 0x12345
movl  %eax, %ecx      /* записать в регистр %ecx операнд, 
                         который находится в регистре %eax */
movl  (%ebx), %eax    /* записать в регистр %eax операнд из памяти, 
                         адрес которого находится в регистре адреса %ebx */

В листинге 2 показано, как выглядит последовательность ассемблерных инструкций для реализации системного вызова exit( EXIT_SUCCESS ) на архитектуре x86.

Листинг 2. Пример выполнения системного вызова exit() в Linux-системе
movl  $1, %eax        /* номер системного вызова exit - 1   */
movl  $0, %ebx        /* передать 0 как значение параметра  */
      int $0x80       /* вызвать exit(0)                    */

В GCC ассемблер можно использовать двумя способами:

  1. Создать отдельный файл с расширением .S, содержащий только ассемблерный код, затем откомпилировать его, вызвав gcc (или даже непосредственно программу as), и позже скомпоновать полученный объектный файл с другими частями проекта;
  2. Использовать механизм инлайновых включений коротких фрагментов на ассемблере непосредственно в исходный код на языке С. При этом ассемблерные включения имеют доступ к именам (переменным, функциям) во включающем коде высокого уровня. Это важное синтаксическое преимущество, доступное в компиляторе GCC.

Инлайновый ассемблер GCC

GCC Inline Assembly — встроенный ассемблер компилятора GCC, представляющий собой язык макроописания интерфейса компилируемого высокоуровневого кода с ассемблерной вставкой.

Синтаксис инлайновой вставки для C-кода выглядит следующим образом:

asm [volatile] ( 
    "команды и директивы ассемблера"
    "как последовательная текстовая строка"
    : [<выходные параметры>] : [<входные параметры>] : [<изменяемые параметры>]
    );

В инлайновых ассемблерных вставках также используется нотация AT&T. Чем же является такая конструкция с точки зрения синтаксиса? Это макровызов, в который весь ассемблерный код передаётся как одна символьная строка (заключённая в кавычки). Но эта строка, по правилам C, может разрываться, переноситься, продолжаться следующей строкой. В простейшем случае это выглядит так:

asm [volatile] ( "команды ассемблера" );

Несколько примеров использования инлайнового ассемблера

Несколько строк, содержащих инструкции ассемблера, можно записать в одном ассемблерном фрагменте:

asm volatile( "nop\n" 
              "nop\n" 
              "nop\n" 
            );

Ключевое слово volatile в инструкции asm используется для того, чтобы указать компилятору, что вставляемый ассемблерный код может обладать побочными эффектами, поэтому попытки оптимизации могут привести к логическим ошибкам.

В листинге 3 приведен пример выполнения системного вызова write(), (показанный ранее в архиве int80.tgz):

Листинг 3. Реализация системного вызова write() в Linux
int write_call( int fd, const char* str, int len ) { 
   long __res; 
   __asm__ volatile ( "int $0x80": 
        "=a" (__res):
        "0"(__NR_write),"b"((long)(fd)),
        "c"((long)(str)),"d"((long)(len)) ); 
   return (int) __res; 
}

В листинге 4 показано, как в форме инлайнового включения определить в программе макровызов с параметрами (произвольный в общем случае или умножения на 5 в конкретном показанном примере). Для этого определяется новый функциональный вызов, который вызывается позже из С-кода с помощью стандартного формата вызова.

Листинг 4. Определение функции times5()
times5( n, n ) :  
#define times5(arg1, arg2) \ 
__asm__ ( "leal (%0,%0,4),%0" \ 
          : "=r" (arg2) \ 
          : "r" (arg1) );

Инлайновые включения в gcc являются отдельной интересной темой, особенно для читателей, занимающихся созданием модулей ядра и драйверов устройств, но выходящей за рамки данного цикла статей. Некоторое количество примеров, в виде работающих программ, показано в прилагаемых архивах для самостоятельного рассмотрения. Интересующимся может оказаться интересным перевод статьи "GCC-Inline-Assembly-HOWTO" (см. ссылку в разделе "Ресурсы").

Сравнение двух подходов к использованию ассемблера в GCC

Осталось рассмотреть только различные варианты выполнения операции, эквивалентной системному вызову exit(), чтобы научиться отличать их друг от друга. Для интересующихся читателей, в разделе "Материалы для скачивания" представлены архивы gas-prog.tgz и as-inline.tgz с исходным кодом обсуждаемых примеров. Сами архивы из-за большого объема в статье рассматриваться не будут, так как там показаны образцы различных приёмов использования ассемблера.

Вот как выглядит эта операция в отдельном файле exit.S, который содержит только ассемблерный код в нотации AT&T и должен быть автономно cкомпилирован, и только потом собран воедино с остальными частями приложения:

        movl    $1, %eax 
        movl    8(%ebp), %ebx 
        int     $0x80

А вот полный функциональный аналог показанного выше действия, но записанный в форме инлайновой ассемблерной вставки в теле С-кода внутри файла gas2_2.c:

asm volatile ( "movl  $1, %%eax\n" 
               "movl  %0, %%ebx\n" 
               "int $0x80\n" : : "b"(ret) : "%eax" );

В коде ядра Linux присутствует код, выполненный как в одной стилистике, так и в другой. Эти подходы отличаются функциональными возможностями и предназначением. В качестве отдельно компилируемых ассемблерных файлов, например, часто пишутся коды инициализации аппаратуры (Board Support Package, BSP, пакет поддержки платформы) для разных процессорных платформ (во многих случаях без привычной поддержки BIOS). В качестве инлайновых включений, с другой стороны, написан тонкий интерфейсный слой к системным вызовам Linux (<linux/syscalls.h>), который отличается для каждой архитектуры, но создающий одинаковую картину системных вызовов при взгляде "сверху".

Заключение

В статье был выполнен минимальный обзор возможностей использования языка ассемблера. Вряд ли вам часто придётся прибегать к написанию ассемблерного кода в Linux. Но представление о нём, хотя бы в объёме предложенного обзора, необходимо для понимания некоторых примеров, как из числа обсуждавшихся ранее, так и ожидающихся далее.

Важная деталь, которую часто упускают из вида: ассемблерные фрагменты могут быть написаны с большей изобретательностью и обладать высокой производительностью. Но при этом не следует забывать, что такой код сразу становится "привязанным" только к одной конкретной процессорной архитектуре. Подобные эффекты могут вовсе не иметь никакого значения в операционных системах, работающих только на одной единственной архитектуре (Windows), или крайне ограниченном наборе возможных платформ (Solaris). Но это может стать существенным ограничивающим фактором для Linux, который с одинаковым успехом работает на более чем десятке различных архитектур.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=819305
ArticleTitle=Разработка модулей ядра Linux: Часть 15. Ассемблерные возможности компилятора GCC
publish-date=06012012