Коллекция компиляторов GNU (GCC) — это система компиляторов, разработанная проектом GNU, которая поддерживает различные языки программирования. GCC — это стандартный компилятор для большинства Unix-подобных операционных систем, а также ключевой компонент цепочки инструментов GNU. В этой статье я объясню, что именно происходит на каждом этапе процесса компиляции. В общих чертах последовательность событий после того, как команда компиляции передана GCC, включает:
- Предварительная обработка
- Сборник
- сборка
- Связывание
Привет мир!
В качестве примера мы рассмотрим каждый этап компиляции, используя всеми любимую программу «Hello World» main.c
. Код для main.c
выглядит следующим образом:
#include <stdio.h> int main (void) { printf ("Hello, world!\n"); return 0; }
Важно отметить, что GCC выполняет каждую последовательность команд автоматически, не сообщая о том, что именно происходит за кулисами. Однако, если вы хотите увидеть выполнение каждого отдельного шага, gcc -v main.c
запустит подробную компиляцию, которая отобразит подробную информацию о выполняемых командах. Первая строка программы, #include<stdio.h>
, представляет собой заголовочный файл, определяющий процедуры ввода/вывода, используемые нашей программой. Следующая строка кода, int main (void)
, является «точкой входа» в нашу программу. За ним следует printf
, который выводит символы между кавычками в «стандартный вывод». Символ \n
не печатается, потому что gcc интерпретирует его как «новую строку». Наконец, программа возвращает целое число 0
в последней строке кода, поскольку мы определили основную процедуру как возвращающую целое число (int
).
Препроцессор
Когда мы пишем программу на C, первым этапом процесса компиляции является развертывание макросов и включенных заголовочных файлов через препроцессор. Для этого GCC выполняет следующую команду:
$ cpp main.c > main.i
Полученный файл main.i
содержит исходный код с расширенными макросами. В соответствии с соглашением программирования C предварительно обработанные файлы помечаются расширением '.i'
.
Компилятор
На этом этапе происходит компиляция предварительно обработанного исходного кода на ассемблере для конкретного процессора. В командной строке, чтобы преобразовать предварительно обработанный исходный код C в язык ассемблера, введите следующий код и нажмите Enter:
$ gcc -Wall -S main.i
Обратите внимание, что запущенный gcc
с параметром -S
не создаст объектный файл. В результате получается файл main.s
. Чтобы увидеть язык ассемблера, хранящийся в файле, введите в командной строке следующее:
$ cat main.s
Ассемблер
Как следует из названия, ассемблер преобразует язык ассемблера в машинный код и генерирует объектный файл. Когда в исходном файле сборки выполняются вызовы внешних функций, ассемблер оставляет адреса внешних функций неопределенными, чтобы компоновщик мог заполнить их на следующем шаге процесса. Чтобы сгенерировать объектный файл с помощью ассемблера, введите и выполните эту команду:
$ as main.s -o main.o
Параметр -o
выше указывает, что вывод должен быть направлен в файл main.o
. Файл main.o
содержит инструкции на языке, понятном машине, для запуска нашей программы. Как я упоминал ранее, в этот момент любые вызовы внешних функций будут неопределенными.
Линкер
На этом этапе процесса компиляции компоновщик приступает к связыванию объектных файлов для создания исполняемого файла. Создание исполняемого файла обычно требует вызовов внешних функций из библиотек времени выполнения C и системы. Например, полная команда для связывания нашей программы:
$ ld -dynamic-linker /lib/ld-linux.so.2 /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc-lib/i686/3.3.1/crtbegin.o -L/usr/lib/gcc-lib/i686/3.3.1 main.o -lgcc -lgcc_eh -lc -lgcc -lgcc_eh /usr/lib/gcc-lib/i686/3.3.1/crtend.o /usr/lib/crtn.o
К счастью, нам никогда не потребуется напрямую вводить эту гигантскую команду, потому что процесс связывания незаметно обрабатывается gcc
при вводе следующего кода:
$ gcc main.o
Эта команда связывает объектный файл main.o
со стандартной библиотекой C, после чего создается исполняемый файл с именем a.out
. Чтобы запустить программу Hello World, просто введите:
$ ./a.out
Вывод должен выглядеть так, если все сделано правильно:
$ ./a.out Hello, world!
Теперь, когда мы понимаем, сколько всего требуется на каждом этапе процесса компиляции, мы можем по-настоящему оценить простоту ввода gcc main.c
в терминал и предоставления GCC выполнения тяжелой работы.
Источник: http://www.network-theory.co.uk/docs/gccintro/gccintro_87.html