Компиляция и компоновка с помощью GNU Compiler Collection (GCC)

GCC - GNU compiler collection – это инструментальное средство разработки программ на языках Си, Си++, Фортран и других.

Подробнее GCC the GNU Compiler Collection (англ.)

В состав GCC входят:

  • Препроцессоры программ на языках Си и Си++.
  • Компиляторы для поддерживаемых языков. В мире Unix под компилятором (в узком смысле) понимается программа, выдающая в качестве результата текст программы на языке ассемблера.
  • Стандартные библиотеки языков Си++ и других (кроме Си).
  • Программы-драйверы компиляции, которые предоставляют универсальный интерфейс командной строки ко всем компонентам GCC и связанным с ними системным утилитам. Например, программа gcc позволяет управлять компиляцией программ на Си, g++ - компиляцией программ на Си++ и т. д.

В состав GCC не входят:

  • Ассемблер (GNU Assembler, команда as), компоновщик (GNU linker, команда ld1 ) и некоторые другие утилиты для работы с объектными и исполняемыми файлами. В Linux они находятся в инсталляционном пакете binutils.
  • Заголовочные файлы и объектные модули стандартной библиотеки языка Си. В Linux они находятся в инсталляционных пакетах glibc, glibc-devel, glibc-static

Тем не менее, они необходимы для компиляции программ на Си, ввиду чего будут рассмотрены наряду с инструментами GCC. Команда запуска GCC для языка Си в общем виде выглядит следующим образом:

gcc <параметры>

где в параметрах могут идти вперемешку имена входных файлов для компиляции и опции, управляющие компиляцией. В дальнейших разделах использование gcc описывается более подробно.

Схема трансляции программ написанных на Си 

Трансляция программы состоит из следующих этапов:

  • препроцессирование;
  • трансляция в ассемблер;
  • ассемблирование;
  • компоновка.

Традиционно исходные файлы программы на языке Си имеют суффикс имени файла .c, заголовочные файлы для программы на Си имеют суффикс .h. В файловых системах Unix регистр букв значим, и если, например, имя файла имеет суффикс .C, такой файл считается содержащим текст программы на языке Си++, и будет компилироваться компилятором языка Си++, а не Си

Препроцессирование.

Препроцессор просматривает входной .c файл, исполняет содержащиеся в нём директивы препроцессора, в частности, включает в него содержимое других файлов, указанных в директивах #include.

Файл-результат препроцессирования не содержит директив препроцессора, не раскрытых макросов, вместо директив #include в файл-результат подставлено содержимое соответствующих заголовочных файлов. Файл с результатом препроцессирования обычно имеет суффикс .i, однако после завершения трансляции все промежуточные временные файлы по умолчанию удаляются, поэтому чтобы увидеть результат препроцессирования (что, например, бывает полезно при отладке ошибок, связанных с небрежным использованием макросов) нужно использовать опцию -E командной строки gcc. Результат препроцессирования называется единицей трансляции (англ., translation unit) или единицей компиляции (англ., compilation unit)

Трансляция в ассемблер.

На вход подаётся одна единица трансляции, а на выходе (при отсутствии синтаксических и семантических ошибок) выдаётся файл на языке ассемблера для (как правило) машины, на которой ведётся трансляция. Файл с оттранслированной программой на языке ассемблера имеет суффикс имени .s, но точно так же, как и результат работы препроцессора, он по умолчанию удаляется.

Ассемблирование.

На этой стадии работает ассемблер. Он получает на входе результат работы предыдущей стадии и генерирует на выходе объектный файл. Объектные файлы в UNIX имеют суффикс .o

Компоновка.

Компоновщик получает на вход набор объектных файлов, соответствующим единицам трансляции, составляющим программу, подключает к ним стандартную библиотеку языка Си и библиотеки, указанные пользователем, и на выходе получает исполняемую программу.

Запуск транслятора gcc

