Он был великий эконом...
Скованные одной цепью
Потом положите в кастрюлю все, что нашли дома
Поваренная книга
Карта острова сокровищ
Подводная гора камней
Камень на шее
Мы строили, строили и наконец...
И кое-что еще
А может, не надо?

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

Он был великий эконом...

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

Решение проблемы дают разделяемые библиотеки. Разделяемые билиотеки представляют собой такой способ формирования библиотеки общего пользования, при помощи которого в ОС UNIX можно хранить в памяти всего одну копию требуемой библиотеки. Лучшим примером экономии, пожалуй, является стандартная С-библиотека ввода/вывода stdio.

Скованные одной цепью

Каждая программа так или иначе связана с окружением. В частности, она использует подпрограммы и переменные (например, errno), определенные за ее пределами и позволяет обращаться извне к собственным подпрограммам и переменным. Полностью собранная программа либо содержит машинные код, привязанный к определенным адресам памяти, либо полностью переместима и имеет базовый регистр для адресации данных, либо в процессе загрузки выполняет (самостоятельно или системными средствами) соответствующую настройку. В ОС UNIX обычно используется механизм виртуальной памяти; программа всегда настраивается на конкретные адреса, реальное распределение физической памяти (и вообще наличие требуемых подпрограмм, данных и физической памяти) определяется операционной системой. Поэтому и разделяемые библиотеки должны быть привязаны к какому-то строго фиксированному адресу. Этот адрес относится к виртуальному пространству процесса и не связан непосредственно с реальными адресами памяти. Для функционирования любой библиотеки необходимы какие-то локальные переменные, данные, которые не могут разделяться между процессами, но тоже должны иметь во всех процессах один и тот же виртуальный адрес. Поэтому память приобретает следующий вид:

Код программы А
Код программы А
Код программы В
Данные процесса А0
Данные процесса А1
Данные процесса В0
РБибл. С
РБибл. С
РБибл. С
Данные С для А0
Данные С для А1
Данные С для В0

Выполняемых код программ (например, А) и кодбибилиотек (в данном примере С) разделяется между всеми процессами. Данные же у всех процессов свои. При этом данные разделены на несколько порций, т. е. личные данные приложения, личные данные библиотеки. Увы, описанная схема таит в себе несколько проблем:

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

Б. Что делать, если понадобиться изменить библиотеку и исправить ряд ошибок, добавить новые возможности и т. п.? Понятно, чтопосле пересборки все адреса "поплывут". Ранее созданные программы, обращаясь по старым адресам, будут попадать на совершенно случайный код.

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

Для того, чтоб решить указанные проблемы, делается следующее:

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

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

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

fprintf(id, "%s", tempo);

можно заменить на

(*_addr_fprintf) (id,"%s", tempo);

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

#ifdef SHRLIB
#define fprintf (*_addr_fprintf)
.............
#endif

В соответствии с этим при компиляции с ключом - DSHLIB автоматически выполнится требуемое преобразование. (Впрочем, на этом пути встречаются подводные камни. К ним мы еще вернемся.)

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

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

Потом положите в кастрюлю все, что нашли дома

Если чуть повнимательнее последить за ходом выполнения какой-либо простой команды типа:

cc -# -o testik -O testtik.c

или

gcc -v -o testtik -o testik.c

можно обнаружить, что на этапе сборки возникают какие-то странные модули с названиями /lib/crt1.o и /lib/crtn.o.

Модуль /lib/crt1.o содержит определение главной точки входа _start (именно с нее начинается выполнение программы), установку требуемого значения в переменную Environ, последовательный запуск процедур _istart, setchrclass, main (это собственно и есть наша основная программа) и, наконец, exit с параметром, который вернет процедура main.

Именно _istart обеспечивает инициализацию всех необходимых разделяемых библиотек. Расскажем, как это делается.

Обычный объектный файл состоит из трех секций:

- .text-секция содержит код программы, который может разделяться между процессами;

- .data-секция содержит инициализированные данные; каждый процесс должен получить собственную копию этой секции;

- .bss-секция содержит инициализированные данные; каждый процесс должен получить собственную копию этой секции.

Если же выполнить команду:

dump -hv /lib/crt1/o

можно обнаружить, что этот файл содержит не три секции, как это бывает с обычным файлом, а четрые. Добавляется еще одна

.init-секция, которая содержит команды инициализации, необходимые для работы процедуры main.

