Использование импортируемых имен
Функции разделяемой библиотеки не могут прямо ссылаться на определяемые вне библиотеки имена, но существует способ обойти это ограничение: в области данных нужно определить указатель на это имя и обеспечить правильную его инициалазацию на этапе выполнения. Внешними (импортируемыми) могут быть как имена функций, так и имена переменных. Более того, импортируемыми могут быть имена, определяемые пользователем, определяемые в другой библиотеке, либо даже определяемые в самой разделяемой библиотеке. На рисунке, приведенном ниже, внешние имена _libc.ptr1 и _libc.ptr2 определяются пользователем, а _libc_malloc определяется внутри разделяемой библиотеки.
Далее мы рассмотрим некоторые вопросы использования внешних имен.
Внешние имена, определяемые вне библиотеки
Архивные библиотеки обычно состоят из перемещаемых файлов, содержащих неразрешенные внешние ссылки. Хотя разделяемая библиотека сборки и является архивом, нельзя забывать о разделяемой библиотеке выполнения, которая во многом похожа на выполняемый файл. В частности, разделяемая библиотека выполнения не может содержать неразрешенных внешних ссылок.
Следовательно, разделяемая библиотека должна обеспечивать адресацию всех имен, которые в ней используются, но не определяются, то есть импортировать эти имена. Очевидно, некоторые разделяемые библиотеки будут строиться на основе уже существующих архивных библиотек. В силу изложенных выше причин, не все модули архива целесообразно включать в разделяемую библиотеку. Если что-либо, необходимое библиотеке, останется вне ее, Вам придется определить указатель на соответствующее имя.
Внешние имена, которые пользователь мог бы переопределить
Разделяемые библиотеки могут объявлять свои внутренние имена импортируемыми. С первого взгляда это может показаться излишним, однако рассмотрим следующую проблему. Функции семейства malloc имеются в двух архивах, libc и libmalloc. Хотя команды ОС UNIX, как правило, используют malloc из libc, они могут пользоваться любой библиотекой, или же определять malloc самостоятельно.
Когда мы создавали разделяемую библиотеку языка C... |
У нас было три варианта работы с malloc. Во-первых, мы могли не включать malloc(3X) в библиотеку. malloc вызывается другими функциями библиотеки, поэтому она стала бы импортируемой. Недостаток этого варианта заключается в уменьшении экономии памяти. |
Наконец, объявив имя malloc(3X) импортируемым, мы могли бы, тем не менее, включить malloc(3X) в разделяемую библиотеку, что мы и сделали. Хотя malloc(3X) и находится в библиотеке, на нее нигде нет прямых ссылок. Если прикладная программа не переопределяет malloc(3X), как пользовательские, так и библиотечные обращения к ней используют библиотечную версию. Соответственно, если malloc переопределяется пользователем, все обращения направляются к его версии.
Можно обеспечить возможность переопределения всех без исключения функций библиотеки, Для этого нужно об,явить импортируемыми все имена, определяемые в библиотеке, в дополнение к тем, которые в библиотеке используются, но не определяются, Хотя это несколько уменьшает быстродействие и увеличивает затраты памяти, таким образом обеспечивается стопроцентная совместимость с существующими архивными библиотеками как во время редактирования внешних связей, так и на этапе выполнения.
Техника импорта имен
Пусть в разделяемой библиотеке нужно сделать malloc импортируемым. Далее слева приведен исходный текст для архива, а справа - переработанный текст для разделяемой библиотеки.
/* см. ниже pointers.c */
extern char *malloc(); extern char *(*_libc_malloc)();
export () export () { { . . . . . . p=malloc (n); p=(*_libc_malloc) (n); . . . . . . } }
Метод преобразования исходного текста очевиден, но все же нужно одновременно поддерживать два варианта исходного текста: для архивной и для разделяемой библиотеки. Несколько простых макроопределений обеспечат трансформацию текста на этапе трансляции, сделав исходные тексты совместимыми. Нужно только хранить две версии включаемого файла, содержащего макроопределения.
Чтобы указать препроцессору C-компилятора, в каком каталоге искать файл с макросами, можно использовать опцию -I команды cpp(1). Далее снова приведены два варианта исходного текста включаемого файла import.h, причем справа записан вариант для разделяемой библиотеки:
/* пустой файл */ /* Макросы для импортируемых имен: по одному на имя. */ . . . #define malloc (*_libc_malloc) . . .
Эти включаемые файлы позволяют использовать один и тот же исходный текст как для архивной, так и для разделяемой библиотеки, поскольку в последнем случае обеспечиваются косвенные ссылки на импортируемые имена.
Общий исходный текст:
#include "import.h"
extern char *malloc ();
export() { . . . p=malloc (n); . . . }
В случае использования разделяемого варианта import.h описание malloc() в действительности есть описание указателя _libc_malloc.
Другой вариант: поместить #include внутрь #ifdef:
#ifdef SHLIB # include "import.h" #endif
extern char *malloc ();
export() { . . . p=malloc (n); . . . }
сборки, она модифицирует файл pmalloc.o, добавив туда перемещаемые команды, соответствующие оператору присваивания:
_libc_malloc = &malloc;
Эти команды редактор внешних связей извлечет из разделяемой библиотеки сборки вместе с файлом pmalloc.o и поместит в выполняемый файл. Затем редактор разрешит внешние ссылки и соберет все фрагменты инициализации вместе. При запуске выполняемого файла стартовая процедура crt1 выполнит эти фрагменты, установив таким образом правильные значения библиотечных указателей.
Избирательное использование импортируемых имен
В каждом элементе архива лучше определять небольшое количество указателей на импортируемые имена. Это предотвращает включение в выполняемый файл лишних об,ектных модулей. Например, если в элементе архива определены три указателя, все три ссылки будут разрешены редактором внешних связей, даже если реально будет использоваться только одна из них.
Чтобы уменьшить количество дозагружаемых модулей, можно сделать несколько исходных файлов, определяющих указатели на импортируемые имена по одному, либо небольшими группами. Если импортируемая функция будет использоваться индивидуально, указатель на нее следует поместить в отдельный исходный файл (тогда он попадет в отдельный элемент архива), что позволит редактору внешних связей строить оптимальные выполняемые файлы.
Рассмотрим несколько примеров. В первом приближении можно определить все указатели в одном исходном файле pointers.c:
. . . int (*_libc_ptr1)() = 0; char *(*_libc_malloc)() = 0; int (*_libc_ptr2)() = 0; . . .
Чтобы соответствующие функции можно было бы использовать по отдельности, нужно иметь несколько исходных файлов, и, соответственно, несколько элементов архива. В каждом исходном файле должен определяться один указатель или небольшая группа совместно используемых указателей. Пример:
Файл | Содержимое |
ptr1.c | int (*_libc_ptr1)() = 0; |
pmalloc.c | char *(*_libc_malloc)() = 0; |
ptr2.c | int (*_libc_ptr2)() = 0; |