Содержание


Инструменты ОС Linux для разработчиков приложений для ОС Windows. Часть 5. Работа с компилятором GCC и управление библиотеками

Comments

Хотя основным компилятором на платформе Linux является GCC, но кроме него могут использоваться и другие варианты, например:

  • компилятор CC из состава IDE SolarisStudio;
  • развивающийся в рамках проекта LLVM компилятор Clang, который должен заменить GCC в ОС FreeBSD по причине несовместимости лицензий;
  • PCC (Portable C Compiler) — новая реализация компилятора 70-х годов, широко практикуемого в NetBSD и OpenBSD.

Тонкость заключается в том, что GCC имеет дополнительные синтаксические расширения (главным из которых являются инлайновые ассемблерные вставки), которые не распознаются другими компиляторами. Поэтому альтернативные компиляторы могут использоваться для сборки приложений, но непригодны для пересборки ядра Linux и сборки модулей ядра.

Первая реализация компилятора GCC была представлена Ричардом Столлманом GCC в 1985 году на нестандартном и непереносимом диалекте языка Паскаль. Но позднее компилятор был переписан на языке Си Леонардом Тауэром и Ричардом Столлманом и выпущен в 1987 как компилятор для проекта GNU.

The GNU Compiler Collection includes front ends for C, C++, Objective-C, Fortran, Java, Ada, and Go, as well as libraries for these languages (libstdc++, libgcj,...).

Компилятор GCC работает по принципу 2-ух фазной компиляции:

  1. на начальной стадии (front end) лексического анализа, в зависимости от языка программирования, происходит синтаксическое распознавание исходного кода и преобразование его в структуры на основе деревьев и промежуточное RTL-представление, напоминающее S-выражения языка LISP.
  2. конечная стадия (back end) генерации кода получает с предыдущей стадии языково-независимые RTL-инструкции и создает исполняемый код для заданной платформы.

В GCC существуют дополнительные лексические анализаторы для языков Pascal, D, Модула-2, Modula-3, Mercury, VHDL и PL/1.

Рассмотрим простейшую программу на языке С:

#include <stdio.h>
int main( int argc, char *argv[] ) {
   printf( "Hello, world!\n" );
};

И выполним её сборку, как показано ниже:

$ gcc hello_world.c
$ ls -l
-rwxrwxr-x 1 olej olej 4735 Мар 19 15:44 a.out
-rw-rw-rw- 1 olej olej   94 Мар 19 15:48 hello_world.c
$ g++ hello_world.c
$ ls -l
-rwxrwxr-x 1 olej olej 5180 Мар 19 15:49 a.out
-rw-rw-rw- 1 olej olej   94 Мар 19 15:48 hello_world.c

В предпоследней команде мы скомпилировали эту программу, но компилятором g++ для языка C++, и по размерам выходного файла видно, что результаты компиляции различаются. Но главное различие заключается в том, что код, откомпилированный различными компиляторами (C или C++), будет использовать совершенно разные разделяемые библиотеки периода выполнения.

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

Компилятор распознаёт язык программирования, на котором был написан файл исходного кода, по расширению файла, например: *.c — C, *.cc — C++, *.S — ассемблер в AT&T нотации (не Intel!). Но используемый язык программирования можно определить и ключом -x <язык>. Поэтому в одной команде GCC в качестве входных файлов могут сочетаться файлы различных форматных представлений (исходные файле на языке С, на языке ассемблера, и даже объектные):

$ gcc f1.c f2.S f3.o —o resfile

Также GCC может выполнить частичную обработку, подготовив результат в зависимости от указанного ключа:

  • -c — только компилировать в объектный формат (не вызывать компоновщик):
    $ gcc fin.c -c —o fout.o
  • -S — компилировать в ассемблерный код:
    $ gcc fin.c -S —o fout.S
  • -E — только выполнить препроцессорную обработку и разрешить макросы:
    $ gcc fin.c -E —o fout.c

Дополнительную информацию можно найти в справочной системе GCC или в справочнике man:

$ gcc --version
gcc (GCC) 4.1.2 20071124 (Red Hat 4.1.2-42)
$ gcc --help
Синтаксис: gcc [ключи] файл...
...
$ man gcc
GCC(1)                                GNU                               GCC(1)
NAME
       gcc - GNU project C and C++ compiler
...

Запустим нашу программу и убедимся в её работоспособности:

$ gcc hello_world.c -o hello_world
$ ./hello_world
Hello, world!

Библиотеки

В ОС Linux библиотеки являются важнейшим компонентом любого программного обеспечения (аналогичная ситуация и в ОС Windows, где практически всё ПО является DLL-библиотеками). По непонятным причинам, различные аспекты работы с библиотеками в ОС Linux мало описаны в существующей документации, но мы постараемся исправить это упущение.

Использование библиотек

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

  1. раздельная компиляция помогает сократить время, затрачиваемое на компиляцию при внесении изменений в проект, за счёт чего ускоряется темп разработки;
  2. устраняется дублирование общих частей исходного кода, используемых в разных компонентах проекта (хотя этого можно добиться и включениями #include общих фрагментов, но это абсолютно разные способы реализации);
  3. упрощается внесение изменений в код и его сопровождение, а в итоге увеличивается "цельность" (consistency) проекта и сокращается число несоответствий при правках;

Библиотеки могут создаваться двумя альтернативными способами: как статические или как динамические (разделяемые), и в большинстве случаев с равным успехом может быть выбран любой из этих вариантов. Вся информация, представленная в последующих разделах, относится как к библиотекам, создаваемым непосредственно в ходе разработки проекта, так и к 3rd party библиотекам, предоставляемым независимыми разработчиками и используемыми в проекте. К 3rd party, кстати, относятся и многие библиотеки, устанавливаемые с самой операционной системой Linux.

Связывание библиотек

Как создавать собственные библиотеки мы рассмотрим позже, а сейчас необходимо разобраться, как использовать уже существующие библиотеки со своими приложениями. Чтобы воспользоваться библиотекой, объектный код приложения должен быть скомпонован с отдельными объектными модулями, извлекаемыми из библиотеки. Эта работа выполняется компоновщиком (линкером), входящим в состав программ проекта GCC. Для того чтобы подключить библиотеку к приложению используется команда ld, как показано ниже, но этот же шаг может выполняться и в сценарии Makefile:

$ gcc -c hello.c -o hello.o
$ ld /lib/crt0.o hello.o -lc -o hello

Первая команда компилирует (ключ -c) приложения (файл исходного кода hello.c) в объектный формат, а вторая команда вызывает компоновщик ld, который компонует полученный объектный файл с:

  • стартовым объектным модулем, указанным абсолютным именем /lib/crt0.o;
  • со всеми необходимыми объектными модулями функций из стандартной библиотеки С — libc.so;

Далее мы расскажем о том, как соотносятся имя библиотеки, указываемое в ключах сборки, и имя файла библиотеки.

Но обычно компоновщик вызывается неявно, из gcc с необходимыми по умолчанию параметрами. Вот эквивалентная запись предыдущего примера:

$ gcc -c hello.c -o hello.o
$ gcc hello.o -o hello

В данном имя стартового объектного модуля (/lib/crt0.o) и имя стандартной библиотеки С (libc.so) компилятор gcc передаст компоновщику ld, как параметры по умолчанию. К аналогичному результату приведёт и такая команда:

$ gcc hello.c -o hello

Где на одной строке сначала вызывается компилятор, а затем компоновщик.

В зависимости от того, библиотеки какого типа используются для сборки приложения, они могут (должны) компоноваться с приложением по разному (при этом способ компоновки определяется ключом -B):

  • статически:
    $ gcc -Bstatic -L<путь> -l<библиотека> ...
  • динамически:
    $ gcc [-Bdynamic] -L<путь> -l<библиотека> ...
  • или смешанным способом
    $ gcc -Bstatic -l<библиотека1> ... -Bdynamic -l<библиотека2> ...

Причём, при смешанной записи статические и динамические библиотеки могут чередоваться произвольное число раз:

$ gcc -Bstatic -l<библ1> <библ2> -Bdynamic -l<библ3> -l<библ4> -Bstatic -l<библ5>

Если способ связывания не определён в командной строке (не указан ключ -B), то по умолчанию предполагается динамический способ связывания.

Вернёмся к соотношению между именем библиотеки и собственно её файлом. В качестве значения опции -l указывается имя библиотеки, но сами библиотеки содержатся в файлах! Имена файлов, содержащих статические библиотеки, имеют расширение .a (archive), а файлов, содержащих динамические библиотеки - .so (shared objects). Полное имя файла библиотеки образуется конкатенацией префикса lib и имени библиотеки. Таким образом, если требуется скомпоновать программу prog.c с разделяемой библиотекой с именем xxx, то в путях поиска должен находиться путь к библиотечному файлу с именем libxxx.so, а записывается команда компиляции так:

$ gcc prog.c -lxxx -oprog

Если подобную операцию необходимо выполнить со статической библиотекой, то необходимо иметь библиотечный файл с именем libxxx.a, а команду компиляции записать уже так:

$ gcc prog.c -Bstatic -lxxx -oprog

Посмотреть на какие файлы разделяемых библиотек ссылается уже собранная бинарная (формата ELF) программа можно командой ldd:

$ ldd hello
        linux-gate.so.1 =>  (0x00f1b000)
        libc.so.6 => /lib/libc.so.6 (0x00b56000)
        /lib/ld-linux.so.2 (0x00b33000)

Здесь может удивить то, что будут указаны правильные имена используемых библиотек, но пути к ним будут отличаться от ожидаемых. Это связано с тем, что на самом деле используется не та версия библиотеки, что, соответственно, может привести и к неправильной работе программы.

Некоторую сложность могут вызывать следующие вопросы:

  1. где компоновщик ищет библиотеки при компоновке;
  2. где и как исполняемая программа ищет библиотеки для загрузки;
  3. как это всё грамотно определить при сборке.

Пути, которые по умолчанию используются для поиска библиотеки, можно найти в разделах справочника man для компоновщика ld. Обычно в таком случае поиск ведётся в каталогах /lib и /usr/lib.

В порядке приоритета компоновщик обходит для поиска библиотек следующие пути:

  1. пути, указанные ключом -L;
  2. пути, указанным списком в переменной окружения LD_LIBRARY_PATH;
  3. default'ные пути: /lib и /usr/lib;
  4. пути, сохраненные в кэше загрузчика (/etc/ld.so.cache)

При статической компоновке процесс поиска на этом заканчивается.

Посмотреть текущее содержимое кэша загрузчика можно следующим способом:

$ strings '/etc/ld.so.cache' | head -n8
ld.so-1.7.0
glibc-ld.so.cache1.1
libzrtpcpp-1.4.so.0
/usr/lib/libzrtpcpp-1.4.so.0
libzltext.so.0.13
/usr/lib/libzltext.so.0.13
...

Это один из предлагаемых в литературе, но не официальный способ, когда мы произвольно выделяем символьные строки из не символьной последовательности байт. Другой (легальный) способ посмотреть имена используемых библиотек с полными путями их размещения — это непосредственное использование утилиты для работы с библиотеками ldconfig; но нужно учесть, что объём информации, выводимый этой утилитой, может оказаться очень большим и поэтому стоит прибегнуть к фильтрации с помощью grep, как показано ниже:

$ ldconfig -p | head -n10
2341 библиотек найдено в кэше «/etc/ld.so.cache»  
1482 библиотек найдено в кэше «/etc/ld.so.cache»
	libzrtpcpp-1.4.so.0 (libc6) => /usr/lib/libzrtpcpp-1.4.so.0
	libzltext.so.0.13 (libc6) => /usr/lib/libzltext.so.0.13
...
$ ldconfig -p | grep libxml2
	libxml2.so.2 (libc6) => /usr/lib/libxml2.so.2
	libxml2.so (libc6) => /usr/lib/libxml2.so

В кеш загрузчика пути к библиотекам, попадают с помощью всё той же утилиты обновления (добавления) ldconfig из файла /etc/ld.so.conf. Однако в новых системах этот файл практически не содержит информации, так как включает в себя последовательно перечисленное содержимое всех файлов каталога /etc/ld.so.conf.d с расширением .conf. Поэтому, информация для обновления кэша накапливается из файлов этого каталога:

$ cat /etc/ld.so.conf
include ld.so.conf.d/*.conf
$ ls /etc/ld.so.conf.d
mysql-i386.conf  qt4-i386.conf  qt-i386.conf  usr-local-lib.conf  xulrunner-32.conf

Непосредственный путь для поиска библиотеки как раз и содержится в таком конфигурационном файле:

$ cat /etc/ld.so.conf.d/usr-local-lib.conf
/usr/local/lib

Таким способом каталог /usr/local/lib тоже попадает в пути для поиска библиотек.

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

$ ldconfig --help
Использование: ldconfig [КЛЮЧ...]
Конфигурирует связи времени выполнения
для динамического компоновщика.
...

Мы рассмотрели вопросы поиска любых (статических и динамических) библиотек для последующего связывания на этапе компоновки. Но для динамических библиотек потребуется выполнить поиск ещё раз, уже при запуске приложения, использующего библиотеку (которая, например, могла быть перенесена со времени сборки приложения). Такой поиск производится функциями (динамического линкера), содержащимися в динамической библиотеке /lib/ld-2.13.so (номер версии в имени файла может меняться), с которой по умолчанию компонуется любая программа, использующая разделяемые библиотеки. Библиотеки, необходимые приложению для работы, ищутся на путях, указанных в ключах -rpath и -rpath-link при сборке программы, а затем по значению путей в списке переменной окружения с именем LD_RUN_PATH. Далее проводится поиск по пунктам 3 и 4, описанным выше.

Использование динамических библиотек

Существует два различных метода использования динамических библиотек из приложения, впервые появившиеся в ОС Sun Solaris. Первый, традиционный способ, который мы уже рассматривали, – это динамическая компоновка приложения с совместно используемой библиотекой. При этом способе загрузка библиотеки при запуске приложения выполняется операционной системой.

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

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

  • dlopen(), которая принимает на вход строку с указанием имени библиотеки для загрузки и её загружает в память (если она еще не была загружена), или увеличивает количество ссылок на библиотеку (если она уже найдена в памяти). Возвращает дескриптор, который потом используется в функциях dlsym() и dlclose();
  • dlsym(), которая по имени функции возвращает указатель на ее код;
  • dlclose(), которая уменьшает счетчик ссылок на библиотеку и выгружает ее, если счетчик становится равным 0.
  • dlerror(), которая возвращает в приемлемом для человека текстовом формате описание ошибки в вызовах dlopen(), dlsym() или dlclose(), со времени последнего вызова dlerror().

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

  • RTLD_LAZY - разрешение адресов по именам объектов происходит только для используемых объектов (это не относится к глобальным переменным — они разрешаются непосредственно в момент загрузки);
  • RTLD_NOW - разрешение всех адресов происходит до возврата из функции;
  • RTLD_GLOBAL - разрешает использовать объекты, определенные в загружаемой библиотеке для разрешения адресов из других загружаемых библиотек;
  • RTLD_LOCAL - запрещает использовать объекты из загружаемой библиотеки для разрешения адресов из других загружаемых библиотек.
  • RTLD_NOLOAD - указывает не загружать библиотеку в память, а только проверить ее наличие в памяти. При использовании совместно с флагом RTLD_GLOBAL позволяет «глобализовать» библиотеку, предварительно загруженную локально (RTLD_LOCAL).
  • RTLD_DEEPBIND - помещает таблицу загружаемых объектов в самый верх таблицы глобальных символов. Это гарантирует использование только своих функций и нивелирует их переопределение функций другими библиотеками.
  • RTLD_NODELETE - отключает выгрузку библиотеки при уменьшении счетчика ссылок на нее до 0.

Заключение

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

В следующей статье мы рассмотрим, как собрать собственную библиотеку.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=968137
ArticleTitle=Инструменты ОС Linux для разработчиков приложений для ОС Windows. Часть 5. Работа с компилятором GCC и управление библиотеками
publish-date=04102014