Рассмотрим основные возможности транслятора GNU Си. В командной строке задаётся список файлов для обработки. Какие операции необходимо выполнить с файлами – зависит от суффикса имен файлов. Возможные суффиксы перечислены в таблице ниже. Если имя файла имеет нераспознанный суффикс, это имя передаётся компоновщику

Суффикс имени файла Выполняемые действия
.h Заголовочный файл на языке Си. Не должен использоваться в аргументах команды gcc. Попытка трансляции такого файла вызывает сообщение об ошибке.
.c Файл на языке Си. Выполняется препроцессирование, трансляция ассемблирование и компоновка.
.i Препроцессированный файл на языке Си. Выполняется трансляция, ассемблирование и компоновка.
.s Препроцессированный файл на языке Си. Выполняется трансляция, ассемблирование и компоновка.
.S Файл на языке ассемблера. Выполняется препроцессирование, ассемблирование и компоновка
.o Объектный файл. Выполняется компоновка.
.a Файл статической библиотеки. Выполняется компоновка.

Действия по трансляции файла определяются для каждого указанного в командной строке файла индивидуально. Например, если в командной строке указаны имена файлов 1.c и 2.o, то для первого файла будут выполнены все шаги трансляции, а для второго – только компоновка. Исполняемый файл будет содержать результат трансляции первого файла, скомпонованный со вторым файлом и стандартными библиотеками.

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

Опция Описание
-E Остановиться после препроцессирования. Результат работы препроцессора выводится по умолчанию на стандартный поток вывода. Имя выходного файла можно указать с помощью опции -o. При этом если в командной строке указано несколько файлов, то в выходной файл будет помещён результат препроцессирования последнего файла.
-S Остановиться после трансляции в ассемблер. По умолчанию имя выходного файла получается из имени входного файла заменой суффикса .c или .i на суффикс .s. Явное имя выходного файла можно указать с помощью опции -o. Попытка использования опции -o и нескольких имён входных файлов вызывает сообщение об ошибке.
-c Остановиться после ассемблирования. По умолчанию имя выходного файла получается из имени входного файла заменой суффикса его имени на суффикс .o. Явное имя выходного файла можно указать с помощью опции -o, которая несовместима с указанием одновременно нескольких транслируемых файлов.
Если ни одной из перечисленных выше опций не задано, выполняются все стадии трансляции. Имя выходного файла по умолчанию равно a.out, но может быть изменено с помощью опции -o.
-o Позволяет задать явное имя выходного файла для любой стадии трансляции.

Например, командная строка


gcc 1.c 2.c -o 1

транслирует два файла на языке Си, объединяя их в одну программу с именем 1.

Командная строка


gcc 3.o 4.o -o 3 -lm

компонует два объектных файла, добавляя к ним стандартную библиотеку языка Си и стандартную математическую библиотеку (опция -lm), и помещает результат в исполняемый файл с именем 3. Прочие полезные опции транслятора gcc перечислены в таблице.

Опция Описание
-I PATH Добавляет каталог PATH в начало списка каталогов, которые просматриваются препроцессором при поиске файлов, подключаемых директивой #include. В командной строке может быть указано несколько опций -I, тогда каталоги просматриваются в порядке, в котором они указаны в командной
-D NAME Определяет макрос с именем NAME, который получает значение 1.
-D NAME=VALUE Определяет макрос с именем NAME, который получает заданное значение VALUE.
-Wall Включает выдачу большого количества предупреждающих сообщений, которые по умолчанию не выдаются. Опция должна использоваться при компиляции программ, все предупреждающие сообщения компилятора должны быть внимательно проанализированы, поскольку могут указывать на ошибки в программе.
-g Включает генерацию отладочной информации в исполняемую программу. Наличие отладочной информации позволяет отлаживать программу в терминах исходного языка, а не машинного кода
-O2 Включает большинство оптимизаций программы, которые одновременно уменьшают размер программы и увеличивают скорость её выполнения.
-L PATH Добавляет путь PATH в начало списка каталогов, которые просматриваются редактором связей при поиске библиотек, указанных с помощью опции -L. Если в командной строке указано несколько опций -L, они добавляются в том же порядке, в котором указаны в командной строке
-l name Добавляет библиотеку name к списку библиотек, которые участвуют в компоновке программы (обратите внимание на отсутствие пробела между опцией и именем библиотеки). В системах Unix редактор связей просматривает библиотеки один раз, поэтому неправильный порядок задания библиотек может привести к тому, что некоторые имена останутся неопределёнными, и компиляция завершится с ошибкой. Файл, хранящий библиотеку с именем name, называется libname.a, если библиотека статическая, и libname.so, если библиотека динамическая.
-static Указывает, что при компоновке не должны использоваться динамические библиотеки. Реализации всех используемых в программе функций будут добавлены непосредственно в исполняемый файл. Размер исполняемого файла программы может вырасти на сотни килобайт, зато такая программа перестанет быть зависимой от динамических библиотек. На некоторых системах могут отлаживаться только статически скомпонованные программы.

