Библиотека сайта rus-linux.net
Цилюрик О.И. Модули ядра Linux | ||
Назад | Окружение и инструменты | Вперед |
Компилятор GCC
Основным компилятором Linux является GCC. Но могут использоваться и другие, некоторые примеры таких иных компиляторов (используемых разными коллективами в Linux) являются: а). компилятор CC из состава IDE SolarisStudio операционной системы OpenSolaris, б). активно развивающийся в рамках проекта LLVM компилятор Clang (кандидат для замены GCC в FreeBSD, причина — лицензия), в). PCC (Portable C Compiler) — новая реализация компилятора 70-х годов, широко практикуемый в NetBSD и OpenBSD. Тем не менее, вся эта альтернативность возможна только в проектах пользовательского адресного пространства; в программировании ядра и, соответственно, модулей ядра применим исключительно компилятор GCC.
Примечание: Существуют экспериментальные проекты по сборке Linux компилятором, отличным от GCC. Есть сообщения о том, что компилятор Intel C имеет достаточную поддержку расширений GCC чтобы компилировать ядро Linux. Но при всех таких попытках пересборка может быть произведена только полностью, «с нуля»: начиная со сборки ядра и уже только потом сборка модулей. В любом случае, ядро и модули должны собираться одним компилятором.
Начало GCC было положено Ричардом Столлманом, который реализовал первый вариант GCC в 1985 на нестандартном и непереносимом диалекте языка Паскаль; позднее компилятор был переписан на языке Си Леонардом Тауэром и Ричардом Столлманом и выпущен в 1987 как компилятор для проекта GNU (http://ru.wikipedia.org/wiki/GCC). Компилятор GCC имеет возможность осуществлять компиляцию:
- с нескольких языков программирования (точный перечень зависит от опций сборки самого компилятора gcc);
- в систему команд множества (нескольких десятков) процессорных архитектур;
Достигается это 2-х уровневым процессом: а). лексический анализатор (вариант GNU утилиты bison, от общей UNIX реализации анализатора yacc; в комплексе с лексическим анализатором flex) и б). независимый генератор кода под архитектуру процессора.
Одно из свойств (для разработчиков модулей Linux), отличающих GCC в положительную сторону относительно других компиляторов, это расширенная многоуровневая (древовидная) система справочных подсказок, включённых в саму утилиту gcc, начиная с:
$ gcc --version
gcc (GCC) 4.4.3 20100127 (Red Hat 4.4.3-4)
Copyright (C) 2010 Free Software Foundation, Inc.
...
И далее ... самая разная справочная информация, например, одна из полезных — опции компилятора, которые включены по умолчанию при указанном уровне оптимизации:
$ gcc -Q -O3 --help=optimizer
Следующие ключи контролируют оптимизацию: -O<number> -Os -falign-functions [включено] -falign-jumps [включено] ...
Для подтверждения того, что установки опций для разных уровней оптимизации отличаются, и уточнения в чём состоят эти отличия, проделаем следующий эксперимент:
$ gcc -Q -O2 --help=optimizer > O2
$ gcc -Q -O3 --help=optimizer > O3
$ ls -l O*
-rw-rw-r-- 1 olej olej 8464 Май 1 11:24 O2 -rw-rw-r-- 1 olej olej 8452 Май 1 11:24 O3
$ diff O2 O3
... 49c49 < -finline-functions [выключено] --- > -finline-functions [включено] ...
Существует множество параметров GCC, специфичных для каждой из поддерживаемых целевых платформ, которые можно включать при компиляции модулей, например, в переменную EXTRA_CFLAGS используемую Makefile. Проверка платформенно зависимых опций может делаться так:
$ gcc --target-help
Ключи, специфические для целевой платформы: ... -m32 Генерировать 32-битный код i386 ... -msoft-float Не использовать аппаратную плавающую арифметику -msse Включить поддержку внутренних функций MMX и SSE при генерации кода -msse2 Включить поддержку внутренних функций MMX, SSE и SSE2 при генерации кода ...
GCC имеет значительные синтаксические расширения (такие, например, как инлайновые ассемблерные вставки, или использование вложенных функций), не распознаваемые другими компиляторами языка C — ещё и поэтому альтернативные компиляторы вполне пригодны для сборки приложений, но непригодны для пересборки ядра Linux и сборки модулей ядра.
Невозможно в пару абзацев даже просто назвать то множество возможностей, которое сложилось за 25 лет развития проекта, но, к счастью, есть исчерпывающее полное руководство по GCC более чем на 600 страниц, и оно издано в русском переводе [8], которое просто рекомендуется держать под рукой на рабочем столе в качестве справочника.
Ассемблер в Linux
В сложных случаях иногда бывает нужно изучить ассемблерный код, генерируемый GCC как промежуточный этап компиляции. Увидеть сгенерированный GCC ассемблерный код можно компилируя командой с ключами:
$ gcc -S -o my_file.S my_file.c
Примечание: Посмотреть результат ещё более ранней фазы препроцессирования можно, используя редко применяемый ключ -E :
$ gcc -E -o my_preprocessed.c my_file.c
Возможно использование ассемблерного кода для всех типов процессорных архитектур (x86, PPC, MIPS, AVR, ARM, ...) поддерживаемых GCC — но синтаксис записи будет отличаться.
Для генерации кода GCC вызывает as (раньше часто назывался как gas), конфигурированный под целевой процессор:
$ as --version
GNU assembler 2.17.50.0.6-6.el5 20061020 Copyright 2005 Free Software Foundation, Inc. ... This assembler was configured for a target of `i386-redhat-linux'.
Примечание: По моему личному мнению, которое может быть и ошибочно, разработчику модулей ядра Linux совершенно не обязательно умение писать на ассемблере, но в высшей степени на пользу умение хотя бы поверхностно читать написанное не нём. Например, для поиска, в заголовочных файлах или исходных кодах ядра, изменений, произошедших в структурах и API в новой версии ядра.
Нотация AT&T
Ассемблер GCC использует синтаксическую нотацию AT&T, в отличие от нотации Intel (которую используют все инструменты Microsoft, компилятор С/С++ Intel, многоплатформенный ассемблер NASM).
Примечание: Обоснование этому простое - все названные инструменты, использующие нотацию Intel, используют её применительно к процессорам архитектуры x86. Но GCC является много-платформенным инструментом, поддерживающим не один десяток аппаратных платформ, ассемблерный код каждой из этих множественных платформ может быть записан в AT&T нотации.
В AT&T строка записанная как:
movl %ebx, %eax
Выглядит в Intel нотации так:
mov eax, ebx
Основные принципы AT&T нотации:
- Порядок операндов: <Операция> <Источник>, <Приемник> - в Intel нотации порядок обратный.
- Названия регистров имеют явный префикс % указывающий, что это регистр. То есть %eax, %dl, %esi,%xmm1 и т. д. То, что названия регистров не являются зарезервированными словами, — несомненный плюс.
- Явное задание размеров операндов в суффиксах команд: b-byte, w-word, l-long, q-quadword. В командах типа movl %edx, %eax это может показаться излишним, однако является весьма наглядным средством, когда речь идет о: incl (%esi) или xorw $0x7, mask
- Названия констант начинаются с $ и могут быть выражением. Например: movl $1,%eax
- Значение без префикса означает адрес. Это еще один камень
преткновения новичков. Просто следует запомнить, что:
movl $123, %eax — записать в регистр %eax число 123,
movl 123, %eax — записать в регистр %eax содержимое ячейки памяти с адресом 123,
movl var, %eax — записать в регистр %eax значение переменной var,
movl $var, %eax — загрузить адрес переменной var - Для косвенной адресации необходимо использовать круглые скобки. Например: movl (%ebx), %eax — загрузить в регистр %eax значение переменной, по адресу находящемуся в регистре %ebx.
- SIB-адресация: смещение ( база, индекс, множитель ).
Примеры:
popw %ax /* извлечь 2 байта из стека и записать в %ax */
movl $0x12345, %eax /* записать в регистр константу 0x12345
movl %eax, %ecx /* записать в регистр %ecx операнд, который находится в регистре %eax */
movl (%ebx), %eax /* записать в регистр %eax операнд из памяти, адрес которого
находится в регистре адреса %ebx */
Пример: Вот как выглядит последовательность ассемблерных инструкций для реализации системного вызова на exit( EXIT_SUCCESS ) на x86 архитектуре:
movl $1, %eax /* номер системного вызова exit - 1 */ movl $0, %ebx /* передать 0 как значение параметра */ int $0x80 /* вызвать exit(0) */
Инлайновый ассемблер GCC
GCC Inline Assembly — встроенный ассемблер компилятора GCC, представляющий собой язык макроописания интерфейса компилируемого высокоуровнего кода с ассемблерной вставкой.
Синтаксис инлайн вставки в C-код - это оператор вида:
asm [volatile] ( "команды и директивы ассемблера" "как последовательная текстовая строка" : [<выходные параметры>] : [<входные параметры>] : [<изменяемые параметры>] );
В простейшем случае это может быть:
asm [volatile] ( "команды ассемблера" );
Примеры:
1. то, как записать несколько строк инструкций ассемблера:
asm volatile( "nop\n" "nop\n" "nop\n" );
2. пример выполнения системного вызова write(), (показанный ранее в архиве int80.tgz):
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; }
Для чего в случае asm служит ключевое слово volatile? Для того чтобы указать компилятору, что вставляемый ассемблерный код может давать побочные эффекты, поэтому попытки оптимизации могут привести к логическим ошибкам.
Пример использования ассемблерного кода
Для сравнения того, как внешне выглядит функционально идентичный код, записанный на C (gas2_0.c), в виде ассемблерного файла (gas2_1.c) и инлайновой ассемблерной вставки (gas2_2.c), рассмотрим такой пример (архив gas-prog.tgz); прежде всего его сценарий сборки :
Makefile :
LIST = gas1 gas2_0 gas2_1 gas2_2 all: $(LIST) gas2_1: gas2_1.c exit.S gcc -c gas2_1.c -o gas2_1.o gcc -c exit.S -o exit.o gcc gas2_1.o exit.o -o gas2_1 rm -f *.o # gas2_0 и gas2_2 собираются по умолчанию на основании суффикса, и не требуют целей
И далее сами файлы реализации:
gas2_0.c :
#include <stdio.h> #include <stdlib.h> int main( int argc, char *argv[] ) { printf( "----- begin prog\n" ); int ret = 7; exit( ret ); printf( "----- final prog\n" ); return 0; // never! };
gas2_1.c :
#include <stdio.h> extern void asmexit( int retcod ); int main( int argc, char *argv[] ) { printf( "----- begin prog\n" ); int ret = 7; asmexit( ret ); printf( "----- final prog\n" ); return 0; // never! };
exit.S :
# коментарий может начинаться или с # как AT&T, // так и ограничиваться как в C: // & /* ... */ /* void asmnexit( int retcod ); */ .globl asmexit .type asmexit, @function asmexit: pushl %ebp // соглашение о связях movl %esp, %ebp movl $1, %eax movl 8(%ebp), %ebx int $0x80 popl %ebp // соглашение о связях ret
gas2_2.c :
#include <stdio.h> int main( int argc, char *argv[] ) { printf( "----- begin prog\n" ); int ret = 7; asm volatile ( "movl $1, %%eax\n" "movl %0, %%ebx\n" "int $0x80\n" : : "b"(ret) : "%eax" ); printf( "----- final prog\n" ); return 0; // never! };
Убеждаемся, что по исполнению все три варианта абсолютно идентичные:
$ ./gas2_0
----- begin prog
$ echo $?
7
$ ./gas2_1
----- begin prog
$ echo $?
7
$ ./gas2_2
----- begin prog
$ echo $?
7
$ echo $?
0
Предыдущий раздел: | Оглавление | Следующий раздел: |
Подсистема X11, терминал и текстовая консоль | О сборке модулей детальнее |