Линковка с библиотеками linux

Компоновщики и загрузчики. Часть 2

Оригинал: Linkers and Loaders
Автор: Sandeep Grover
Дата: 26 ноября 2002
Перевод: Александр Тарасов aka oioki
Дата перевода: 4 декабря 2008

Это продолжение. Первую часть статьи можно прочесть здесь.

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

Статическая библиотека — это набор нескольких объектных файлов одного и того же типа. Библиотеки хранятся на диске в виде архива. Помимо самих объектных файлов этот архив содержит индексную информацию, по которой впоследствии будет легче найти определенные символы. Каждый ELF-архив начинается с магической последовательности из 8 символов: !\n , здесь \n — символ новой строки.

Статические библиотеки можно передавать компоновщику в виде аргументов командной строки, при этом из библиотек будут взяты лишь требуемые объектные модули, а лишние проигнорированы. В UNIX-системах библиотека libc.a содержит все стандартные функции языка C, включая printf и fopen, используемые большинством программ.

Библиотека libm.a — это стандартная математическая библиотека UNIX-систем. Она содержит объектные модули для вычисления квадратного корня, тригонометрических функций и т.д.

При использовании статических библиотек, в процессе разрешения символа компоновщик просматривает перемещаемые объектные файлы и архивы слева направо, как указано в командной строке. При этом компоновщик модифицирует три множества: множество O, перемещаемые объектные файлы, которые попадут в исполняемый файл; множество U, неразрешенные символы; и множество D, состоящее из символов, определенных в одном из обработанных модулей. В самом начале все три множества пусты.

  • Для каждого входного аргумента командной строки, компоновщик определяет, является ли он объектным файлом или архивом. Если вход — это перемещаемый объектный файл, тогда компоновщик добавляет его во множество O, обновляет множества U и D и переходит к следующему входному файлу.
  • Если на входе архив, компоновщик просматривает его содержимое на предмет наличия определений для неразрешенных до сих пор символов (из множества U). Если в каком-либо модуле библиотеки встречается ранее неразрешенный символ, то он добавляется во множество O, а множества U и D обновляются символами, найденными в этом модуле. Этот процесс выполняется для всех модулей библиотеки.
  • После обработки всех входных файлов по упомянутым двум шагам, если множество U содержит какой-либо элемент (т.е. есть неразрешенные символы), компоновщик выдает ошибку и завершает свою работу. Если же U пусто, тогда он объединяет и переразмещает объектные файлы в множестве O и собирает итоговый исполняемый файл.

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

Релокация

Итак, теперь компоновщик разрешил все символы, и каждый упоминаемый символ имеет точно одно определение. На данном этапе компоновщик начинает процесс релокации (переразмещения), включающий в себя следующие два шага:

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

Когда ассемблер встречает неразрешенный символ, он создает для него запись релокации и помещает ее в секции .relo.text / .relo.data . Запись релокации содержит информацию о том, как разрешить ссылку. Типичная запись релокации ELF содержит следующие элементы:

  • Смещение (Offset) — смещение релоцируемой ссылки внутри секции. Для перемещаемого объектного файла это означает байтовое смещение от начала секции до единицы хранения, которая подвергается релокации.
  • Символ — символ, на который должна будет указывать изменяемая ссылка. Это индекс производимой релокации в таблице символов.
  • Тип — тип релокации, обычно это R_386_PC32 , что означает относительную PC-адресацию. Значение R_386_32 означает абсолютную адресацию.
Читайте также:  Invalid handle как исправить windows 10

Компоновщик проходит по всем записям релокации, присутствующим в модулях и переразмещает неразрешенные символы, в зависимости от их типа. К примеру, для типа R_386_PC32 адрес релокации вычисляется как S+A-P; для типа R_386_32 адрес вычисляется как S+A. Здесь S означает значение символа из записи релокации, P — смещение секции либо адрес единицы хранения, которую переразмещаем (он вычисляется по значению смещения из записи релокации), а A — это адрес, который требуется для вычисления значения переразмещаемого поля.

Динамическое связывание: разделяемые библиотеки