Использование стандартных библиотек языка Си

В языках Си и Си++ библиотеки состоят из двух частей:

  • Заголовочных файлов, содержащих объявления типов данных, констант, прототипов функций и внешних переменных, которые подключаются к исходным файлам на этапе препроцессирования, формируя единицы трансляции.

  • Файлов реализации, содержащих тела функций и определения переменных, которые подключаются к программе на этапе компоновки (в случае статических библиотек) или на этапе выполнения (в случае динамических библиотек).

Заголовочные файлы стандартной библиотеки находятся в каталоге /usr/include и его подкаталогах, например, /usr/include/stdio.h или /usr/include/sys/types.h. Программа-драйвер gcc автоматически добавляет этот каталог в список для поиска заголовочных файлов, поэтому каталог /usr/include не нужно задавать в опции –I.

Файлы динамических библиотек размещаются в каталоге /lib или /usr/lib, а файлы статических библиотек – в каталоге /usr/lib. Они задаются автоматически и опция –L для них не нужна. Файл динамической библиотеки языка Си называется libc.so и полный путь к нему – /lib/libc.so.

Таким образом, если выписать явно пути и библиотеки, задаваемые при компиляции программы на Си с помощью gcc неявно, мы получим примерно следующую командную строку:


gcc -I/usr/include -L/lib -L/usr/lib jeltz.c –lc 

Исключением являются математические функции стандартной библиотеки Си, объявленные в заголовочном файле <math.h>, например, sin. Их реализации вынесены в отдельную библиотеку libm.so (libm.a), которая не указывается в списке подключаемых библиотек по умолчанию. Для компоновки программ, использующих математические функции, необходимо в командной строке gcc указать опцию -lm:


gcc -Wall -O2 marvin.c -omarvin –lm

Для того, чтобы увидеть все пути, передаваемые драйвером компиляции gcc препроцессору, компилятору, ассемблеру и компоновщику, можно использовать опцию -v:


gcc -g -O0 -v prosser.c -o prosser

Компоновка программы

Если исполняемая программа компонуется из нескольких единиц трансляции, компоновщик использует свои правила видимости имён, которые приведены ниже:

  • Все имена, объявленные с классом памяти static, видимы только в пределах своей единицы трансляции и не влияют на компоновку.
  • Если некоторая единица трансляции использует внешнее имя (переменной или функции), которое не определено ни в какой единице трансляции, выдаётся сообщение об ошибке.
  • Если несколько единиц трансляции определяют нестатическую функцию с одним и тем же именем, выдаётся сообщение об ошибке.
  • Если некоторое нестатическое имя определяется и как переменная, и как функция, выдаётся сообщение об ошибке.
  • Если несколько единиц трансляции определяют нестатическую инициализированную переменную с одним и тем же именем, выдаётся сообщение об ошибке.
  • Если несколько единиц трансляции определяют переменную с одним и тем же именем, которая инициализируется не более чем в одной единице трансляции, все определения размещаются, начиная с одного адреса.

