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

UnixForum





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

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


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

1. Предисловие

В течение долгого времени программисты для того, чтобы можно было использовать часто используемые фрагменты кода, собирали этот код в виде библиотек. Благодаря этому экономилось время разработки и уменьшалось количество ошибок, поскольку повторно используемый код приходилось отлаживать только один раз. В системах, где одновременно работают десятки и сотни процессов, повторное использование кода на стадии компоновки программ является лишь частичным решением проблемы. Во многих процессах будут использоваться одни и те же фрагменты кода, импортируемые из библиотек. С помощью систем управления памятью, которые есть в современных операционных системах, принцип разделения кода можно также применять и во время выполнения программ. Он реализуется путем однократной загрузки кода в физическую память и повторному к нему обращению из многих процессов через виртуальную память. Библиотеки подобного вида называются разделяемыми библиотеками (shared libraries).

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

Главным рассматриваемым вопросом является двоичный формат. Это формат, который используется для описания кода приложения. Давно прошло то время, когда достаточно было только иметь дамп памяти. В системах со множеством процессов требуется идентифицировать различные части файла, содержащего программу, например, текст, данные и части, содержащие отладочную информацию. Для этого, сначала были введены двоичные форматы. В ранних системах Unix наиболее часто используемыми форматами были такие форматы, как a.out или COFF. Очевидно, что эти двоичные форматы разрабатывались без учета возможности использования разделяемых библиотек.

1.1. Немного истории

Двоичным форматом, первоначально используемым в Linux, был формат a.out. Когда стали внедряться разделяемые библиотеки, то для того, чтобы их можно было использовать условиях ограничений формата a.out, потребовалось принять определенные технологические решения. Главным ограничением было то, что не допускалось перемещение кода во время загрузки и после его загрузки. Разделяемые библиотеки должны были существовать на диске в том виде, в каком они использовались на стадии выполнения программы. Из-за этого возникло главное ограничение, относящееся к сборке и загрузке разделяемых библиотек: каждая разделяемая библиотека должна была иметь фиксированный адрес загрузки; в противном случае нельзя было бы создавать динамические библиотеки, которые не должны были перемещаться.

Надо было назначать фиксированные адреса загрузки и это должно происходить без перекрытий и конфликтов и с некоторым запасом на будущее, позволяющим увеличивать размер разделяемой библиотеки. Поэтому для выделения диапазонов адресов требовался единый централизованный механизм, что само по себе было серьезной проблемой. Однако все еще хуже: если взять современную систему Linux с ее многими сотнями динамически разделяемых объектов (Dynamic Shared Objects — DSO), адресное пространство и виртуальная память, доступная приложению, становятся сильно фрагментированными. Из-за этого требуется ограничивать размер блоков памяти, которые должны выделяться динамически, что создаст для некоторых приложений непреодолимые проблемы. Если бы это имело место сегодня, то, по крайней мере на 32-разрядных машинах, диапазона имеющихся адресов оказалось недостаточно для их назначения.

Мы все еще не перечислили всех недостатков разделяемых библиотек в формате a.out. Поскольку приложения, использующие разделяемые библиотеки, не должны были заново перекомпоновываться после изменения разделяемой библиотеки, которую они используют, то не должны были изменяться точки входа, то есть адреса функций и переменных. Это можно гарантировать только в случае, если точки входа хранятся отдельно от собственно кода, поскольку в противном случае должны жестко задаваться ограничения на размеры функций. В Linux решением была таблица заглушек функций, из которых происходил вызов фактической реализации функции. Статический компоновщик получал из специального файла (файл с расширением .sa) адрес каждой заглушки функции. Во время исполнения использовался файл, заканчивающийся на .so.X.Y.Z, который должен был соответствовать используемому файлу .sa. В результате, в свою очередь, требовалось, чтобы некоторая запись, находящаяся в таблице заглушек, всегда использовалась для одной и той же функции. Размещение таблицы надо было выполнять аккуратно. Добавление нового интерфейса означало расширение таблицы. Никогда нельзя было из таблицы удалять запись. Чтобы избежать использования старой разделяемой библиотеки с программой, скомпонованной с новой версией, некоторые записи нужно было хранить в приложении: из суффикса .so.X.Y.Z брались части X и Y имени и динамический компоновщик мог обеспечить соответствие минимальным требованиям.

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

  1. необходим был централизованный механизм распределения адресов;
  2. возможны были коллизии (скорее всего) с катастрофическими результатами;
  3. адресное пространство становилось сильно фрагментированным.

По всем этим и другим причинам в Linux на ранней стадии в качестве двоичного формата перешли к использованию формата ELF (Executable Linkage Format — формат исполняемых и компонуемых файлов). Формат ELF определяется на основе общей спецификации (gABI), к которой добавляются расширения (psABI), касающиеся конкретных процессоров. Оказалось, что амортизационные расходы были таким же, как и для формата a.out, но ограничения были сняты.


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