У статических библиотек есть один существенный недостаток. Рассмотрим, к примеру, стандартные функции, такие как printf и scanf. Они используются во многих программах. Теперь представьте, что у вас работает 50-100 процессов, и каждый процесс хранит в памяти свою копию исполняемого кода для printf и scanf. Таким образом оперативная память расходуется по сути впустую. Здесь нам и пригодятся разделяемые библиотеки, устраняющие этот недостаток статических библиотек. Разделяемая библиотека — это объектный модуль, загружаемый по произвольному адресу памяти при запуске программы. Разделяемые библиотеки иногда называют разделяемыми объектами. В большинстве UNIX-систем они имеют расширение .so; в системе HP-UX используется расширение .sl, а в ОС от фирмы Microsoft они называются DLL (dynamic link libraries — библиотеки динамического связывания).

Чтобы получить разделяемый объект, компилятор должен быть вызван со специальным ключом:

Эта команда указывает компилятору, что нужно создать разделяемую библиотеку, libfoo.so , состоящую из объектных модулей a.o и b.o . Ключ -fPIC указывает компилятору на то, что нужно создать код, независимый от адреса (position independent code, PIC).

Теперь представьте, что объектный модуль с функцией main называется bar.o , и он зависит от объектных файлов a.o и b.o . В этом случае компоновщик нужно вызывать с помощью команды:

Будет создан исполняемый файл a.out , причем таким образом, что связывание с библиотекой libfoo.so будет происходить при запуске программы. Иными словами, a.out не будет содержать в себе кода объектных модулей a.o и b.o , в отличие от предыдущего случая, когда мы компилировали их в статическую библиотеку, а не в разделяемую. Исполняемый файл содержит информацию о релокации и таблицу символов, что позволяет программе разрешить ссылки на код и данные библиотеки libfoo.so во время ее запуска. Поэтому a.out можно назвать частично исполняемым файлом, ведь у него есть зависимость от libfoo.so . Также в файле хранится секция .interp , содержащая имя динамического компоновщика, который в Linux-системах сам по себе является разделяемым объектом ( ld-linux.so ). При загрузке программы в память загрузчик передает управление динамическому компоновщику. Динамический компоновщик содержит стартовый код, отображающий разделяемые библиотеки на адресное пространство программ. Далее выполняются следующие шаги:

  • переразмещает текст и данные библиотеки libfoo.so в сегмент памяти;
  • переразмещает все ссылки в a.out на символы, определенные в libfoo.so .

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

Загрузка разделяемых библиотек из приложений

Разделяемые библиотеки могут быть загружены из приложения не сразу, а в процессе ее выполнения. Приложение посылает запрос динамическому компоновщику на загрузку и связывание разделяемой библиотеки. При этом приложение даже может не содержать в себе вкомпилированных ссылок на эти разделяемые библиотеки. В Linux, Solaris и в других системах есть ряд функций, позволяющих таким образом загружать разделяемые объекты. В Linux существуют системные вызовы dlopen, dlsym и dlclose, позволяющие соответственно загрузить разделяемый объект, получить указатель на какой-либо содержащийся в нем символ, закрыть объект. В системах Windows есть функции LoadLibrary и GetProcAddress — это соответствующие замены вызовам dlopen и dlsym.

Программы для работы с объектными файлами

Ниже приведен список программ Linux, с помощью которых можно исследовать объектные/исполняемые файлы.

  • ar: создает статические библиотеки.
  • objdump: самый важный инструмент; предназначен для вывода всей инфорамции об объектном бинарном файле.
  • strings: выводит все печатаемые строки, хранящиеся в бинарном файле.
  • nm: выводит список символов, определенных в таблице символов объекта.
  • ldd: выводит список разделяемых библиотек, от которых зависит объект.
  • strip: удаляет таблицу символов.
Читайте также:  Файлы установки для mac os

Источник

Линковка с библиотеками linux

Как уже неоднократно упоминалось в предыдущей главе, библиотека — это набор скомпонованных особым образом объектных файлов. Библиотеки подключаются к основной программе во время линковки. По способу компоновки библиотеки подразделяют на архивы (статические библиотеки, static libraries) и совместно используемые (динамические библиотеки, shared libraries). В Linux, кроме того, есть механизмы динамической подгрузки библиотек. Суть динамической подгрузки состоит в том, что запущенная программа может по собственному усмотрению подключить к себе какую-либо библиотеку. Благодаря этой возможности создаются программы с подключаемыми плагинами, такие как XMMS. В этой главе мы не будем рассматривать динамическую подгрузку, а остановимся на классическом использовании статических и динамических библиотек.