Все секции .init, которые встречаются редактору связей, будут собраны вместе. Поскольку _istart содержится именно в секции .init модуля crt1.o, то их код будет выполнене до процедуры main. После этого модуль crtn.o, который содержит в секции .init команду возврата из подпрограммы, вернет упраление в crt1.o и оно перейдет к main.

Секции .init порождаются именно при создании разделяемых библиотек, и именно в них расположены те самые команды, которые настраивают ссылки на данные и процедуры за рамками разделяемых библиотек.

Полностью же собранная программа (например, с библиотекой libc_s.a):

gcc -o tmp -O tmp.c -lc_s

содержит следующий набор секций:

.text - разделяемый код программы (получается сложением кодов секций .init и .text);

.data - инициализированные данные;

bss - неинициализированные данные;

.f700000 - секция кода разделяемой библиотеки;

.f701000 - секция инициализированных данных разделяемой библиотеки;

.f701003 - секция неинициализированных данных разделяемой библиотеки;

.lib - секция с ссылкой на конкретный загружаемый файл с разделяемой библиотекой (в нашем случае /shlib/libc_s).

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

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

(Число разделяемых библиотек*3+1)
* Размер_описателя_секции
+ Размер_инициализированной_части
- Размер_данных_и_кода_библиотек

Инициализированная часть занимает, как правило, менее килобайта, а коды и данные библиотеки обычно состаляют десятки-сотни килобайт.

Поваренная книга

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

Для сборки разделяемой библиотеки необходимо подготовить:

- специально скомпилированные (так, как говорилось выше) объектные модули;

- объектный модуль для импортируемых функций и данных;

- файл спецификации.

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

/* Генерация имен в зависимости от типа компилятора */
#ifdef _GNUC_
#define GPPN(name) (*_libgpp_##name)
#else
#define GPPN(name) (*_libgpp_/* */name)
#endif
#ifdef SHRLIB /*Нужна генерация имен*/
#define cos GPPN(cos)
#define fprintf GPPN(fprintf)
#define _iob GPPN(_iob)
#define _filbuf GPPN(_filbuf)
. . .
#endif

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

#include "import.h"
int fprintf()=0;  /* Инициализация переменной */
double cos()=0
double sin()=0;
. . .

Зачем такой файл нужен? Благодаря явной инициализации, все переменные (_libgpp_fprintf и т. п.) окажутся в секции .data. Соответствующим образом разместив этот файл в списке сборки, мы добьемся того, что импортируемые имена попадут в самое начало этой секции. Это в конечном итоге означает, что у них будут фиксированные (не изменяющиеся при персборке библиотеки) адреса в адресном пространстве процесса, что необходимо для корректировки инициализации адресов в разделяемой библиотеке. Если мы хотим, чтобы пользователь библиотеки мог обращаться прямо к каким-либо общим данным, то надо либо разместить их за пределами библиотеки (этот вариант более правилен), либо расположить их сразу за переменными с адресами требуемых функций.

Файл специально определяет:

- где будет размещаться загружаемый код разделяемой библиотеки (например, /shlib/libc_s);

- какие адреса имеют секции .text и .data библиотеки;

- какие функции билиотеки должны быть доступны извне (все эти "входы" нумеруются с тем, чтобы зафиксировать их от изменений в дальнейшем);

- какие объектные модули участвуют в сборке библиотеки;

- как надо инициализировать переменные для импортируемых ресурсов (эта часть будет использовна для генерации секции .init);

- экспортируемые данные.

Фрагмент файла спецификации может выглядеть, например, так

#target                  /usr/lib/libgpp_s
#address                .text  0x75000000
#address                .data  0x75100000
#branch
     gppinit                1
. . .
#objects
     gppimport.o
     gppglobals.o
. . .
#init       gppimport.o
     _libgpp_fprintf    fprintf
     _libgpp_cos        cos
. . .
#export    linker
     gpiPCON
. . .

Наиболее интересна секция описания branch. В результате ее обработки будет получен модуль branchtab следующего вида:

gppinit:     jsr.l     _bt1
              .short   0
...........      jsr.l     _bt2
              .short   0

Здесь метка _bt1 (видимая только в пределах самой библиотеки) будет указывать на реальное положение функции gppinit. Извне функция тем самм получит фиксированный адрес, зависимость которого от ее номера в описании branch очень проста:

Адрес секции .text библиотеки + (n-1)*8 байт

Карта острова сокровищ

Адресное пространство загруженного в память и выполняющегося в ОС UNIX System V.3 (например, на рабочей станции Беста) выглядит следующим образом:

.text пустое пространство .data .bss куча (динамически выделяемая память: malloc и т. п.) пустое пространство .text-секция разделяемой библиотеки пустое пространство .data-секция разделяемой библиотеки .bss-секция разделяемой библиотеки (повторение секций других разделяемых библиотек) пустое пространство стек пустое пространство

При попытке обращения к "пустому пространству" (разделяющему секции с различным механизмом защиты памяти) генерируется ошибка ("memory fault"). Однако, пустое пространство над кучей (размер его управляется системным вызвовм sbrk()), а также над и под стеком может использоваться для отображения на области разделяемой памяти или на физические адреса (средствами ios-драйверов). Другие области отображать не рекомендуется.

Сама .text-секция при этом будет иметь следующий вид:

_start:
    команды инициализации адресов для разделяемых
    библиотек в виде:
    mov.l       &fprintf, _libgpp_fprintf
    . . .
    rts        (возврат к основной программе из /lib/crtn.o)
_start:
    инициализация стека, внешнего окружения и т. п.
    jsr           _istart (вызов инициализации библиотек)
    jsr           main (вызов основной программы)
    jsr           exit (окончание процесса)
    . . . .
    код основной программы и т. п.

Если разделяемые библиотеки не используются, то команда возврата rts окажется непосредственно по нулевому адресу.

Поэтому сначала будет выполнена инициализация стека и окружения, затем проинициализируются библиотеки, после чего выполняется сама программа.

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

- лишний переход при вызове функций из разделяемых библиотек;

- лишний уровень косвенности при вызове общих функций из библиотеки;

- лишний уровень косвенности при обращении к общим данным за пределами библиотеки.

Конечно, эти накладные расходы - просто микроскопические по сравнение с теми удобвствами, которым обладают разделяемые библиотеки.

Подводная гора камней

Какие неприятности могут подстерегать тех, кто компилирует, собирает или выполняет программы, использующие разделяемые библиотеки? К сожалению, таких ситуаций достаточно много. Обсуждаемые ниже примеры получены автором либо на собственном опыте, либо от В. Горбунова, который сделал разделяемые вариатны нескольких Х-библиотек.

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

