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

UnixForum





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

На главную -> MyLDP -> Программирование и алгоритмические языки


Ulrich Drepper "Как писать разделяемые библиотеки"
Назад Оглавление Вперед

3.9. Межобъектные взаимосвязи

Часть интерфейса ABI также используется при взаимодействии между различными исполняемыми модулями и объектами DSO, которые созданы с использованием неопределенных ссылок. Требуется обеспечить, чтобы при запуске программы динамический компоновщик мог найти правильные объекты DSO. Статический компоновщик использует SONAME для DSO из записи, в которой указывается взаимозависимость между двумя объектами. Эта информация хранится в записях DT NEEDED в динамической секции объекта с неопределенными ссылками. Обычно это только имя файла без указания полного пути. Тогда во время запуска приложения задачей динамического компоновщика будет именно поиск нужного файла.

Хотя с этим может быть проблема. По умолчанию динамический компоновщик для того, чтобы найти объекты DSO, просмотривает только несколько каталогов (в Linux, ровно два каталога - /lib и /usr/lib). Можно добавить еще каталоги, если указать их имена в файле /etc/ld.so.conf, который всегда просматривается динамическим компоновщиком. Перед запуском приложения, пользователь может добавить в среду окружения процесса переменную среды LD LIBRARY PATH. Это еще один список каталогов, который будет просмотрен.

Но все это означает, что выбор каталогов находится под контролем администратора. Автор программы только косвенно может повлиять на эти параметры, указывая требования в документации. Но у программиста также есть способ указать путь непосредственно. Иногда это важно. Доступ к настройкам системы может быть недоступен одновременно для всех программ.

Для каждого объекта, объекта DSO, а также исполняемого модуля автор может определить "путь выполнения" ("run path"). Когда динамический компоновщик будет искать зависимости объекта, он будет использовать значение строки пути, которое определено в пути выполнения. Пути выполнения передаются в двух переменных, одна из которых считается устаревшей. Доступ к путям выполнения осуществляется через записи в динамической секции, представляющей собой поле с тегами DT RPATH и DT RUNPATH. Различие этих двух значений в том, когда они используются при поиске зависимостей. Значение DT RPATH используется первым прежде, чем будет просмотрен любой другой путь, в частности, перед тем, как просматривается путь, определенный в переменной среды окружения LD LIBRARY PATH. Это проблематично, поскольку пользователю не всегда предоставляется возможность переписать значение. Поэтому DT RPATH считается устаревшим вариантом. Введение нового варианта, DT RUNPATH, исправляет эту оплошность за счет того, что он используется после использования пути, указанного в LD LIBRARY PATH.

Если есть обе записи DT RPATH и DT RUNPATH, то первая запись игнорируется. Для того, чтобы добавить строку в путь выполнения, нужно для компоновщика указать параметр -rpath или -R. То есть, в командной строке gcc должно быть что-то вроде следующего

gcc -Wl,-rpath,/some/dir:/dir2 file.o

Это позволит добавить в путь выполнения два именованных каталога, причем в том порядке, в котором они присутствуют в командной строке. Если задается более одного параметра -rpath/-R, то отдельные параметры разделяются между собой двоеточиями. Порядок здесь снова тот же самый, что и в командной строке компоновщика. Из соображений совместимости с более старой версией компоновщика записи DT RPATH создаются по умолчанию. Чтобы также добавить запись DT RUNPATH, нужно воспользоваться параметром компоновщика --enable-new-dtags. В результате будут созданы обе записи DT RPATH и DT RUNPATH.

Есть целый ряд подводных камней, о которых надо помнить при использовании путей выполнения. Во-первых, пустой путь представляет текущий рабочий каталог (CWD) процесса в момент выполнения приложения. Можно построить такой пустой путь с помощью явного указания параметра (-Wl,-R, ""), но это также и опасно из-за того, что могут подряд идти два двоеточия, либо строка может начинаться или завершаться двоеточием. Это значит, что если задано значение пути выполнения, равное ":/home::/usr:", то поиск будет происходить в каталоге CWD, в домашнем каталоге, снова в каталоге CWD, в каталоге usr и, наконец, снова в каталоге CWD {Примечание 13}. Очень легко добавить такой пустой путь. В файлах makefile часто содержится что-то вроде следующего:

RPATH = $(GLOBAL\_RPATH):$(LOCAL\_RPATH)
LDFLAGS += -Wl,-rpath,$(RPATH)
Примечание 13: Динамический компоновщик, конечно, легко сможет избежать тройного прохода, так как после первого прохода ему будет известен результат.

Если одна из переменных GLOBAL RPATH или LOCAL RPATH является пустой строкой, то динамический компоновщик должен будет просмотреть текущий рабочий каталог. Поэтому когда конструируются строки, нужно всегда быть осторожным с пустыми строками.