С точки зрения модели КИС, библиотека — это сервер. Библиотеки несут в себе одну важную мысль: возможность использовать одни и те же механизмы в разных программах. В Linux библиотеки используются повсеместно, поскольку это очень удобный способ «не изобретать велосипеды». Даже ядро Linux в каком-то смысле представляет собой библиотеку механизмов, называемых системными вызовами.

Статическая библиотека — это просто архив объектных файлов, который подключается к программе во время линковки. Эффект такой же, как если бы вы подключали каждый из файлов отдельно.

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

Рассмотрим преимущества и недостатки статических и совместно используемых библиотек. Статические библиотеки делают программу более автономной: программа, скомпонованная со статической библиотекой может запускаться на любом компьютере, не требуя наличия этой библиотеки (она уже «внутри» бинарника). Программа, скомпонованная с динамической библиотекой, требует наличия этой библиотеки на том компьютере, где она запускается, поскольку в бинарнике не код, а ссылка на код библиотеки. Не смотря на такую зависимость, динамические библиотеки обладают двумя существенными преимуществами. Во-первых, бинарник, скомпонованный с совместно используемой библиотекой меньше размером, чем такой же бинарник, с подключенной к нему статической библиотекой (статически скомпонованный бинарник). Во-вторых, любая модернизация динамической библиотеки, отражается на всех программах, использующих ее. Таким образом, если некоторую библиотеку foo используют 10 программ, то исправление какой-нибудь ошибки в foo или любое другое улучшение библиотеки автоматически улучшает все программы, которые используют эту библиотеку. Именно поэтому динамические библиотеки называют совместно используемыми. Чтобы применить изменения, внесенные в статическую библиотеку, нужно пересобрать все 10 программ.

В Linux статические библиотеки обычно имеют расширение .a (Archive), а совместно используемые библиотеки имеют расширение .so (Shared Object). Хранятся библиотеки, как правило, в каталогах /lib и /usr/lib. В случае иного расположения (относится только к совместно используемым библиотекам), приходится немного «подшаманить», чтобы программа запустилась.

3.2. Пример статической библиотеки

Теперь давайте создадим свою собственную библиотеку, располагающую двумя функциями: h_world() и g_world(), которые выводят на экран «Hello World» и «Goodbye World» соответственно. Начнем со статической библиотеки.

Начнем с интерфейса. Создадим файл world.h: Здесь просто объявлены функции, которые будут использоваться.

Теперь надо реализовать серверы. Создадим файл h_world.c: Теперь создадим файл g_world.c, содержащий реализацию функции g_world(): Можно было бы с таким же успехом уместить обе функции в одном файле (hello.c, например), однако для наглядности мы разнесли код на два файла.

Теперь создадим файл main.c. Это клиент, который будет пользоваться услугами сервера:

Теперь напишем сценарий для make. Для этого создаем Makefile: Не забывайте ставить табуляции перед каждым правилом в целевых связках.

Осталось только проверить, работает ли программа и разобраться, что же мы такое сделали:

Итак, в приведенном примере появились три новые вещи: опции -l и -L компилятора, а также команда ar. Начнем с последней. Как вы уже догадались, команда ar создает статическую библиотеку (архив). В нашем случае два объектных файла объединяются в один файл libworld.a. В Linux практически все библиотеки имеют префикс lib.

Читайте также:  File windows system32 ntoskrnl exe error code 0xc0000098

Как уже говорилось, компилятор gcc сам вызывает линковщик, когда это нужно. Опция -l, переданная компилятору, обрабатывается и посылается линковщику для того, чтобы тот подключил к бинарнику библиотеку. Как вы уже заметили, у имени библиотеки «обрублены» префикс и суффикс. Это делается для того, чтобы создать «видимое безразличие» между статическими и динамическими библиотеками. Но об этом речь пойдет в других главах книги. Сейчас важно знать лишь то, что и библиотека libfoo.so и библиотека libfoo.a подключаются к проекту опцией -lfoo. В нашем случае libworld.a «урезалось» до -lworld.