fprintf(id,"%x %x
", i, j);

следующую строку

(*_addr_fprintf) (id,:%x %x
, i, j);

Первая строка вполне допустима с точки зрения языка С без описания функции printf, вторая же без описания _addr_fprintf недопустима. Если, однако, включаемый файл с подстановками указан в программе до файла stdio.h, то все необходимые определения (и, тем самым, преобразования) мы получим:

extern int . . . .  fprintf() . . . . ;

преобразуется в

extern int . . . . (*_addr_fprintf) () . . . . ;

Вторая неприятность, которая проявляется при компиляции, связана с применением сложно определяемых объектов. Особенно, если эти макросы определены через другие макросы. Примером может служить определение stderr (описатель стандартного протокола):

#define stderr         (&_iob[2])

Для того, чтобы корректно использовать stderr наших текстах, необходимо описать _iob. Поэтому необходим определенный анализ окружения для того, чтобы выделить требуемые внешние имена. Удобнее всего это сделать на этапе сборки, когда редактор связей выдаст все, что он о нас думает.

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

В файле /usr/include/sys/stat.h определена стуктура stat:

struct stat {
      dev_t st_dev;
. . . . .

Существует также системных вызов stat(2):

struct stat a;
. . . .
if(stat("/tmp", &a)) . . . .
. . . .

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

#define stat(x) (*_addr_stat) (x)

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

ExecFunc("/Tmp", stat);

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

Еще один нюанс демонстрирует следующая конструкция:

{
         int fprintf();
         k=fprintf(id, "%x", id);
}

Фрагмент предельно просто и не вызывает каких-либо подозрений. Увы, это очень опасное место. После подстановки выходит:

{
         int  (*_addr_fprintf) ();
         k = (*_addr_fprintf) (id, "%x", id);
}

Первая из этих строк из описания внешней функции превратилась в определение локальной неинициализированной переменной. Ну а следующая строка в силу этого определения представляет собой переход в никуда с соответствующим никудышным результатом. Ручное вмешательство в текст здесь обязательно. Можно поступить двояко: либо исключить описание из блока и вынести его за пределы функции (лучше всего использовать stdio.h), либо просто добавить extern:

  extern int fprintf();           /* Делаем внешней */
{
  extern int fprintf();           /* Аналогично */
  k = fprintf(id, "%x", id);
}

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

int UndefinedBeginProc(); /* Описание функции */
int (*BeginProc)() = UndefinedBeginProc;
int UndefinedBeginProc() /* Определение функции */
{
  . . . . .
}
int SubProg()
{
  . . . . .    if(BeginProc == UndefinedBeginProc)
                {    . . . .
                 }
  . . . . .
}

Предположим, что первые два описанияч находятся за пределам разделяемой библиотеки, а вторые два внутри. Что мы получим? Наше условное выражение не сможет правильно работать! Дело в том, что UndefinedBeginProc имеет различные адреса за пределами библиотеки и внутри ее. За пределами библиотеки (BeginProc) для UndefinedBeginProc используется адрес в таблице переходов, внутри же библиотеки используется реальный адрес. Это эффект невозможно обойти без вмешательства в текст программы. Как это можно сделать? Зная генерируемые команды, сравнение можно выполнить макросом следующего сорта ( х - имя переменной, у - явное имя):

#define MC680X0JUMP 0x4ef9
#define EQFUN(x,y) (x==y || 
             (short*)x == MC680X0JUMP && 
* (int *)((int)x+2) == (int)y )

(Данные макрос предназначен для MC680x0 и жестко ориентирован на способ организации таблицы переходов.)

Необходимо отметить еще один факт (автору о нем рассказал В. Горбунов). Если попытаться сгенерировать все требуемые файлы для импорта/экспорта данных и таблицы branch автоматически, то не все экспортируемые символы из секции .text следует прямиком отправлять в таблицу branch. Если среди экспортируемых символов будут аднные (например, строки), то внешняя программа, вероятно, предпочтет обращаться именно к данным, а не к какой-то там команде, передающей управление к этим данным.

Камень на шее

Следующая пробелма в создании библиотеки: что делать с переменными, которые проинициализированы адресами внешний объектов? Ясно, что обычная подстановки ничего не даст. Мы получим нестатическое выражение для инициализации переменной:

int (*f)() = (*_addr_fprintf);

Ожидать милости от компилятора не приходится. Возможны два решения: либо добавить соответствующую функцию непосредственно в библиотеку (например, в X Window можно слить несколько библиотек высокого уровня и убрать перекрестные ссылки - такой подход тем более правилен, поскольку библиотеки взаимозависимы), либо, если так поступить нельзя, то необходимо оперативное вмешательство в текст. В этом случае необходима инициализационная функция. Поэтому в инициализации переменной рекомендуется поставить просто NULL, написать функцию (или функции), которая инициализирует все переменные подобного рода, и вызывать ее в начале более общей функции инициализации:

SHLibInit()
{
     f = *_addr_fprintf;
}

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

text
jsr SHLibInit

ld может использовать для сборки специальный файл с описанием процесса сборки (см. напримерр, /lib/default.ld или /usr/src/uts/unildinstr). Пользуясь им, секции можно формировать весьма замысловато. В частности, после компиляции, можно слить воедино инициализацию библиотеки и полученный кусочек. Для этого надо определить, какие именно модули библиотеки содержат секцию .init. Если библиотека чужая, следует воспользоваться командой:

dump -hv library_name

Предположим, что мы обрабатываем объектный модуль import.o. Извлечем его из билиотеки и соберем новый вариант:

ar -x library_name import.o
cp import.o oldimport.o
ld -r -o newimport.o spec.spec import.o additional.o
mv newimport.o import.o
ar -rv library_name import.o

spec.spec должен иметь следующий вид:

SECTIONS
{
   GROUP :
   {
       .text : { }
       .data : { }
        .bss : { }
   }
   GROUP :
   {
       .init :(import.o).init)additional.o(.text))
   }
}

Эта спецификация требует перенести "добавления" из секции .text, куда они попали "по воле" ассемблера, в секцию .init. Аналогичным образом можно менять и другие библиотеки, в частности, стандартную библиотеку /lib/libc_s.a.

Мы строили, строили и наконец...

Что получается, когда разделяемая библиотека сгенерирована? Результат - это два файла. Один из них представляет собой собственно библиотеку, которую можно использовать для сборки. Второй содержит код, подгружаемый на этапе выполнения. При более внимательном изучении содержимого разделяемой библиотеки (например, /lib/lib_s.a) можно обнаружить модули нескольких типов:

- модули определения функций;

- модули инициализации переменных (определяются создателем библиотеки);

- модуль определения секций;

- другие модули (добавлены на правах обычных модулей).

Модули определения функций не содержат секций и имеют следующий состав (на примере atol.o):

1. Определение функций (как в обычной библиотеке) с абсолютными адресами (atol).

