Подготовка исходного текста для разделяемой библиотеки
Все, что написано на языке C и годится для разделяемой библиотеки, может использоваться и в архивной библиотеке. Обратное, вообще говоря, неверно, так как в разделяемой библиотеке могут оказаться внешние (импортируемые) имена. Рекомендации этого раздела помогут Вам перерабатывать исходный текст функций для разделяемой библиотеки так, чтобы они становились лишь немного медленнее и больше по размеру, чем их версии для архивной библиотеки. Здесь, главным образом, освещаются вопросы организации данных, так как большинство проблем совместимости связано со структурами данных в разделяемых и архивных библиотеках.
Уменьшайте объем глобальных данных
В настоящее время все внешние переменные разделяемой библиотеки глобальны, то есть доступны прикладным программам, что может затруднить поддержку библиотеки. Вам необходимо стремиться уменьшить объем глобальных данных, следуя изложенным ниже советам.
Во-первых, используйте, где возможно, стековые (automatic) переменные. Это уменьшает объем статических данных и количество переменных, доступных прикладным программам.
Во-вторых, проверяйте, действительно ли та или иная переменная должна быть внешней. Статические переменные невидимы для прикладных программ, поэтому их адреса могут быть изменены в последующих версиях библиотеки. Только адреса внешних переменных должны оставаться постоянными.
В-третьих, не определяйте память для переменных при компиляции, а запрашивайте ее на стадии выполнения. Это важно по двум причинам. Уменьшается размер области данных библиотеки, что экономит память процессов, которые будут получать только ту память, которая им действительно нужна. Кроме того, размеры динамически размещаемых переменных могут изменяться без ущерба для совместимости (размеры статически размещаемых переменных нельзя изменить, не изменяя адресов других переменных).
Храните функции и глобальные данные в различных исходных файлах
Отделение команд от данных уменьшает вероятность перемещения данных. Если нужно определить новые экспортируемые переменные, их можно добавить в конец соответствующего файла, не меняя адресов уже определенных переменных.
Из архивной библиотеки редактор внешних связей извлекает отдельные элементы, поэтому программисты обычно помещают функцию и соответствующие ей данные в один и тот же исходный файл. Это хорошо только при условии, что адреса данных определяются в процессе редактирования связей, однако для разделяемых библиотек имеется ряд ограничений. Если внешние (экспортируемые) переменные разбросаны по различным модулям библиотеки, они перемешиваются с невидимыми пользователю данными. Изменение этих невидимых извне данных, например цепочки символов "hello" в следующем примере, приводит к смещению остальных данных, в том числе и экспортируемых.
Рассмотрим два варианта программы:
int head=0; int head=0; func() func() { { . . . . . . p="hello"; p="hello, world"; . . . . . . } } int tail=0; int tail=0;
Пусть относительный виртуальный адрес переменной head будет нулевым для обоих примеров. Текстовые константы тоже будут иметь одинаковые адреса, но их длины различны. Старый адрес tail будет равен 12, а новый - 20, поэтому, если предполагается, что переменная tail будет доступна извне библиотеки, две приведенные версии программы будут несовместимы.
Добавление новых экспортируемых переменных в разделяемую библиотеку может изменить адреса статических имен, но это не влияет на совместимость. Выполняемый файл не может прямо ссылаться на статические имена, поэтому он не зависит от их адресов. В связи с этим, следует собрать вместе все экспортируемые данные и разместить их выше (с меньшими адресами) статических (невидимых) данных. Для этого в списке объектных файлов файла спецификаций указывайте первыми файлы с глобальными данными:
#objects data1.o . . . lastdata.o text1.o text2.o . . .
Если модули с данными включать не первыми, безобидное, казалось бы, изменение, например, замена текстовой константы, может привести к потере работоспособности уже созданных выполняемых файлов.
Пользователи разделяемой библиотеки, независимо от организации исходных файлов библиотеки, получают всю ее область данных во время выполнения. Поэтому Вы можете без опасений хранить описания всех экспортируемых переменных в одном исходном файле. Разумеется, их можно хранить и в нескольких файлах.
Инициализируйте глобальные переменные
Инициализируйте экспортируемые переменные, в том числе и указатели на внешние (импортируемые) имена, хотя это и увеличивает размер разделяемой библиотеки выполнения. Это увеличение касается только файла самой библиотеки. Инициализация этих переменных дает дополнительную гарантию неизменности их адресов в будущем.
C-компилятор ОС UNIX V собирает неинициализированные переменные в отдельную секцию, а редактор внешних связей назначает им адреса в непредсказуемом порядке. Иными словами, порядок следования неинициализированных переменных может измениться после следующего редактирования внешних связей библиотеки. Порядок же инициализированных переменных остается неизменным, что помогает обеспечить совместимость будущих версий библиотеки с предыдущими.
Сохраняйте порядок следования функций в таблице переходов
Новые функции добавляйте в конец таблицы переходов. Создавая новый файл спецификаций библиотеки, старайтесь добиться совместимости с более ранними ее версиями. Для этого добавлять новые функции нужно так, чтобы адреса уже включенных функций не изменялись. В этом случае с новой версией поставляемой Вами библиотеки смогут без перередактирования работать все выполняемые файлы, созданные с предыдущей версией.