Последнее правило можно продемонстрировать на следующем примере. Предположим, что в трёх файлах определена переменная var следующим образом:


// Первый файл:
int var = 1;
int add1(void) {
    return var++;
} 

// Второй файл:
int var;
int add2(void) {
    return var += 2;
} 

// Третий файл
int var;
int add3(void) {
    return var+=3;
} 

Если все три единицы компиляции объединяются в одну программу, то переменная var каждого из трёх файлов будет располагаться по одному и тому же адресу, и каждая из трёх функций будет работать, по сути, с общей переменной. Чтобы предотвратить такое слияние переменных можно использовать явную инициализацию переменной var, тогда компоновщик выдаст сообщение об ошибке как показано ниже.


// Первый файл:
int var = 1;
int add1(void) {
    return var++;
} 

// Второй файл:
int var = 1; 
int add2(void) {
    return var += 2;
} 

// Третий файл
int var = 1; 
int add3(void) {
    return var+=3;
} 

Видимость глобальных переменных при компоновке можно также регулировать с помощью спецификаторов класса памяти extern и static. Спецификатор extern запрещает компилятору выделять память под переменную в данном модуле. Спецификатор static локализует область видимости переменной единицей трансляции, в которой она определена.

Программы из нескольких единиц трансляции 

Как правило, сложные программы состоят из нескольких исходных файлов, которые объединяются компоновщиком. При написании таких программ полезно следовать следующим рекомендациям

При группировке функций и переменных по исходным файлам логически сильно связанные функции объединяются в один исходный файл. Данная рекомендация соответствует способу реализации на Си парадигмы модульного программирования, предполагающего разбиение программы на независимые части – модули. Например, если речь идет о программе-редакторе табличных данных, функции обеспечивающие сохранение таблицы в файл, могут быть помещены в один исходный файл, функции, которые выводят на экран содержимое таблицы, – в другой файл, функции, которые анализируют ввод пользователя, – в третий.

Чем больше переменных объявлено в единице компиляции с классом памяти static вместо класса памяти по умолчанию, тем лучше. Программа может быть легче модифицирована, если доступ к данным всегда происходит с помощью вызовов функций. Чем меньше "чужих" переменных использует некоторая единица компиляции, тем она проще для понимания.

Для каждого .c файла должен существовать интерфейсный файл с тем же именем, но суффиксом .h, в котором определяются переменные, функции, типы данных и пр., которые могут использоваться извне данной единицы компиляции.

Исходный .c файл должен обязательно подключать свой собственный .h файл. В этом случае транслятор обнаружит рассогласования между объявлениями в .h-файлах и определениями в .c файле.

Интерфейсный .h файл должен быть обязательно защищён от повторного включения т.н. стражем включения (англ., include guard):


#ifndef __NAME_H__
#define __NAME_H__
    <здесь находится текст файла>
#endif 

Здесь NAME – это имя файла (без суффикса). Некоторые .h- файлы могут включать другие .h файлы, поэтому, когда программа становится большой, человек не может отследить, какие файлы уже включались, а какие – ещё нет. При использовании стражей включения можно не задумываясь включать в .c файл все заголовочные файлы, необходимые данной единице компиляции. Защита от повторного включения предотвращает появление ошибок о переопределённых типах, переменных и функциях.

В заголовочном файле помещаются макроопределения и типы данных, являющиеся интерфейсом данной единицы компиляции, то есть необходимые для использования функций и переменных этой единицы компиляции. С классом памяти extern помещаются необходимые переменные и прототипы функций, объявленные в соответствующей единице компиляции.

В заголовочный файл никогда не помещаются определения переменных с классом памяти, отличным от класса extern, и тела функций. В заголовочный файл никогда не помещаются прототипы функций с классом памяти static. Если некоторый тип или константа используются только в теле какой-либо функций и не требуется для корректного использования функциональности данной единицы компиляции другими модулями, этот тип или константа также не помещаются в заголовочный файл.