Разработка модулей ядра Linux
Часть 15. Ассемблерные возможности компилятора GCC
Серия контента:
Этот контент является частью # из серии # статей: Разработка модулей ядра 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:
- Порядок операндов (направление выполнения операции) слева - направо:
<Операция> <Источник> <Приемник>
, в нотации Intel используется обратный порядок (справа - налево). - В названиях регистров имеется обязательный префикс %, указывающий, что это регистр:
%eax, %dl
и т.д. То, что названия регистров не являются зарезервированными словами, это несомненное преимущество такого решения, так как для различных процессорных архитектур могут использоваться разные формы записи, например:%1, %2
и т.д. (VAX, Motorola 68000). - Обязательное указание размеров операндов в суффиксах команд: b - байт, w — 16 бит, l — 32 бит, q — 64 бит. В командах типа
movl %edx, %eax
это может показаться и излишним, однако будет весьма полезным для таких выражений как:incl (%esi)
илиxorw $0x7, mask
. - Названия констант начинаются с литеры $ и могут быть выражением. Например:
movl $1, %eax
. - Значение без префикса всегда означает адрес, поэтому просто следует запомнить, что:
movl $123, %eax
— записать в регистр %eax число 123;movl 123, %eax
— записать в регистр %eax содержимое ячейки памяти с адресом 123;
- Для косвенной адресации необходимо использовать круглые скобки (в нотации 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 ассемблер можно использовать двумя способами:
- Создать отдельный файл с расширением .S, содержащий только ассемблерный код, затем откомпилировать его, вызвав gcc (или даже непосредственно программу as), и позже скомпоновать полученный объектный файл с другими частями проекта;
- Использовать механизм инлайновых включений коротких фрагментов на ассемблере непосредственно в исходный код на языке С. При этом ассемблерные включения имеют доступ к именам (переменным, функциям) во включающем коде высокого уровня. Это важное синтаксическое преимущество, доступное в компиляторе 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, который с одинаковым успехом работает на более чем десятке различных архитектур.
Ресурсы для скачивания
- этот контент в PDF
- Образец кода (as-inline.tgz | 2KB)
- Образец кода (gas-prog.tgz | 2KB)
Похожие темы
- Инструкция по работе с примерами исходного кода в цикле статей "Разработка модулей ядра Linux
- GCC-Inline-Assembly-HOWTO: статья, посвященная ассемблерным возможностям компилятора GCC
- Разработка модулей ядра Linux: Часть 14. Компилятор GCC