2. Определение имени вида /shlib/libc_s[atol.o], т. е. имя библиотеки с именем модуля.

3. Статические имена, игнорируемые на этапе сбороки (в данном модуле отсутствуют).

4. Неопределенные ссылки на другие модули в аналогичном формате, т. е. имя библиотеки и модуля /shlib/libc_s[stype_def.o].

5. Ссылки на неопределенные переменные (_libc_ctype), которые должны быть проинициализированы для того, чтобы данный модуль был работоспособен.

6. Неопределенный символ для самой библиотеки /shlib/libc_s[].

Неопределенные ссылки модулей обеспечивают поиск и подсоединение модулей, которые эти ссылки могут определить. Наличие общего неопределенного символа для библиотеки обеспечивает подсоединение модуля с определением секций библиотеки (только модуль /lib/libc_s.a[Xlibc_s] содержит секции .f700000, .f701000, .f701003 и .lob). Соответствующий модуль инициализации переменных содержит четыре секции. Все секции кроме секции .init пустые, .init содержит несколько команд инициализации. Поскольку модулей с инициализацией много, библиотека /lib/libc_s.a не требует присоединения к программе тех функций, которые используются функциями из разделяемой библиотеки, но для данной программы не нужны. Однако, если сгенерировать только один файл с инициализируемыми переменными, то всегда будет происходить подключение всех импортируемых функций, функий, вызываемых ими и т. д. Провильное разбиение инициализируемых переменных на файлы и соответствующий порядо генерации библиотеки позволяет избавиться от подобных накладных расходов.

И кое-что еще

Кроме экономии дискового пространства и оперативной памяти, использование разделяемых библиотек позволяет разделить ошибки библиотеки и ошибки остальной программы.

Действительно, библиотека пересобирается отдельно от программы, которая ее использует. Как следствие, все программы, собранные с данной библиотекой, будут работать и с новой версией библиотеки. Если в новой версии исправлены ошибки, то они исправяться во всех программах одновременно без какой-либо пересборки.

Серьезные изменения, исправления и расширения разделяемой библиотеки должны быть явно запланированы.

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

А. Нельзя изменять адреса экспортируемых функций.

Б. Нельзя изменять адреса экспортируемых констант (например, сообщений, размещенных в секции .text).

В. Нельзя изменять адреса экспортируемых данных.

Г. Нельзя изменять адреса переменных для импорта объектов извне.

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

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

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

А может, не надо?

Действительно, а нужен ли нам механимз разелядемых библиотек? И если нужен, то зачем?

Если библиотека используется всего в нескольких программах, да и те одновременно на комемпьютере не выполняются, или используют малую долю библиотеки, то при незначительном выигрыше дискового просстранства мы немного потеряем в оперативной памяти (таблицы переходов, фрагментация на уровне страниц, наличие лишних функций, данных и т. п.). Чтобы применение разделяемой библиотеки стало оправданным, она должна активно использоваться большим количеством программ (экономия дискового простарнства) и предпочтительно одновременно) экономия памяти). Поэтому наиболее удачным оказываются разделяемые библиотеки для поддержки языков программирования (С, С++ и других), иснтрументальных средств и баз данных, оконных графических систем (X Window) и т.п.

Сформулируем рекомендацию по отбору функций и данных-кандидатов в разделяемую библиотеку:

- функции должны быть большими и использоваться часто; редко используемые функции просто занимают пространство в памяти;

- желательно, чтобы функция содержалась вместе с теми, которые используются ею;

- желательно не увиличивать .data и .bss секции библиотеки за счет редко используемых данных; поэтому редко используемые функции с большими .data и .bss секциями являются первыми кандидатами на невключение; по этой же причине обычно лучше использовать динамическое, а не статическое выделение памяти;

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

Не следует забывать, что после того, как разделяемая библиотека собрана, с ней можзно обращаться при помощи утилиты ar, как с обычно библиотекой. Другими словами, ничто не мешает разделить функции на вставляемые и не вставляемые в разделяемую библиотеку, собрать вставляемые в библиотеку и добавть оставльные. Именно так, в частности, орагнизована и библиотека /lib/libc_s.a. Например, a641.o или bsearch.o добавлены на правах обычнх модулей, а chdir.o, atol.o, fgets.o являются частью разделяемой библиотеки. Эти данные импортируются библиотекой. Данный факт, кстати, можно использовать для борьбы с ограничениями библиотеки stdio, описанными в статье "Единожды солгавши..." в зимнем номере "Открытых систем" за 1994 год.

Поделитесь материалом с коллегами и друзьями