Опция -L указывает линковщику, где ему искать библиотеку. В случае, если библиотека располагается в каталоге /lib или /usr/lib, то вопрос отпадает сам собой и опция -L не требуется. В нашем случае библиотека находится в репозитории (в текущем каталоге). По умолчанию линковщик не просматривает текущий каталог в поиске библиотеки, поэтому опция -L. (точка означает текущий каталог) необходима.

3.3. Пример совместно используемой библиотеки

Для того, чтобы создать и использовать динамическую (совместно используемую) библиотеку, достаточно переделать в нашем проекте Makefile.

Внешне ничего не изменилось: программа компилируется, запускается и выполняет те же самые действия, что и в предыдущем случае. Изменилась внутренняя суть, которая играет для программиста первоочередную роль. Рассмотрим все по порядку.

Правило для сборки binary теперь содержит пугающую опцию -Wl,-rpath,. Ничего страшного тут нет. Как уже неоднократно говорилось, компилятор gcc сам вызывает линковщик ld, когда это надо и передает ему нужные параметры сборки, избавляя нас от ненужной платформенно-зависимой волокиты. Но иногда мы все-таки должны вмешаться в этот процесс и передать линковщику «свою» опцию. Для этого используется опция компилятора -Wl,option,optargs. Расшифровываю: передать линковщику (-Wl) опцию option с аргументами optargs. В нашем случае мы передаем линковщику опцию -rpath с аргументом . (точка, текущий каталог). Возникает вопрос: что означает опция -rpath? Как уже говорилось, линковщик ищет библиотеки в определенных местах; обычно это каталоги /lib и /usr/lib, иногда /usr/local/lib. Опция -rpath просто добавляет к этому списку еще один каталог. В нашем случае это текущий каталог. Без указания опции -rpath, линковщик «молча» соберет программу, но при запуске нас будет ждать сюрприз: программа не запустится из-за отсутствия библиотеки. Попробуйте убрать опцию -Wl,-rpath,. из Makefile и пересоберите проект. При попытке запуска программа binary завершится с кодом возврата 127 (о кодах возврата будет рассказано в последующих главах). То же самое произойдет, если вызвать программу из другого каталога. Верните обратно -Wl,-rpath. пересоберите проект, поднимитесь на уровень выше командой cd .. и попробуйте запустить бинарник командой world/binary. Ничего не получится, поскольку в новом текущем каталоге библиотеки нет.

Есть один способ не передавать линковщику дополнительных опций при помощи -Wl — это использование переменной окружения LD_LIBRARY_PATH. В последующих главах мы будем подробно касаться темы окружения (environment). Сейчас лишь скажу, что у каждого пользователя есть так называемое окружение (environment) представляющее собой набор пар ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ, используемых программами. Чтобы посмотреть окружение, достаточно набрать команду env. Чтобы добавить в окружение переменную, достаточно набрать export ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ, а чтобы удалить переменную из окружения, надо набрать export -n ПЕРЕМЕННАЯ. Будьте внимательны: export — это внутреннаяя команда оболочки BASH; в других оболочках (csh, ksh, . ) используются другие команды для работы с окружением. Переменная окружения LD_LIBRARY_PATH содержит список дополнительных «мест», разделенных двоеточиеями, где линковщих должен искать библиотеку.

Не смотря на наличие двух механизмов передачи информации о нестандартном расположении библиотек, лучше помещать библиотеки в конечных проектах в /lib и в /usr/lib. Допускается расположение библиотек в подкаталоги /usr/lib и в /usr/local/lib (с указанем -Wl,-rpath). Но заставлять конечного пользователя устанавливать LD_LIBRARY_PATH почти всегда является плохим стилем программирования.

Следующая немаловажная деталь — это процесс создания самой библиотеки. Статические библиотеки создаются при помощи архиватора ar, а совместно используемые — при помощи gcc с опцией -shared. В данном случае gcc опять же вызывает линковщик, но не для сборки бинарника, а для создания динамической библиотеки.

Источник

Оцените статью