Вторая проблема, связанная с путями выполнения, в том, никакой из путей, например, /usr/lib/someapp, не будет "перемещаемым". То есть, пакет не может быть установлен в другом месте без использования таких трюков, как создание символических ссылок из каталога /usr/lib/someapp в реально используемый каталог. Возможно использование относительных путей, но это крайне нежелательно. Возможно, это годится для приложения, которое всегда контролирует свой текущий рабочий каталог, но не для объектов DSO, используемых в более чем одном приложении, и использование относительного пути может привести к неприятностям, поскольку приложение может менять свой текущий рабочий каталог.

Выходом из этой ситуации является расширенный синтаксис для всех путей поиска (путей выполнения, но также для LD LIBRARY PATH). Если используется строка $ORIGIN, то в ней будет представлен абсолютный путь к каталогу с файлом, в котором указан этот путь выполнения. Одним из обычных случаев использования такой "лексемы динамически формируемой строки" (DST - "dynamic string token") является программа (обычно установленная в каталоге bin/), поставляемая с одним или с несколькими DSO, для которых она требуется и которые устанавливаются в соответствующий каталог lib/. Например, такими путями могут быть /bin и /lib или /usr/bin и /usr/lib. В таком случае в пути выполнения приложения должен указываться путь $ORIGIN/../lib, который, в нашем случае, будет раскрыт до только что упомянутых /bin/../lib и /usr/bin/../lib, соответственно. Таким образом действующий путь будет указывать на соответствующий каталог lib/.

Лексема $ORIGIN является не единственной ейся лексемой динамически формируемой строки. Динамическая библиотека GNU libc в настоящее время позволяет работать еще с двумя лексемами. Первая - $LIB, используемая на платформах, которые могут работать в 32- и в 64-разрядных режимах (в будущем, возможно, больше). На таких платформах для 32-разрядных двоичных модулей подставляемым значением будет lib, а для 64-битных двоичных модулей - lib64. Т.е. то, что в системных интерфейсах ABI указывается в качестве каталога для исполняемого кода. Если неизвестно, поддерживается ли на платформа более одного режима, то подставляемым значением будет lib. Таким образом, эта лексема упрощает запись файлов makefile, используемых при создании исполняемых файлов, т. к. не нужно знать об этом различии.

Последней лексемой динамически формируемой строки является $PLATFORM. Она заменяется динамическим компоновщиком на строку, зависящую от конкретной архитектуры, которая представляет собой (как это следует из ее названия) имя платформы. Например, на машинах IA-32, она, в зависимости от процессоров, будет заменена значением i386 или значением i686. Это позволит использовать лучше оптимизированные версии DSO, если это поддерживается процессором. В системе с GNU libc это, как правило, не нужно, поскольку динамический компоновщик по умолчанию самостоятельно ищет такие подкаталоги. Реально используемые правила достаточно сложны и здесь обсуждаться не будут. Важно помнить, что лексема $PLATFORM в действительности бесполезна; она присутствует, главным образом, для совместимости с динамическим компоновщиком системы Solaris.

Еще один аспект записей DT NEEDED, который стоит рассматривать в первую очередь, это то, насколько они необходимы. Особенно в случае, когда следуют описанному выше правилу использовать параметр компоновщика -z defs, многие проекты выдают имена всех видов DSO в командной строке компоновщика независимо от того, действительно ли необходимы DSO или нет в конкретном случае. Эта проблема усугубляется бесполезными оболочками такими, как pkg-config, которые во время компоновки только добавляют тонны зависимостей. Все это было правильным в те времена, когда компоновка выполнялась статически, но по умолчанию для DSO, которые появляются в командной строке компоновщика, это означает, что они всегда добавляются в результирующий список зависимостей, независимо от того, используются ли они на самом деле или нет. Чтобы определить, есть ли в исполняемом файле или в DSO такие ненужные зависимости, можно использовать следующий скрипт ldd:

$ ldd -u -r \
	/usr/lib/libgtk-x11-2.0.so.0.600.0
Unused direct dependencies:

		/usr/lib/libpangox-1.0.so.0
		/lib/libdl.so.2

Такие ссылки можно устранены вручную, если в командной строке компоновщика не указывать название DSO. Либо можно воспользоваться параметром компоновщика --as-needed. Если используется этот параметр, то все DSO, указанные в командной строке после этого параметра, будут добавляться в список зависимостей только в случае, если они действительно необходимы. Этот режим можно снова отключить с помощью параметра --no-as-needed. Таким образом, ленивый программист все еще может указывать все DSO, которые могут потребоваться в любое время. Всю тяжелую работу выполнит компоновщик.

Но этим не следует пользоваться бездумно. Если в списке прямых зависимостей больше нет DSO, то это означает, что изменился путь поиска символа. Если в искомом DSO имеется определение символа, которое также есть во втором DSO и второй DSO будет теперь находиться в пути поиска раньше первого, семантика программы может измениться. Однако, это редкий случай.


Предыдущий раздел:   Следующий раздел:
Назад Оглавление Вперед