Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux
Цилюрик О.И. Модули ядра 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 нотации:

  1. Порядок операндов: <Операция> <Источник>, <Приемник> - в Intel нотации порядок обратный.
  2. Названия регистров имеют явный префикс % указывающий, что это регистр. То есть %eax, %dl, %esi,%xmm1 и т. д. То, что названия регистров не являются зарезервированными словами, — несомненный плюс.
  3. Явное задание размеров операндов в суффиксах команд: b-byte, w-word, l-long, q-quadword. В командах типа movl %edx, %eax это может показаться излишним, однако является весьма наглядным средством, когда речь идет о: incl (%esi) или xorw $0x7, mask
  4. Названия констант начинаются с $ и могут быть выражением. Например: movl $1,%eax
  5. Значение без префикса означает адрес. Это еще один камень преткновения новичков. Просто следует запомнить, что:
    movl $123, %eax — записать в регистр %eax число 123,
    movl 123, %eax — записать в регистр %eax содержимое ячейки памяти с адресом 123,
    movl var, %eax — записать в регистр %eax значение переменной var,
    movl $var, %eax — загрузить адрес переменной var
  6. Для косвенной адресации необходимо использовать круглые скобки. Например: movl (%ebx), %eax — загрузить в регистр %eax значение переменной, по адресу находящемуся в регистре %ebx.
  7. 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, терминал и текстовая консоль   О сборке модулей детальнее